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

# Webhook retries and ordering - Payments API

> Delivery guarantees, the retry backoff schedule, endpoint suspension, and how to reconcile without relying on webhooks alone.

Webhook delivery is **at-least-once** and **unordered**. Both are deliberate
trade-offs for reliability, not bugs — design your handler around them rather than
assuming a single, ordered, exactly-once stream.

## Delivery guarantees

* **At-least-once**: the same event can arrive more than once — after a retry, after
  a network blip that hides a successful `2xx` from RadiumOne, or (rarely) more than
  once for other reasons. Dedupe on the top-level `id` before you act on an event a
  second time. See [Build your handler](/payments-api/webhooks/overview#build-your-handler).
* **Unordered**: deliveries for different events, and even for the same transaction,
  aren't guaranteed to arrive in the order they occurred. See [Ordering](#ordering)
  below for how to recover the right sequence when it matters.

## Retry schedule

If your endpoint doesn't return a `2xx` (timeout, non-2xx status, connection error),
RadiumOne retries on this backoff schedule by default. The schedule is a
platform-configurable default, not a contractual guarantee — build your
reconciliation job (see below) to not depend on an exact attempt count or timing.

| Attempt | Delay before this attempt |
| - | - |
| 1 | Immediate |
| 2 | 60 seconds |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 5 hours |
| 7 | 10 hours |
| 8 (final) | 12 hours |

Eight attempts total, spanning **≈29.6 hours** from the first attempt to the last, by
default — not the \~72 hours you may see quoted elsewhere; the schedule above is the
authoritative source. After the final attempt fails, that delivery is exhausted and
isn't retried further.

<Info>
  A `2xx` you return **after** a slow but eventually-successful attempt still counts
  as delivered — it's specifically the timeout/non-2xx cases above that trigger the
  next scheduled retry.
</Info>

## Timeouts

Respond within RadiumOne's delivery timeout. If your handler needs to do slow work
(database writes, calling other services), enqueue it and respond `2xx` immediately —
see [Build your handler](/payments-api/webhooks/overview#build-your-handler). A handler that's
merely slow, not broken, looks identical to a failing one from RadiumOne's side and
consumes the same retry budget.

## Ordering

Deliveries for the same transaction aren't guaranteed to arrive in the order the
underlying state changes happened — a retry of an earlier event can arrive after a
later one. Don't assume "the last webhook I received" is the current state.

* Use each event's `created_at` as the ordering token if you need to reconcile the
  sequence of events for one transaction.
* If your logic depends on the **current** state rather than the event history (for
  example, deciding whether to fulfil an order), call
  `GET /v1/transactions/{id}/status` ([API reference](/payments-api/reference/transactions/transaction-status-inquiry)) to re-fetch the authoritative current state
  instead of trusting whichever event arrived most recently.

## Endpoint suspension

RadiumOne stops sending to an endpoint that's provably broken, not merely slow or
temporarily down:

* **What trips it**: sustained failures whose cause **can't fix itself** — an
  unresolvable hostname, a bad TLS certificate, an SSRF-blocked address, or a
  `410 Gone` response. This must recur across **multiple separate deliveries** (at
  least 5 by default) over a **sustained window** (24 hours by default) before your
  endpoint is suspended.
* **What never counts**: timeouts, `5xx` responses, and ordinary `4xx` responses.
  These are treated as transient — a bad deploy afternoon doesn't get you suspended.
* **What suspension means**: a suspended endpoint receives **nothing**. Events aren't
  queued for it and aren't replayed once it's re-activated — they're dropped for that
  endpoint entirely while it's suspended.
* **Recovery is manual, not automatic.** There's no health probe and no auto-resume.
  &#x20;[Contact support](/resources/support) to re-activate a
  suspended endpoint once you've fixed the underlying issue. Re-activating an
  endpoint that's still broken simply suspends it again once the same failure
  pattern recurs.

<Warning>
  Because a suspended endpoint silently receives nothing, monitor your own delivery
  health (for example, alert if you haven't received any webhook in an unexpectedly
  long window) rather than relying on RadiumOne to tell you your endpoint stopped
  receiving events.
</Warning>

## Reconciliation job

Webhooks are the primary signal, not the only one you should rely on. Because
delivery can be delayed, retried, or (if your endpoint was suspended) missed
entirely, run a periodic reconciliation job as a backstop:

1. For transaction `id`s you already stored (from create responses or prior
   webhooks) that you haven't heard a terminal outcome for, poll `GET
   /v1/transactions/{id}/status`. RadiumOne has no list-by-`order_reference`
   endpoint, so this only works against `id`s you've already recorded. For
   settlement batches, use [Retrieve a settlement
   batch](/payments-api/settlement-and-reconciliation#retrieve-a-settlement-batch).
2. Compare their current state against your own records.
3. Treat a mismatch — a transaction you expected to be `CAPTURED` that your records
   still show as pending, or a settlement batch your records never saw — as a signal
   to investigate, not just to log.

See [Recover from missed, duplicate or out-of-order webhooks](/payments-api/handle-failures/missed-duplicate-or-out-of-order-webhooks) for the full walkthrough.

This is the same authenticated-read pattern used to recover from a timed-out
request — see [Recovery after timeouts](/payments-api/webhooks/overview#recovery-after-timeouts).

## Test your integration

See [Test your integration](/resources/test-your-integration#webhooks) for simulated
retry, duplicate-delivery, and out-of-order scenarios.

## Go-live notes

* Make your handler idempotent on event `id` before you go live — a retried or
  duplicate delivery must never double-process an order.
* Set up a reconciliation job (see above) rather than treating webhooks as
  infallible.
* Review the full [go-live checklist](/resources/go-live-checklist).

## Next steps

<Columns cols={2}>
  <Card title="Webhooks overview" icon="webhook" href="/payments-api/webhooks/overview">
    Endpoint setup and the handler steps these guarantees assume.
  </Card>

  <Card title="Webhook event types" icon="list-ordered" href="/payments-api/webhooks/event-types">
    Every event and its payload shape.
  </Card>
</Columns>
