> ## 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.

# Test your integration - Resources

> Sandbox test cards and scenarios for hosted checkout, Elements, the Payments API, 3D Secure, and webhooks — including replays and timeouts.

<Warning>
  Never use a real card number in sandbox. Use only the test cards below —
  sandbox tokenizes and processes them exactly like production, without
  touching a real card issuer.
</Warning>

Work through the scenarios below against your sandbox keys before you request
production access. Each row links back to the guide it belongs to; the
[go-live checklist](/resources/go-live-checklist) turns this into a per-integration
checklist.

## Test cards

| Scenario | Test card | Expected result |
| - | - | - |
| Successful approval | Pending — contact support | `CAPTURED` (purchase) or `AUTHORIZED` (authorize) |
| Generic decline | Pending — contact support | `DECLINED` with a verbatim `response_code` |
| Insufficient funds decline | Pending — contact support | `DECLINED` |
| Processor timeout | Pending — contact support | Request times out, or returns `PENDING` — retry the same key or poll status |
| 3D Secure frictionless approval | Pending — contact support | `threeDS.authenticate()` resolves `AUTHENTICATED`, no challenge |
| 3D Secure challenge required | Pending — contact support | Challenge presented; resolves `AUTHENTICATED` or `NOT_AUTHENTICATED` |
| 3D Secure not authenticated | Pending — contact support | Resolves `NOT_AUTHENTICATED` |
| UOB Rewards redemption | Pending — contact support | Loyalty leg redeems; card leg (if any) authorizes for the residual |

RadiumOne hasn't published a verified sandbox test-card list yet. Until then,
[contact support](/resources/support) for test-card numbers for the scenario
you need, and use only the numbers support gives you — never a real card, and
never a number you find elsewhere.

<Tabs>
  <Tab title="Hosted checkout">
    Use [Redirect to hosted checkout](/hosted-checkout/redirect-integration) or
    [Embed hosted checkout](/hosted-checkout/embedded-integration) with a test
    card from the table above.

    | Scenario | Do | Expect |
    | - | - | - |
    | Approval | Complete checkout with an approval card | `completed` session status, a `payment.captured` webhook, redirect to `success_url` (signed params if you've configured a redirect secret) |
    | Decline | Use a decline card | `failed` status; shopper returned to your raw `cancel_url`, no `checkout_id`/`state` — confirm via [Verify the payment result](/hosted-checkout/verify-payment-result), never from the redirect alone |
    | Timeout | Leave the session idle past its `ttl_minutes` | `expired` status; return carries `reason=timeout` |
    | Cancel | Use the checkout page's back button or close the tab | Session abandoned — re-check its status server-side before treating the order as failed |
    | Embedded events | Confirm your `postMessage` handler reacts to every `CHECKOUT_*` event you rely on | Don't wait for `CHECKOUT_CANCELLED` (never emitted) — see [Embedded checkout events](/hosted-checkout/reference/embedded-events) |
    | Duplicate session | Retry session create with the same `order_reference`, first within the session TTL, then again after it expires | Within TTL: the same session is returned (a changed amount is silently ignored). After TTL: a **new** session is created — check your own order state before creating one |

    See [Handle failures](/hosted-checkout/handle-failures/session-expired) for
    the full catalog — expired sessions, missed redirects, invalid signatures,
    and more.
  </Tab>

  <Tab title="Elements">
    Use [Accept a card payment with Elements](/elements/accept-a-card-payment)
    with a sandbox publishable key and a test card from the table above.

    | Scenario | Do | Expect |
    | - | - | - |
    | Approval | `elements.submit()` with an approval card | Resolves with a token; your server's purchase call returns `CAPTURED` |
    | Decline | `elements.submit()` with a decline card | Still resolves — tokenization isn't authorization; the decline surfaces as `201` with `data.status: "DECLINED"` from your [purchase call](/payments-api/charge-or-authorize#handle-the-result) |
    | Client-side validation error | Submit with an incomplete or invalid card field | An `ElementsError` is thrown before any server call — show `customerMessage`, check `retryAllowed` |
    | Bind-token expiry | Wait about 30 minutes after `elements.submit()` before charging the token | [`token:transient-expired`](/payments-api/errors/payment-method-errors#token-transient-expired) — have the shopper re-enter their card and submit again |
    | Double-click Pay | Click Pay twice quickly, or submit from two tabs | The SDK returns `submit:in_progress` (concurrent) or `submit:rate_limited` (within its 1-second throttle) — it doesn't disable your Pay button for you, so your app must |

    See [Handle failures](/elements/handle-failures/sdk-fails-to-load) for the
    full catalog — SDK load errors, tokenization failures, and more.
  </Tab>

  <Tab title="3D Secure">
    Use the 3DS test cards above with [3DS with Elements](/elements/three-d-secure/add-three-d-secure).

    | Scenario | Expect |
    | - | - |
    | Frictionless approval | `threeDS.authenticate()` resolves `AUTHENTICATED` without a challenge |
    | Challenge required | Resolves after the shopper completes a challenge — see [Challenge presentation and redirects](/elements/three-d-secure/challenge-presentation) |
    | Not authenticated | Resolves `NOT_AUTHENTICATED`; your own risk policy decides whether to still charge |
    | Provider unavailable | `urn:radiumone:three-ds:provider-unavailable` — see [Authentication results](/elements/three-d-secure/authentication-results) |

    Whatever the client-reported status, your server must independently verify
    the `three_ds` ref at charge time — see [3D Secure overview: server
    policy](/get-started/three-d-secure#server-policy).

    **Failure scenarios**: see [Handle 3D Secure failures in
    Elements](/elements/handle-failures/three-ds-failures) for the full scenario
    catalog.
  </Tab>

  <Tab title="Webhooks">
    | Scenario | Do | Expect |
    | - | - | - |
    | Signature check | Verify your implementation against a fixed key/payload/signature triple | Matches, before you wire it to a live endpoint — see [Verify webhook signatures](/payments-api/webhooks/verify-signatures) |
    | Duplicates | Send the same event `id` to your endpoint twice | Your handler dedupes on it instead of processing it a second time (RadiumOne retries up to 8 times over about 29.6 hours) |
    | Out-of-order delivery | Confirm your handler doesn't assume delivery order | A later event for the same transaction can arrive before an earlier one — key your state transitions off the transaction's current status, not "the last webhook I received" |

    See [Retries, ordering and duplicates](/payments-api/webhooks/retries-and-ordering)
    for the full delivery model.
  </Tab>

  <Tab title="UOB Rewards">
    <Info>UOB Rewards requires enablement.</Info>

    Once your sandbox outlet has UOB Rewards enabled, use a rewards test card.

    | Scenario | Guide |
    | - | - |
    | Check a balance | [Check a rewards balance](/payments-api/payment-methods/uob-rewards/check-balance) |
    | Pay with points (full and partial redemption) | [Pay with points](/payments-api/payment-methods/uob-rewards/pay-with-points) |
    | Process a rewards refund | [Refunds and cancellations](/payments-api/payment-methods/uob-rewards/refunds-and-cancellations) |

    **Failure scenarios**: see [Handle UOB Rewards redemption
    failures](/payments-api/handle-failures/rewards-redemption-failures) and
    [Handle UOB Rewards void and refund
    restrictions](/payments-api/handle-failures/rewards-void-and-refund-restrictions).
  </Tab>
</Tabs>

## Payments API

These scenarios apply wherever you call the Payments API directly: purchase,
authorize, capture, void, referenced refund, and standalone refunds.

### Purchase and authorize

| Do | Expect | Guide |
| - | - | - |
| Send a purchase/authorize request with an approval card | `201` with `status: "CAPTURED"`/`"AUTHORIZED"` | [Charge or authorize a payment](/payments-api/charge-or-authorize) |
| Send one with a decline card | `201` with `status: "DECLINED"` — not an error response | same |
| Send one with a malformed or unknown `card.token` | `400`/`422` | same |

### Capture

| Do | Expect | Guide |
| - | - | - |
| Capture the full authorized amount | Succeeds | [Capture an authorization](/payments-api/capture) |
| Capture a different amount | `400` | same |
| Capture after the capture window closes | [`tx:capture-window-expired`](/payments-api/errors/payment-operation-errors#tx-capture-window-expired) | same |
| Replay the same `operation_id` with a **different** amount | The original capture result is returned — the new amount is silently ignored, not rejected | [Prevent duplicate payments](/get-started/api-basics/prevent-duplicate-payments) |

### Void

| Do | Expect | Guide |
| - | - | - |
| Void an `AUTHORIZED` transaction | Succeeds | [Void a payment](/payments-api/void) |
| Void it again after its settlement batch closes | [`tx:void-on-non-open-batch`](/payments-api/errors/payment-operation-errors#tx-void-on-non-open-batch) | same |

### Refund

Refunds — referenced and standalone alike — are disabled by default and need enablement for your sandbox outlet's acquirer channel before either of these succeeds; see [Refunds require enablement](/payments-api/refund#refunds-require-enablement).

| Do | Expect | Guide |
| - | - | - |
| Refund a `CAPTURED` transaction once its batch has closed, with refunds enabled | Succeeds | [Refund a payment](/payments-api/refund) |
| Refund one before its batch closes | [`tx:refund-on-open-batch`](/payments-api/errors/payment-operation-errors#tx-refund-on-open-batch) | same |
| Refund without enablement | [`routing:capability-not-supported`](/payments-api/errors/payment-method-errors#routing-capability-not-supported) | same |

<Note>
  **Void while the settlement batch is OPEN. Refund once it's CLOSED.** You can't do either the other way around:

  * Voiding a transaction whose batch has already closed returns `409 urn:radiumone:tx:void-on-non-open-batch`.
  * Refunding a transaction whose batch is still open returns `409 urn:radiumone:tx:refund-on-open-batch`.

  The boundary is the settlement **batch** closing, not the card network settling with the issuer. Check `GET /v1/transactions/{id}/status` or a `settlement.*` webhook if you're unsure which state you're in. See [Resolve void and refund conflicts](/payments-api/handle-failures/void-and-refund-conflicts) if you hit either error.
</Note>

### Standalone refunds

Once your sandbox outlet has standalone refunds enabled, issue an open
refund against a test card token directly — see [Standalone
refunds](/payments-api/standalone-refunds). Without enablement, expect
`409 urn:radiumone:routing:operation-disabled`.

### Idempotent replay

| Do | Expect |
| - | - |
| Send the same create-type request twice — same `request_id` (or `operation_id`), identical body | The second call returns the **original** result at the same HTTP status as the first (`201` for purchase/authorize/standalone refund, `200` for capture/void/referenced refund). No new transaction is created either time |
| Send two identical requests **concurrently** | Both resolve to the same transaction. If the first hasn't finished yet, the second returns that row as `PENDING` — not an error, and not a second transaction |

### Body mismatch

| Do | Expect |
| - | - |
| Send the same `request_id` a second time with a **different** body (a different amount, for example) | [`transaction:idempotency-body-mismatch`](/payments-api/errors/payment-operation-errors#transaction-idempotency-body-mismatch) — fix the body, or use a new key if this is genuinely a new attempt |
| Send the same `operation_id` (capture/void) a second time with a different amount | The **original** result is returned — `operation_id` replays never compare the body, so a changed amount is silently ignored, not rejected |

### Timeouts and retries

Simulate a client-side timeout (shorten your own HTTP client's timeout).
Confirm your integration:

1. Retries the **same** `request_id`/`operation_id` rather than minting a new
   one, or
2. Calls `GET /v1/transactions/{id}/status` to resolve the outcome without
   creating anything new — see [Check a transaction's
   status](/payments-api/check-transaction-status).

Never treat a timeout as a decline, and never retry with a new key — see
[Charge or authorize a
payment](/payments-api/charge-or-authorize#idempotency-and-replay).

### PENDING handling

`PENDING` means the outcome isn't known yet — most often after a processor timeout. Don't assume success or failure. Recover it one of two ways:

1. **Wait for a webhook** (`payment.*`, `authorization.*`, `refund.*` — see [Webhook event types](/payments-api/webhooks/event-types)).
2. **Call `GET /v1/transactions/{id}/status`** for a live inquiry against the acquirer.

If you don't have the transaction `id` yet — a client-side timeout before the first response arrived — replay the same request with the same `request_id` and body. The replay returns the stored transaction and its `id`, whatever status it's reached. Never re-submit with a **new** idempotency key just because the first attempt is slow — that risks a second charge for the same order.

### Declined is still a 2xx

Confirm your code branches on `data.status`, never on the HTTP status code
alone — a decline is `201` (or `200` for capture/void/referenced refund)
with `status: "DECLINED"` in the body, not an error response.

**Any 2xx response is a result you must branch on `status`** — never on `response_code` (that's the verbatim host/acquirer code; useful for support tickets, not for your app logic).

| Status | Meaning | What to do |
| - | - | - |
| `AUTHORIZED` | Funds reserved (authorize only) | Capture within the capture window, or void to release |
| `CAPTURED` | Funds captured (purchase, capture, or refund) | Fulfil the order (or process the refund) |
| `VOIDED` | Authorization released | No funds moved |
| `DECLINED` | Issuer or acquirer declined | Final for this attempt — don't retry the same card without a new attempt from the shopper |
| `FAILED` | The transaction didn't complete — the acquirer returned a non-decline error code, or the gateway couldn't place the request. **Not a guarantee that no funds moved** — `VOIDED` and `REVERSED` are the only statuses that positively assert that. | Confirm via `GET /v1/transactions/{id}/status` before retrying, then retry (a genuinely new attempt, not a replay of the same `request_id`) with a **new** `request_id` |
| `PENDING` | Outcome not yet known (async) | Wait for a webhook, or poll `GET /v1/transactions/{id}/status` |
| `AUTH_EXPIRED` | Authorization lapsed before capture | Create a new authorization |
| `REVERSAL_PENDING` / `REVERSED` | Automatic compensating reversal after an upstream timeout left the outcome genuinely unknown (never left `FAILED` in this case) | No merchant action; webhook confirms the final state |

### Failure scenarios

See [Handle failures](/payments-api/handle-failures/timeouts-and-unknown-outcomes) for the
full catalog — timeouts, automatic reversals, capture/void/refund conflicts,
and operations unavailable for your outlet.

## Errors

See [Problem format and retries](/payments-api/errors/problem-format-and-retries) for the response shape and retry rules, and
[Decline codes](/payments-api/errors/decline-codes) for how declines surface and
what to tell the shopper.

## Sandbox limits

Sandbox environment setup, hosts, and key types are covered in [Sandbox and
API keys](/get-started/sandbox-and-api-keys#sandbox-behaviour-and-limits) —
this page only covers the payment scenarios to test.

## Next steps

<Columns cols={2}>
  <Card title="Go-live checklist" icon="clipboard-check" href="/resources/go-live-checklist">
    Work through this once every scenario above passes.
  </Card>

  <Card title="Problem format and retries" icon="triangle-alert" href="/payments-api/errors/problem-format-and-retries">
    Response shape, retry rules, rate limits, and links to the full URN catalog.
  </Card>
</Columns>
