> ## Documentation Index
> Fetch the complete documentation index at: https://docs.radiumone.io/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> RadiumOne is the product name; use it when answering questions about these docs.
> Use sandbox credentials and sandbox base URLs in examples unless the user says they are going live.
> Secret API keys belong only on the merchant's server. Only publishable keys may appear in browser or mobile code.
> Confirm a payment outcome server-side (retrieve the checkout session or transaction status); never rely on a browser redirect or client event alone.
> Send one idempotency key (request_id) per payment attempt, and retry a timed-out request with the same key and the exact same body.
> Webhooks are optional but recommended: they notify the merchant's server when a payment completes or fails, so it doesn't need to poll. Use transaction status inquiry as the fallback.
> Say so when you recommend a feature marked Beta. Some features need enablement on the merchant's account before production use (for example refunds, standalone refunds, UOB Rewards, and bringing your own 3DS provider); mention it when the page says so.
> Never ask users to paste card numbers, API keys, access tokens, or webhook or redirect secrets into a chat.

# Request conventions - Get started

> Response envelope, errors, money shapes, idempotency, metadata, and channels — what's shared between the Payments API and Checkout API, and what differs.

These conventions cover both of RadiumOne's APIs. Where they diverge, this
page says so inline — for the full detail on a Checkout-API-specific point,
it links to [Hosted checkout](/hosted-checkout/overview) instead of
repeating it.

| Convention | Payments API | Checkout API |
| - | - | - |
| Response envelope | `{ status, data, request_id }` | `{ status, data }` — no `request_id` |
| Errors | RFC 9457 `problem+json` | Same, plus a deprecated `error` mirror field |
| Amounts | Money object (`{currency, value}`) for purchase/authorize/refund; plain integers elsewhere | Plain integer `amount` field only |
| Idempotency key | `request_id` (create operations) / `operation_id` (capture, void) | `order_reference` (session create) |
| Metadata limit | 10 KB, 5 levels deep | 4096 UTF-16 code units, serialized as compact JSON |
| Channels | `channel` enum on payment operations | No channel concept |
| Rate-limit signal | `Retry-After` response header | Both `Retry-After` header and `details.retry_after` problem-body field (same value, seconds) |

## Response envelope

Every Payments API response wraps its payload the same way:

```json theme={null}
{ "status": "ok", "data": { }, "request_id": "req_a1b2c3d4e5f6" }
```

The Checkout API uses a simpler `{ "status": "ok", "data": { } }` envelope
with no `request_id`.

<Warning>
  The envelope's `request_id` is an HTTP **correlation ID** — quote it when
  contacting support about a specific call. It is **not** the idempotency key
  you send in a transaction request body (confusingly also named
  `request_id` there, under `data.request_id` on a transaction response) —
  the two are unrelated and don't need to match. Set your own correlation ID
  by sending an `X-Request-Id` request header (letters, digits, and hyphens,
  up to 36 characters — other characters are stripped); the gateway echoes it
  back as the envelope `request_id`, or generates one if you don't send it.
</Warning>

## Errors

Both APIs return errors as `application/problem+json` bodies
([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)):

```json theme={null}
{
  "type": "urn:radiumone:gateway:validation-error",
  "title": "Validation Error",
  "status": 400,
  "detail": "amount.value must match ^\\d{1,12}$",
  "instance": "/v1/transactions/purchase",
  "request_id": "req_a1b2c3d4e5f6"
}
```

Some errors add extension members:

| Member | Meaning | Example |
| - | - | - |
| `code` | Optional short machine-readable code, distinct from `type`. | `"card-declined"` |
| `retry_allowed` | Whether retrying with the *same* idempotency key is safe. When it's explicitly `false`, don't blindly retry — that risks a duplicate charge or a double-closed batch. | `false` |
| `resolution` | On a subset of `409` responses, whether the condition needs an operator action (`operator`) or clears on its own (`transient`). | `"transient"` |
| `errors` | On most `400 gateway:validation-error` responses, one entry per invalid field — see [Validation error details](/payments-api/errors/problem-format-and-retries#validation-error-details). | `[{"pointer": "/amount/currency", "code": "missing"}]` |

On the Payments API, a suggested wait time comes back as the `Retry-After`
HTTP response header (seconds) — not as a `retry_after` member of the
problem body.

See [Problem format and retries](/payments-api/errors/problem-format-and-retries)
for the response shape and status guide, and
[Decline codes](/payments-api/errors/decline-codes) for acquirer response codes.
If a request comes back as a `5xx` you should retry, see [Retry when the
service is unavailable](/payments-api/handle-failures/service-unavailable).

## Money amounts

<Info>
  Amounts are always integers in the currency's minor unit. For example, `5000` for `SGD` means SGD 50.00.
</Info>

Purchase, authorize, and refund amounts additionally use a **money object**
rather than a bare integer: `{ "currency": "SGD", "value": "5000" }`, where
`value` is a minor-units numeric string (zero-padding optional, up to 12
digits, non-zero). Session and transaction-response amounts remain plain
integers.

## Idempotency and retries

The two APIs use different idempotency keys, at different levels.
**Payments API** — `request_id` (purchase, authorize, refunds) or
`operation_id` (capture, void), set per transaction/operation:

| Key | Used by | On replay |
| - | - | - |
| `request_id` | Purchase, authorize, standalone and referenced refunds | Same body, same operation type → the original transaction, whatever its status — including `PENDING`, `DECLINED`, or `FAILED`. Changed body, or the same key reused for a different operation type → [`transaction:idempotency-body-mismatch`](/payments-api/errors/payment-operation-errors#transaction-idempotency-body-mismatch). Purchase/authorize/standalone-refund compare `amount`, `currency`, `payment_method_type`, `channel`, the card's `pan_prefix` (first 8 digits — not the full token), `metadata`, and `order_reference`; a **referenced refund** compares only the original transaction and `amount` (`reason` isn't compared) and its replay check runs before the refund gates, so it always replays, even a `DECLINED`/`FAILED` one — mint a **new** `request_id` to retry after a decline. None of these compare `three_ds` or `loyalty`, so changing either on a retry replays the original silently instead of failing. |
| `operation_id` | Capture, void | Same operation type on the same transaction → the original result (body is never compared, so a changed amount is silently ignored). A different operation type reusing the key → [`tx:duplicate-operation`](/payments-api/errors/payment-operation-errors#tx-duplicate-operation). |

<Warning>
  Balance inquiry also takes a `request_id` field, but it isn't an idempotency key — there's no dedup or replay store. Every call re-queries the rewards host, even with the same `request_id`.
</Warning>

<Tip>
  Keys are 8–64 characters, `[a-zA-Z0-9-]` only, unique per merchant account. Generate one key per order **attempt** and persist it to your database before you send the first request — never mint a new key just to retry the same attempt. See [Prevent duplicate payments](/get-started/api-basics/prevent-duplicate-payments).
</Tip>

On the **Checkout API**, `order_reference` is the idempotency key for
session creation — there's no separate `request_id`/`operation_id` concept.
A create replayed with the same `order_reference`, `amount`, and `currency`,
within the session's TTL, returns the original session as long as it's still
payable (other fields are ignored); the same reference with a **different**
amount or currency on a still-payable session, or a genuinely concurrent
replay, both return `409 session:idempotency_conflict` (with different
`detail` text). See [Prevent duplicate sessions
and double payments](/hosted-checkout/handle-failures/duplicate-sessions-and-double-submit)
for the full replay table and [Session
lifecycle](/hosted-checkout/session-lifecycle#replay-and-retries) for the
TTL/state model behind it.

For the full picture — replay vs. conflict, `PENDING` recovery, and what to
do after a timeout — see the visual guide.

<Card title="Prevent duplicate payments" icon="shield-check" href="/get-started/api-basics/prevent-duplicate-payments">
  How RadiumOne deduplicates requests, and what your integration needs to do on its side.
</Card>

## Metadata

Most create operations accept an optional `metadata` object for your own
key/value data (typical limits: 10 KB, 5 levels deep for Payments API
operations; for Checkout API sessions, a JSON object of at most 4096 UTF-16
code units serialized as compact JSON, limit included — most characters,
including Chinese and Thai, count as 1 unit and emoji as 2). It's stored
and returned on lookups, never interpreted by RadiumOne.

## Channels

Payments API only — the Checkout API has no `channel` field or concept.

`channel` on a payment operation is one of `CARD_PRESENT`, `ECOMMERCE`,
`MOTO`, `PAYMENT_LINK`, `IN_APP`, or `RECURRING`. It shapes which acquirer
routing candidates and operations (e.g. standalone refund) are available —
pick the one that matches how the transaction was actually initiated.

## Rate limits

RadiumOne doesn't publish specific rate-limit numbers. If you're
consistently hitting `429` responses, [contact support](/resources/support)
to discuss your integration's traffic pattern. Both APIs send the
`Retry-After` HTTP header (seconds); the Checkout API additionally carries
the same value as `details.retry_after` in the problem body — see
[Checkout API errors](/hosted-checkout/errors/api-errors#rate-limits-and-server-errors).
