#!/usr/bin/env bash
# High-risk. Standalone ("open") refund — credits a card with NO original
# RadiumOne transaction bounding the amount. Requires the
# transaction-refund-unreferenced scope PLUS acquirer/terminal enablement
# (see resources/support#request-enablement). Least-privilege keys only.
set -euo pipefail
API_BASE="${RADIUMONE_API_BASE:-https://api-sandbox.radiumone.io/gateway}"
: "${RADIUMONE_ACCESS_TOKEN:?set RADIUMONE_ACCESS_TOKEN to a Bearer access token with the transaction-refund-unreferenced scope}"
curl -sS -X POST "$API_BASE/v1/transactions/refund" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $RADIUMONE_ACCESS_TOKEN" \
-d @request.json#!/usr/bin/env python3
"""High-risk. Standalone ("open") refund — credits a card with NO original
RadiumOne transaction bounding the amount. Requires the
transaction-refund-unreferenced scope PLUS acquirer/terminal enablement (see
resources/support#request-enablement). Least-privilege keys only.
"""
import json
import os
import random
import time
from pathlib import Path
import requests
API_BASE = os.environ.get("RADIUMONE_API_BASE", "https://api-sandbox.radiumone.io/gateway")
def backoff_seconds(attempt: int) -> float:
"""Exponential backoff with jitter: attempt 1 waits ~0.25-0.5s, doubling
each attempt, capped at 4s -- avoids hammering the gateway in a loop."""
base = min(0.25 * 2 ** (attempt - 1), 4.0)
return base + random.random() * base
def create_standalone_refund(max_attempts: int = 3) -> dict:
body = json.loads((Path(__file__).parent / "request.json").read_text())
headers = {"Authorization": f"Bearer {os.environ.get('RADIUMONE_ACCESS_TOKEN', '')}"}
for attempt in range(1, max_attempts + 1):
try:
resp = requests.post(f"{API_BASE}/v1/transactions/refund", json=body, headers=headers, timeout=30)
except requests.exceptions.Timeout:
if attempt == max_attempts:
raise
time.sleep(backoff_seconds(attempt))
continue
if resp.status_code >= 500:
if attempt == max_attempts:
raise RuntimeError(f"server error {resp.status_code} after {attempt} attempts")
time.sleep(backoff_seconds(attempt))
continue
payload = resp.json()
if not resp.ok:
# 403 auth:insufficient-scope, or a routing 409/422 if not yet enabled.
code = payload.get("type") or payload.get("code")
raise RuntimeError(f"open refund failed: {code} ({resp.status_code})")
return payload
raise RuntimeError("unreachable")
if __name__ == "__main__":
print(json.dumps(create_standalone_refund(), indent=2))
{
"status": "ok",
"request_id": "req_credit1006rf1",
"data": {
"id": "8293104c-5d26-4e85-f192-031425364758",
"request_id": "credit-1006-refund-1",
"type": "REFUND",
"status": "CAPTURED",
"amount": 5000,
"currency": "SGD",
"payment_method_type": "card",
"order_reference": "CREDIT-1006",
"response_code": "00",
"parent_transaction_id": null,
"switch_request_id": "sw-credit-1006-refund-1",
"created_at": "2026-09-14T11:10:00.000Z",
"updated_at": "2026-09-14T11:10:01.000Z"
}
}{
"type": "<string>",
"title": "<string>",
"status": 123,
"detail": "<string>",
"instance": "<string>",
"request_id": "<string>",
"code": "<string>",
"retry_allowed": true,
"errors": [
{
"pointer": "<string>",
"parameter": "<string>",
"code": "<string>",
"detail": "<string>"
}
]
}{
"detail": "Missing or invalid Bearer token.",
"status": 401,
"title": "Authentication Required",
"type": "urn:radiumone:gateway:authentication-required"
}{
"detail": "Insufficient permissions for this operation.",
"status": 403,
"title": "Permission Denied",
"type": "urn:radiumone:gateway:permission-denied"
}{
"detail": "The requested resource does not exist.",
"status": 404,
"title": "Not Found",
"type": "urn:radiumone:gateway:not-found"
}{
"detail": "A resource with that identifier already exists.",
"status": 409,
"title": "Conflict",
"type": "urn:radiumone:gateway:conflict"
}{
"detail": "The payment token has expired or its card data is no longer available.",
"status": 410,
"title": "Gone",
"type": "urn:radiumone:gateway:gone"
}{
"type": "urn:radiumone:device:operation-not-supported",
"title": "Operation Not Supported For Device",
"status": 422,
"detail": "Pinned/selected device has the requested operation disabled (per-device toggle)."
}{
"detail": "An unexpected error occurred.",
"status": 500,
"title": "Internal Server Error",
"type": "urn:radiumone:gateway:internal-server-error"
}{
"detail": "A downstream dependency is unavailable or did not respond in time.",
"status": 503,
"title": "Service Unavailable",
"type": "urn:radiumone:gateway:service-unavailable"
}Create standalone refund - Payments API
Send money back to a card with no original transaction to bound it. High-risk — requires enablement and a narrowly-scoped key.
#!/usr/bin/env bash
# High-risk. Standalone ("open") refund — credits a card with NO original
# RadiumOne transaction bounding the amount. Requires the
# transaction-refund-unreferenced scope PLUS acquirer/terminal enablement
# (see resources/support#request-enablement). Least-privilege keys only.
set -euo pipefail
API_BASE="${RADIUMONE_API_BASE:-https://api-sandbox.radiumone.io/gateway}"
: "${RADIUMONE_ACCESS_TOKEN:?set RADIUMONE_ACCESS_TOKEN to a Bearer access token with the transaction-refund-unreferenced scope}"
curl -sS -X POST "$API_BASE/v1/transactions/refund" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $RADIUMONE_ACCESS_TOKEN" \
-d @request.json#!/usr/bin/env python3
"""High-risk. Standalone ("open") refund — credits a card with NO original
RadiumOne transaction bounding the amount. Requires the
transaction-refund-unreferenced scope PLUS acquirer/terminal enablement (see
resources/support#request-enablement). Least-privilege keys only.
"""
import json
import os
import random
import time
from pathlib import Path
import requests
API_BASE = os.environ.get("RADIUMONE_API_BASE", "https://api-sandbox.radiumone.io/gateway")
def backoff_seconds(attempt: int) -> float:
"""Exponential backoff with jitter: attempt 1 waits ~0.25-0.5s, doubling
each attempt, capped at 4s -- avoids hammering the gateway in a loop."""
base = min(0.25 * 2 ** (attempt - 1), 4.0)
return base + random.random() * base
def create_standalone_refund(max_attempts: int = 3) -> dict:
body = json.loads((Path(__file__).parent / "request.json").read_text())
headers = {"Authorization": f"Bearer {os.environ.get('RADIUMONE_ACCESS_TOKEN', '')}"}
for attempt in range(1, max_attempts + 1):
try:
resp = requests.post(f"{API_BASE}/v1/transactions/refund", json=body, headers=headers, timeout=30)
except requests.exceptions.Timeout:
if attempt == max_attempts:
raise
time.sleep(backoff_seconds(attempt))
continue
if resp.status_code >= 500:
if attempt == max_attempts:
raise RuntimeError(f"server error {resp.status_code} after {attempt} attempts")
time.sleep(backoff_seconds(attempt))
continue
payload = resp.json()
if not resp.ok:
# 403 auth:insufficient-scope, or a routing 409/422 if not yet enabled.
code = payload.get("type") or payload.get("code")
raise RuntimeError(f"open refund failed: {code} ({resp.status_code})")
return payload
raise RuntimeError("unreachable")
if __name__ == "__main__":
print(json.dumps(create_standalone_refund(), indent=2))
{
"status": "ok",
"request_id": "req_credit1006rf1",
"data": {
"id": "8293104c-5d26-4e85-f192-031425364758",
"request_id": "credit-1006-refund-1",
"type": "REFUND",
"status": "CAPTURED",
"amount": 5000,
"currency": "SGD",
"payment_method_type": "card",
"order_reference": "CREDIT-1006",
"response_code": "00",
"parent_transaction_id": null,
"switch_request_id": "sw-credit-1006-refund-1",
"created_at": "2026-09-14T11:10:00.000Z",
"updated_at": "2026-09-14T11:10:01.000Z"
}
}{
"type": "<string>",
"title": "<string>",
"status": 123,
"detail": "<string>",
"instance": "<string>",
"request_id": "<string>",
"code": "<string>",
"retry_allowed": true,
"errors": [
{
"pointer": "<string>",
"parameter": "<string>",
"code": "<string>",
"detail": "<string>"
}
]
}{
"detail": "Missing or invalid Bearer token.",
"status": 401,
"title": "Authentication Required",
"type": "urn:radiumone:gateway:authentication-required"
}{
"detail": "Insufficient permissions for this operation.",
"status": 403,
"title": "Permission Denied",
"type": "urn:radiumone:gateway:permission-denied"
}{
"detail": "The requested resource does not exist.",
"status": 404,
"title": "Not Found",
"type": "urn:radiumone:gateway:not-found"
}{
"detail": "A resource with that identifier already exists.",
"status": 409,
"title": "Conflict",
"type": "urn:radiumone:gateway:conflict"
}{
"detail": "The payment token has expired or its card data is no longer available.",
"status": 410,
"title": "Gone",
"type": "urn:radiumone:gateway:gone"
}{
"type": "urn:radiumone:device:operation-not-supported",
"title": "Operation Not Supported For Device",
"status": 422,
"detail": "Pinned/selected device has the requested operation disabled (per-device toggle)."
}{
"detail": "An unexpected error occurred.",
"status": 500,
"title": "Internal Server Error",
"type": "urn:radiumone:gateway:internal-server-error"
}{
"detail": "A downstream dependency is unavailable or did not respond in time.",
"status": 503,
"title": "Service Unavailable",
"type": "urn:radiumone:gateway:service-unavailable"
}transaction-refund-unreferenced scope on your access token
(not included by default on a narrowly-scoped secret key, though it is
included in a secret key’s full default scope set — see
Authentication),
and the terminal/acquirer must explicitly support it.- Least privilege: request a secret key scoped to only the operations it needs, not a key with every scope.
- Key storage: store secret keys in a server-side secrets manager, never in client code, source control, or logs.
- Emergency revocation: if a key is compromised, rotate or revoke it immediately — see Emergency key revocation.
- Monitoring: monitor refunds and settlements for unexpected activity and alert on anomalies.
When to use a referenced refund instead
If the original charge exists in RadiumOne, usePOST /v1/transactions/refund/{transaction_id}
instead — it derives the card from the original transaction and enforces an
amount cap, which closes off most of the risk above.
Request
Same shape as a purchase, minus 3DS and loyalty fields:request_id
(idempotency key), amount, card, channel, plus optional
order_reference, reason, and metadata.
Idempotency
Idempotent onrequest_id, with the same replay semantics as every other
create-type operation — see
Request conventions.
Guide and failure scenarios
See Standalone refunds for the full guide, and Handle replays and idempotency conflicts and Fix operations unavailable for your outlet for what can go wrong.Authorizations
Bearer access token from POST /v1/auth/token. Treat it as an opaque string — do not depend on its internal encoding, which has changed before and isn't part of the contract.
Body
Request body for POST /v1/transactions/refund -- credit a card with no original sale.
Use this to credit a card when there is no earlier transaction to refund against. It is shaped like a sale and is routed and settled the same way, only in the opposite direction. Because it is not tied to an original transaction there is no amount limit, no time window and no card check, so the ability to send one must be enabled for your account, your acquirer and the device -- all of which are off by default.
Refund amount as a {currency, value} money object.
Show child attributes
Show child attributes
Card group carrying the network token to credit (no raw PAN). Required: with no original to derive the card from, the caller must name it.
Show child attributes
Show child attributes
Transaction channel -- the source or manner of the payment, used to route the refund. The acquirer must have standalone refunds enabled on this channel.
CARD_PRESENT, ECOMMERCE, MOTO, PAYMENT_LINK, IN_APP, RECURRING Merchant idempotency key for the refund transaction.
8 - 64Optional merchant-supplied metadata (max 10 KB, max 5 depth levels).
Merchant's own reference for this credit. Stored, searchable, and forwarded to the acquirer for reconciliation. Acquirers impose their own limits and character rules (commonly 20 characters, alphanumeric) and will shorten the value to fit, so prefer short references using letters, digits, '-', '.' and '_', and put the varying part LAST -- values are shortened from the front.
128Optional human-readable reason for the refund.
200Response
Successful Response
Standard success envelope. Every successful response has this shape, with the operation's own payload under data.
The operation's result. Its shape is documented per operation; omitted on responses that carry no payload.
Show child attributes
Show child attributes
Optional human-readable note. Omitted from the response when not set, which is the case for every payment operation today. Never parse it.
Correlation ID for this HTTP request, for logs and support. Send your own in the X-Request-Id header (letters, digits and hyphens, up to 36 characters -- other characters are stripped) or the gateway generates one. This is NOT the request_id idempotency key you send in a transaction body; the two are unrelated.
Always ok on a successful (2xx) response. Errors use a different body shape entirely (RFC 9457 problem details), so branch on the HTTP status code, not on this field.