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

# Cancellation conflicts - Hosted checkout

> Why cancelling a checkout session can conflict with its current state, and how to resolve it.

<Info>
  **TL;DR:** Cancel only works on a `pending` session — check the status first if you get a `409`.
</Info>

Cancelling a checkout session only makes sense while it's still `pending`. Calling cancel on a session in any other state returns a conflict, rather than silently doing nothing or charging/refunding on your behalf.

## When this happens

* You call cancel while the shopper is mid-payment (session `processing`), or after it already reached `completed`, `failed`, `expired`, or `cancelled`.
* You call cancel on a `checkout_id` that doesn't exist for your account.

## What you see

| Situation | HTTP status | Error type |
| - | - | - |
| Session is `pending` | `200` | — cancelled successfully |
| Session is already `cancelled` | `200` | — idempotent; returns the same result, no error |
| Session is `processing` or any other non-`pending` state | `409` | `session:invalid_state` |
| Session doesn't exist, or belongs to a different account | `404` | `session:not_found` — identical response either way |
| `checkout_id` or API key is malformed | `404` | `resource:not_found` |

<Info>
  Calling cancel twice on an already-cancelled session is safe — it's treated as idempotent, not an error. Only a session in `processing` or a terminal state (`completed`, `failed`, `expired`) other than `cancelled` returns the `409`.
</Info>

## What to do

<Steps>
  <Step title="Check the session's current status first">
    Before retrying a cancel that returned `409`, find out what actually happened ([API reference](/hosted-checkout/reference/checkout-sessions/get-a-checkout-session)):

    <CodeGroup>
      ```bash cURL theme={null}
      #!/usr/bin/env bash
      # Authenticated merchant view of a checkout session. Branch on data.status;
      # never on gateway_response_code. Note: GET timestamps are epoch
      # milliseconds, unlike the ISO string returned at create time.
      set -euo pipefail

      CHECKOUT_BASE="${RADIUMONE_CHECKOUT_BASE:-https://checkout-sandbox.radiumone.io}"
      : "${RADIUMONE_SECRET_KEY:?set RADIUMONE_SECRET_KEY to your r1sk_* secret key}"
      : "${RADIUMONE_CHECKOUT_ID:?set RADIUMONE_CHECKOUT_ID to the checkout_id to verify}"

      curl -sS "$CHECKOUT_BASE/api/v1/checkout/sessions/$RADIUMONE_CHECKOUT_ID" \
        -H "X-Api-Key: $RADIUMONE_SECRET_KEY"
      ```

      ```javascript Node.js theme={null}
      #!/usr/bin/env node
      // Authenticated merchant view of a checkout session. Branch on data.status;
      // never on gateway_response_code. Node 18+ ESM fetch.
      // Env: RADIUMONE_SECRET_KEY, RADIUMONE_CHECKOUT_ID, RADIUMONE_CHECKOUT_BASE.
      const CHECKOUT_BASE = process.env.RADIUMONE_CHECKOUT_BASE || "https://checkout-sandbox.radiumone.io";
      const secretKey = process.env.RADIUMONE_SECRET_KEY;
      const checkoutId = process.env.RADIUMONE_CHECKOUT_ID;

      async function retrieveCheckoutSession() {
        const res = await fetch(`${CHECKOUT_BASE}/api/v1/checkout/sessions/${checkoutId}`, {
          headers: { "X-Api-Key": secretKey },
        });
        const payload = await res.json();
        if (!res.ok) {
          throw new Error(`checkout session fetch failed: ${payload.code ?? payload.type} (${res.status})`);
        }
        // Confirm order_reference and amount match your order before fulfilling.
        return payload;
      }

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

      ```python Python theme={null}
      #!/usr/bin/env python3
      """Authenticated merchant view of a checkout session. Branch on ``status``;
      never on ``gateway_response_code``.
      """
      import json
      import os

      import requests

      CHECKOUT_BASE = os.environ.get("RADIUMONE_CHECKOUT_BASE", "https://checkout-sandbox.radiumone.io")


      def retrieve_checkout_session() -> dict:
          checkout_id = os.environ["RADIUMONE_CHECKOUT_ID"]
          resp = requests.get(
              f"{CHECKOUT_BASE}/api/v1/checkout/sessions/{checkout_id}",
              headers={"X-Api-Key": os.environ.get("RADIUMONE_SECRET_KEY", "")},
              timeout=30,
          )
          payload = resp.json()
          if not resp.ok:
              code = payload.get("code") or payload.get("type")
              raise RuntimeError(f"checkout session fetch failed: {code} ({resp.status_code})")
          # Confirm order_reference and amount match your order before fulfilling.
          return payload


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

  <Step title="Branch on the status">
    * `processing`: wait and re-check shortly — the shopper is actively completing payment, and cancelling mid-attempt isn't supported.
    * `completed`: the payment already succeeded. Don't retry the cancel — if you need to reverse it, [refund the transaction](/payments-api/refund) through the Payments API instead.
    * `failed` / `expired`: the session is already terminal and never charged anything — no action needed.
    * `cancelled`: already done — treat the original conflicting call as resolved.
  </Step>

  <Step title="Retry the cancel only from pending">
    Once (or if) the session returns to `pending`, cancel is safe to call ([API reference](/hosted-checkout/reference/checkout-sessions/cancel-a-checkout-session)):

    <CodeGroup>
      ```bash cURL theme={null}
      #!/usr/bin/env bash
      # Merchant-initiated cancel (X-Api-Key, not CSRF/customer-driven). Idempotent
      # while pending; 409 if already processing or terminal.
      set -euo pipefail

      CHECKOUT_BASE="${RADIUMONE_CHECKOUT_BASE:-https://checkout-sandbox.radiumone.io}"
      : "${RADIUMONE_SECRET_KEY:?set RADIUMONE_SECRET_KEY to your r1sk_* secret key}"
      : "${RADIUMONE_CHECKOUT_ID:?set RADIUMONE_CHECKOUT_ID to the session to cancel}"

      curl -sS -X POST "$CHECKOUT_BASE/api/v1/checkout/sessions/$RADIUMONE_CHECKOUT_ID/cancel" \
        -H "X-Api-Key: $RADIUMONE_SECRET_KEY"
      ```

      ```javascript Node.js theme={null}
      #!/usr/bin/env node
      // Merchant-initiated cancel (X-Api-Key, not CSRF/customer-driven). Idempotent
      // while pending; 409 if already processing or terminal. Node 18+ ESM fetch.
      // Env: RADIUMONE_SECRET_KEY, RADIUMONE_CHECKOUT_ID, RADIUMONE_CHECKOUT_BASE.
      const CHECKOUT_BASE = process.env.RADIUMONE_CHECKOUT_BASE || "https://checkout-sandbox.radiumone.io";
      const secretKey = process.env.RADIUMONE_SECRET_KEY;
      const checkoutId = process.env.RADIUMONE_CHECKOUT_ID;

      async function cancelCheckoutSession() {
        const res = await fetch(`${CHECKOUT_BASE}/api/v1/checkout/sessions/${checkoutId}/cancel`, {
          method: "POST",
          headers: { "X-Api-Key": secretKey },
        });
        const payload = await res.json();
        if (!res.ok) {
          // 409 session:invalid_state if already processing/terminal.
          throw new Error(`checkout session cancel failed: ${payload.code ?? payload.type} (${res.status})`);
        }
        return payload;
      }

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

      ```python Python theme={null}
      #!/usr/bin/env python3
      """Merchant-initiated cancel (X-Api-Key, not CSRF/customer-driven). Idempotent
      while pending; 409 if already processing or terminal.
      """
      import json
      import os

      import requests

      CHECKOUT_BASE = os.environ.get("RADIUMONE_CHECKOUT_BASE", "https://checkout-sandbox.radiumone.io")


      def cancel_checkout_session() -> dict:
          checkout_id = os.environ["RADIUMONE_CHECKOUT_ID"]
          resp = requests.post(
              f"{CHECKOUT_BASE}/api/v1/checkout/sessions/{checkout_id}/cancel",
              headers={"X-Api-Key": os.environ.get("RADIUMONE_SECRET_KEY", "")},
              timeout=30,
          )
          payload = resp.json()
          if not resp.ok:
              # 409 session:invalid_state if already processing/terminal.
              code = payload.get("code") or payload.get("type")
              raise RuntimeError(f"checkout session cancel failed: {code} ({resp.status_code})")
          return payload


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

## Related

<Columns cols={2}>
  <Card title="Session lifecycle" icon="clock" href="/hosted-checkout/session-lifecycle#merchant-cancellation">
    Full cancellation and TTL behavior.
  </Card>

  <Card title="API errors" icon="triangle-alert" href="/hosted-checkout/errors/api-errors#session-state-and-idempotency">
    The full error reference, including `session:invalid_state`.
  </Card>

  <Card title="Handle failures" icon="triangle-alert" href="/hosted-checkout/handle-failures/overview">
    All ten failure scenarios, symptom → page.
  </Card>
</Columns>
