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

# Automatic reversals - Payments API

> How an outcome-unknown charge becomes a reversal, and what to do while it's pending.

An operation that timed out at the switch can leave a transaction in an indeterminate state. Rather than leaving that charge stuck, RadiumOne automatically reverses it once the outcome resolves. You'll see this most often after a [switch timeout](/payments-api/handle-failures/timeouts-and-unknown-outcomes).

<Info>
  **TL;DR** — `REVERSAL_PENDING` / `REVERSED` means the charge didn't ultimately go through — treat it as not paid. Wait for the confirming webhook.
</Info>

## When this happens

* A purchase, authorize, capture, void, or referenced-refund request times out at the payment switch with an indeterminate outcome (`host_forwarded: "maybe"`). RadiumOne treats it as a reversal candidate rather than leaving it unresolved — this is why `FAILED` is not a guarantee that no funds moved; only `VOIDED` and `REVERSED` positively assert that. A refund that times out with an unknown outcome is armed for reversal the same way, rather than being left in a terminal `FAILED` state.
* A reversal itself gets stuck after repeated attempts — `409 urn:radiumone:transaction:reversal-limit-exceeded`, which needs support to clear.

## What you see

| Signal | Value |
| - | - |
| Transaction status | Moves to `REVERSAL_PENDING`, then `REVERSED` |
| Webhook | `payment.reversed` / `authorization.reversed` / `refund.reversed`, once it settles |
| Reversal stuck | [`transaction:reversal-limit-exceeded`](/payments-api/errors/payment-operation-errors#transaction-reversal-limit-exceeded) — contact support |

## What to do

<Steps>
  <Step title="Treat it as not paid, not as an error">
    `REVERSAL_PENDING` and `REVERSED` both mean the charge didn't ultimately go through. Don't fulfil the order, and don't treat the reversal as a failure on your side — it's RadiumOne resolving an indeterminate outcome safely.
  </Step>

  <Step title="Confirm the final state">
    Wait for the `*.reversed` webhook (recommended), or check its status directly if you don't use webhooks or need an answer now ([API reference](/payments-api/reference/transactions/transaction-status-inquiry)):

    <CodeGroup>
      ```bash cURL theme={null}
      #!/usr/bin/env bash
      # Live acquirer status inquiry for a transaction (no new row written). Use
      # this to recover after a timeout instead of guessing the outcome.
      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}"
      : "${RADIUMONE_TRANSACTION_ID:?set RADIUMONE_TRANSACTION_ID to the transaction to inquire}"

      curl -sS "$API_BASE/v1/transactions/$RADIUMONE_TRANSACTION_ID/status" \
        -H "Authorization: Bearer $RADIUMONE_ACCESS_TOKEN"
      ```

      ```javascript Node.js theme={null}
      #!/usr/bin/env node
      // Live acquirer status inquiry for a transaction (no new row written). Use
      // this to recover after a timeout instead of guessing the outcome. Node 18+
      // ESM fetch. Env: RADIUMONE_ACCESS_TOKEN, RADIUMONE_TRANSACTION_ID, RADIUMONE_API_BASE.
      const API_BASE = process.env.RADIUMONE_API_BASE || "https://api-sandbox.radiumone.io/gateway";
      const accessToken = process.env.RADIUMONE_ACCESS_TOKEN;
      const transactionId = process.env.RADIUMONE_TRANSACTION_ID;

      async function inquireTransactionStatus() {
        const res = await fetch(`${API_BASE}/v1/transactions/${transactionId}/status`, {
          headers: { Authorization: `Bearer ${accessToken}` },
        });
        const payload = await res.json();
        if (!res.ok) {
          throw new Error(`status inquiry failed: ${payload.type ?? payload.code} (${res.status})`);
        }
        return payload; // branch on data.status
      }

      inquireTransactionStatus().then((r) => console.log(JSON.stringify(r, null, 2)));
      ```

      ```python Python theme={null}
      #!/usr/bin/env python3
      """Live acquirer status inquiry for a transaction (no new row written). Use
      this to recover after a timeout instead of guessing the outcome.
      """
      import json
      import os

      import requests

      API_BASE = os.environ.get("RADIUMONE_API_BASE", "https://api-sandbox.radiumone.io/gateway")


      def inquire_transaction_status() -> dict:
          transaction_id = os.environ["RADIUMONE_TRANSACTION_ID"]
          resp = requests.get(
              f"{API_BASE}/v1/transactions/{transaction_id}/status",
              headers={"Authorization": f"Bearer {os.environ.get('RADIUMONE_ACCESS_TOKEN', '')}"},
              timeout=30,
          )
          payload = resp.json()
          if not resp.ok:
              code = payload.get("type") or payload.get("code")
              raise RuntimeError(f"status inquiry failed: {code} ({resp.status_code})")
          return payload  # branch on data.status


      if __name__ == "__main__":
          print(json.dumps(inquire_transaction_status(), indent=2))
      ```
    </CodeGroup>
  </Step>

  <Step title="Escalate a stuck reversal">
    `reversal-limit-exceeded` means the reversal itself needs manual intervention — [contact support](/resources/support) with the transaction or reversal ID.
  </Step>
</Steps>

## Related

<Columns cols={2}>
  <Card title="Payment lifecycle" icon="repeat" href="/payments-api/payment-lifecycle#reversals">
    Where `REVERSAL_PENDING`/`REVERSED` fit in the full state diagram.
  </Card>

  <Card title="Void a payment" icon="rotate-ccw" href="/payments-api/void">
    Release an authorization or reverse a capture while its batch is open.
  </Card>

  <Card title="Handle timeouts and unknown outcomes" icon="clock-alert" href="/payments-api/handle-failures/timeouts-and-unknown-outcomes">
    The switch-timeout case that most often leads here.
  </Card>

  <Card title="Payment operation errors" icon="triangle-alert" href="/payments-api/errors/payment-operation-errors#automatic-reversals">
    The full URN catalog for automatic-reversal errors.
  </Card>
</Columns>
