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

# Missing redirects - Hosted checkout

> How to confirm a payment when the shopper's browser never returns to your site.

<Info>
  **TL;DR:** A missing redirect isn't a failed payment — confirm with a webhook or `GET`, not the browser.
</Info>

The shopper's browser can lose its connection (closed laptop, dropped Wi-Fi, a crashed mobile browser) after they submit payment but before it redirects back to your site. The payment itself is handled entirely between RadiumOne's servers — it doesn't depend on the shopper's browser staying connected — so this is a UX gap on your side, not a lost payment.

## When this happens

* The shopper's device loses connectivity right after submitting the card form.
* The shopper closes the browser tab or app before the hosted page finishes redirecting.

<Warning>
  Treat any client-side redirect or callback as a hint only. Always confirm the final payment status from your server, using an authenticated `GET` request or a webhook — never from a query parameter or browser postMessage alone.
</Warning>

## What you see

| Signal | Value |
| - | - |
| Redirect | Never arrives at `success_url` or `cancel_url` |
| Session status | `processing` while the charge is in flight, then a terminal status (`completed` or `failed`) once it resolves — independent of whether the shopper's browser is still connected |
| Webhook | `payment.captured`, `payment.declined`, or `payment.failed` fires once the gateway resolves the charge |

## What to do

<Steps>
  <Step title="Don't assume the payment failed">
    A missing redirect only tells you the shopper's browser didn't complete the round trip — it says nothing about whether the charge itself succeeded.
  </Step>

  <Step title="Wait for the authoritative signal">
    Fulfil only from a webhook or an authenticated `GET` — never because the redirect didn't happen ([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>

    <CodeGroup>
      ```javascript Node.js theme={null}
      #!/usr/bin/env node
      // Minimal webhook handler using Node's built-in http server (no framework).
      // Verify the signature on the RAW body, respond 2xx quickly (before any slow
      // work), and dedupe on data.id -- deliveries are at-least-once and unordered.
      import { createServer } from "node:http";
      import { createHmac, timingSafeEqual } from "node:crypto";

      const WEBHOOK_SECRET = process.env.RADIUMONE_WEBHOOK_SECRET ?? "";
      const seenEventIds = new Set(); // replace with a persistent store in production

      const SECRET_PATTERN = /^whsec_[0-9a-fA-F]{64}$/;
      const SIGNATURE_HEX_PATTERN = /^[0-9a-fA-F]{64}$/;
      const TIMESTAMP_PATTERN = /^\d+$/;

      // Fails closed on every malformed input (missing header, bad secret shape,
      // non-integer timestamp) -- never throws. See verify-webhook-signature for
      // the fully-documented, standalone version of this function.
      function verifySignature(secret, header, rawBody, toleranceSeconds = 300) {
        if (typeof secret !== "string" || !SECRET_PATTERN.test(secret)) return false;
        if (typeof header !== "string" || header.length === 0) return false;

        const fields = {};
        for (const part of header.split(",")) {
          const idx = part.indexOf("=");
          if (idx < 0) return false;
          fields[part.slice(0, idx)] = part.slice(idx + 1);
        }
        const { v, t, s } = fields;
        if (!v || !t || !s) return false;
        if (v !== "1") return false;
        if (!TIMESTAMP_PATTERN.test(t)) return false;
        if (!SIGNATURE_HEX_PATTERN.test(s)) return false;
        if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > toleranceSeconds) return false;

        const key = Buffer.from(secret.slice("whsec_".length), "hex");
        const signingInput = Buffer.concat([Buffer.from(`${v}.${t}.`), rawBody]);
        const expectedHex = createHmac("sha256", key).update(signingInput).digest("hex");

        const expected = Buffer.from(expectedHex, "hex");
        const actual = Buffer.from(s, "hex");
        return expected.length === actual.length && timingSafeEqual(expected, actual);
      }

      const server = createServer((req, res) => {
        if (req.method !== "POST" || req.url !== "/webhooks/radiumone") {
          res.writeHead(404).end();
          return;
        }

        const chunks = [];
        req.on("data", (chunk) => chunks.push(chunk));
        req.on("end", () => {
          const rawBody = Buffer.concat(chunks); // verify against RAW bytes, not parsed JSON
          const header = req.headers["x-radiumone-signature"] ?? "";

          if (!verifySignature(WEBHOOK_SECRET, header, rawBody)) {
            res.writeHead(401).end();
            return;
          }

          const event = JSON.parse(rawBody.toString("utf8"));
          if (seenEventIds.has(event.id)) {
            res.writeHead(200).end(); // duplicate delivery: acknowledge, don't reprocess
            return;
          }
          seenEventIds.add(event.id);

          // Respond 2xx immediately; do slow work (DB writes, fulfilment) after
          // responding, e.g. via a queue. Never rely on the payload alone to
          // fulfil -- confirm amount and status match your order first.
          res.writeHead(200).end();
          console.log("received", event.type, event.id);
        });
      });

      server.listen(process.env.PORT ?? 3001);
      ```

      ```python Python theme={null}
      #!/usr/bin/env python3
      """Minimal webhook handler using Python's built-in http.server (no framework).

      Verify the signature on the RAW body, respond 2xx quickly (before any slow
      work), and dedupe on ``data.id`` -- deliveries are at-least-once and
      unordered.
      """
      import hmac
      import hashlib
      import json
      import os
      import re
      import time
      from http.server import BaseHTTPRequestHandler, HTTPServer

      WEBHOOK_SECRET = os.environ.get("RADIUMONE_WEBHOOK_SECRET", "")
      SEEN_EVENT_IDS: set[str] = set()  # replace with a persistent store in production

      _SECRET_PATTERN = re.compile(r"^whsec_[0-9a-fA-F]{64}$")
      _SIGNATURE_HEX_PATTERN = re.compile(r"^[0-9a-fA-F]{64}$")
      _TIMESTAMP_PATTERN = re.compile(r"^\d+$")


      # Fails closed on every malformed input (missing header, bad secret shape,
      # non-integer timestamp) -- never raises. See verify-webhook-signature for
      # the fully-documented, standalone version of this function.
      def verify_signature(secret: str, header: str, raw_body: bytes, tolerance_seconds: int = 300) -> bool:
          if not _SECRET_PATTERN.match(secret):
              return False
          if not header:
              return False

          fields = {}
          for part in header.split(","):
              if "=" not in part:
                  return False
              k, _, v = part.partition("=")
              fields[k] = v

          v, t, s = fields.get("v"), fields.get("t"), fields.get("s")
          if not v or not t or not s:
              return False
          if v != "1":
              return False
          if not _TIMESTAMP_PATTERN.match(t):
              return False
          if not _SIGNATURE_HEX_PATTERN.match(s):
              return False
          if abs(int(time.time()) - int(t)) > tolerance_seconds:
              return False

          key = bytes.fromhex(secret[len("whsec_"):])
          signing_input = f"{v}.{t}.".encode("utf-8") + raw_body
          expected_hex = hmac.new(key, signing_input, hashlib.sha256).hexdigest()
          return hmac.compare_digest(bytes.fromhex(expected_hex), bytes.fromhex(s))


      class WebhookHandler(BaseHTTPRequestHandler):
          def do_POST(self):
              if self.path != "/webhooks/radiumone":
                  self.send_response(404)
                  self.end_headers()
                  return

              length = int(self.headers.get("Content-Length", 0))
              raw_body = self.rfile.read(length)  # verify against RAW bytes, not parsed JSON
              header = self.headers.get("X-RadiumOne-Signature", "")

              if not verify_signature(WEBHOOK_SECRET, header, raw_body):
                  self.send_response(401)
                  self.end_headers()
                  return

              event = json.loads(raw_body)
              if event["id"] in SEEN_EVENT_IDS:
                  self.send_response(200)  # duplicate delivery: acknowledge, don't reprocess
                  self.end_headers()
                  return
              SEEN_EVENT_IDS.add(event["id"])

              # Respond 2xx immediately; do slow work (DB writes, fulfilment) after
              # responding, e.g. via a queue. Never rely on the payload alone to
              # fulfil -- confirm amount and status match your order first.
              self.send_response(200)
              self.end_headers()
              print("received", event["type"], event["id"])


      if __name__ == "__main__":
          HTTPServer(("", int(os.environ.get("PORT", 3001))), WebhookHandler).serve_forever()
      ```
    </CodeGroup>
  </Step>

  <Step title="Poll with backoff until the session reaches a terminal state">
    If you're reconciling by polling rather than waiting on the webhook, back off between calls — `processing` can legitimately last a few seconds while the gateway completes the charge. Stop once you see `completed`, `failed`, `expired`, or `cancelled`.
  </Step>
</Steps>

## Related

<Columns cols={2}>
  <Card title="Verify the payment result" icon="shield-check" href="/hosted-checkout/verify-payment-result">
    The full decision table for every result signal.
  </Card>

  <Card title="Session lifecycle" icon="clock" href="/hosted-checkout/session-lifecycle">
    Statuses in full, including `processing`.
  </Card>

  <Card title="Handle payment service outages during checkout" icon="server-crash" href="/hosted-checkout/handle-failures/payment-service-unavailable">
    When the gateway itself — not just the shopper's connection — is the problem.
  </Card>

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