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

# Verify the payment result - Hosted checkout

> Confirm a hosted-checkout payment from your server before fulfilling an order — never from a redirect or postMessage event alone.

Every hosted-checkout integration gives you three signals about a payment's outcome. Only one of them is proof — the other two are UX hints your server should never fulfil on alone.

## The three layers, in order of trust

| Layer | Trust level | What it tells you |
| - | - | - |
| Webhook (`payment.*`) or authenticated `GET` | **Authoritative** | The gateway's own record of the transaction — `status`, amount, currency, order reference |
| Redirect signature (`sig`) | UX hint only | The redirect URL wasn't tampered with in the browser — not proof a charge happened |
| `postMessage` event (embedded mode) | UX hint only | Unauthenticated in-page signal — any page can send a same-shaped message |

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

## Authoritative: webhook or authenticated GET

Your server should confirm a payment one of two ways:

1. **Wait for a gateway webhook** (`payment.captured`, `payment.declined`, `payment.failed`, and related events) — see [Webhook event types](/payments-api/webhooks/event-types) and [Verify webhook signatures](/payments-api/webhooks/verify-signatures).
2. **Call the authenticated [session GET](/hosted-checkout/reference/checkout-sessions/get-a-checkout-session)** directly, with your secret key:

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

If neither signal arrives in a reasonable time, see [Confirm payment when the redirect never arrives](/hosted-checkout/handle-failures/redirect-not-received).

Either way, check that `data.order_reference` and `data.amount`/`data.currency` match the order you're fulfilling — don't fulfil solely because `status` says `completed` without also matching the order.

A malformed `checkout_id` or API key on the GET returns [`404 resource:not_found`](/hosted-checkout/errors/api-errors#checkout-resource-not-found); a missing session, or one that belongs to a different account, returns [`404 session:not_found`](/hosted-checkout/errors/api-errors#checkout-session-not-found) with an identical body either way — don't try to distinguish the two.

## UX hint: the redirect signature

If you've configured a redirect secret, a successful redirect to your `success_url` carries `checkout_id`, `status`, `ts`, `sig`, and `transaction_id` when one is known. `ts` is a Unix timestamp in **seconds** (contrast with the authenticated `GET` response, where `expires_at`/`created_at`/`updated_at` are epoch **milliseconds**, and the create response's `expires_at`, which is an ISO 8601 string). Verifying `sig` confirms the URL wasn't altered in the shopper's browser — it is **not** proof that a charge happened, and it is never sent at all on a decline, expiry, or back-button return (see [Redirect integration](/hosted-checkout/redirect-integration#steps)).

<Info>
  RadiumOne doesn't check `ts` itself — the checkout team's guidance is that your server enforces the tolerance: **reject a signed return whose `ts` is more than 5 minutes old**, the same as an invalid `sig`.
</Info>

<Note>
  In two rare cases — replaying the payment-completion call on a session that's already reached a terminal state — the redirect can carry `success_url` (or, for a `failed` session, `cancel_url`) with none of the usual query parameters, not even `checkout_id`. If you see a return with no parameters at all, don't assume anything from the URL — confirm the outcome with an authenticated `GET` as described above.
</Note>

<Warning>
  **Key encoding differs from webhooks.** The redirect signature's HMAC key is the **full `rsec_…` secret string**, used directly. This is different from the webhook signature, whose key is the raw bytes obtained by hex-decoding the string *after* stripping `whsec_`. Using the wrong encoding for either one makes every signature fail to verify.
</Warning>

<CodeGroup>
  ```javascript Node.js theme={null}
  #!/usr/bin/env node
  // Verify an HPP redirect signature. Node 18+, no dependencies.
  //
  // Key: the FULL `rsec_...` secret STRING, used directly as the HMAC key
  // (this differs from the webhook secret, which is hex-decoded after
  // stripping its prefix -- see verify-webhook-signature).
  // Payload: `${checkout_id}|${status}|${transaction_id_or_empty}|${ts}`.
  // `state` is NOT part of the signed payload.
  // Fail closed: if a secret is configured and `sig` is missing (or invalid),
  // treat the redirect as UNVERIFIED -- never as proof of payment. Always
  // confirm fulfilment via an authenticated GET or a webhook.
  import { createHmac, timingSafeEqual } from "node:crypto";

  const SECRET_PATTERN = /^rsec_[0-9a-fA-F]{48}$/;
  const SIGNATURE_HEX_PATTERN = /^[0-9a-fA-F]{64}$/;

  /**
   * @param {{secret: string, checkoutId: string, status: string, transactionId: string|null,
   *   ts: number, sig: string|null, storedCheckoutId: string, toleranceSeconds?: number, now?: number}} args
   * @returns {boolean}
   */
  export function verifyRedirectSignature({
    secret,
    checkoutId,
    status,
    transactionId,
    ts,
    sig,
    storedCheckoutId,
    toleranceSeconds = 300,
    now = Math.floor(Date.now() / 1000),
  }) {
    // Fail closed: an empty, missing, or wrong-shaped secret is never a valid
    // signing key -- never fall through to HMAC-ing with an empty string.
    if (typeof secret !== "string" || !SECRET_PATTERN.test(secret)) return false;
    if (typeof sig !== "string" || sig.length === 0) return false;
    if (!checkoutId || !storedCheckoutId || !status) return false;
    if (checkoutId !== storedCheckoutId) return false;
    // Number.isInteger (not isFinite): a signed/fractional ts (e.g. 1700000000.5)
    // is not a valid Unix timestamp and must be rejected, not silently truncated.
    if (typeof ts !== "number" || !Number.isInteger(ts) || Math.abs(now - ts) > toleranceSeconds) return false;
    if (!SIGNATURE_HEX_PATTERN.test(sig)) return false;

    const payload = `${checkoutId}|${status}|${transactionId ?? ""}|${ts}`;
    const expected = Buffer.from(createHmac("sha256", secret).update(payload).digest("hex"), "hex");
    const actual = Buffer.from(sig, "hex");

    if (expected.length !== actual.length) return false;
    return timingSafeEqual(expected, actual);
  }

  // Example: verifying a success_url visit.
  // const url = new URL(req.url, "https://shop.example.com");
  // const ok = verifyRedirectSignature({
  //   secret: process.env.RADIUMONE_REDIRECT_SECRET,
  //   checkoutId: url.searchParams.get("checkout_id"),
  //   status: url.searchParams.get("status"),
  //   transactionId: url.searchParams.get("transaction_id"),
  //   ts: Number(url.searchParams.get("ts")),
  //   sig: url.searchParams.get("sig"),
  //   storedCheckoutId: sessionRecord.checkoutId, // your own stored order/session mapping
  // });
  // // The signature is a UX hint only. Always confirm amount + status via
  // // GET /api/v1/checkout/sessions/{id} (X-Api-Key) or a gateway webhook.
  ```

  ```python Python theme={null}
  #!/usr/bin/env python3
  """Verify an HPP redirect signature. Python 3.10+, standard library only.

  Key: the FULL ``rsec_...`` secret STRING, used directly as the HMAC key
  (this differs from the webhook secret, which is hex-decoded after
  stripping its prefix -- see verify-webhook-signature).
  Payload: ``{checkout_id}|{status}|{transaction_id_or_empty}|{ts}``.
  ``state`` is NOT part of the signed payload.
  Fail closed: if a secret is configured and ``sig`` is missing (or invalid),
  treat the redirect as UNVERIFIED -- never as proof of payment. Always
  confirm fulfilment via an authenticated GET or a webhook.
  """
  import hmac
  import hashlib
  import re
  import time

  # fullmatch (not match): plain `match()` with a trailing `$` lets Python
  # accept a string with one trailing "\n" (same class of bug as PCRE without
  # the `D` modifier) -- see verify-webhook-signature/python.py.
  _SECRET_PATTERN = re.compile(r"^rsec_[0-9a-fA-F]{48}$")
  _SIGNATURE_HEX_PATTERN = re.compile(r"^[0-9a-fA-F]{64}$")


  def verify_redirect_signature(
      secret: str,
      checkout_id: str,
      status: str,
      transaction_id: str | None,
      ts,
      sig: str | None,
      stored_checkout_id: str,
      tolerance_seconds: int = 300,
      now: int | None = None,
  ) -> bool:
      now = int(time.time()) if now is None else now

      # Fail closed: an empty, missing, or wrong-shaped secret is never a
      # valid signing key -- never fall through to HMAC-ing with an empty string.
      if not isinstance(secret, str) or not _SECRET_PATTERN.fullmatch(secret):
          return False
      if not sig:
          return False
      if not checkout_id or not stored_checkout_id or not status:
          return False
      if checkout_id != stored_checkout_id:
          return False
      if not isinstance(ts, int) or isinstance(ts, bool):
          return False
      if abs(now - ts) > tolerance_seconds:
          return False
      if not _SIGNATURE_HEX_PATTERN.fullmatch(sig):
          return False

      payload = f"{checkout_id}|{status}|{transaction_id or ''}|{ts}"
      expected_hex = hmac.new(secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256).hexdigest()

      return hmac.compare_digest(bytes.fromhex(expected_hex), bytes.fromhex(sig))


  # Example: verifying a success_url visit (Flask-style).
  # def _parse_ts(raw):
  #     # Never let a malformed query param crash the handler -- an unparsable
  #     # ts must reach verify_redirect_signature as a non-int (it rejects any
  #     # non-int/bool ts), not raise before the fail-closed check even runs.
  #     try:
  #         return int(raw)
  #     except (TypeError, ValueError):
  #         return None
  #
  # ok = verify_redirect_signature(
  #     secret=os.environ["RADIUMONE_REDIRECT_SECRET"],
  #     checkout_id=request.args.get("checkout_id"),
  #     status=request.args.get("status"),
  #     transaction_id=request.args.get("transaction_id"),
  #     ts=_parse_ts(request.args.get("ts")),
  #     sig=request.args.get("sig"),
  #     stored_checkout_id=session_record.checkout_id,
  # )
  # # The signature is a UX hint only. Always confirm amount + status via
  # # GET /api/v1/checkout/sessions/{id} (X-Api-Key) or a gateway webhook.
  ```

  ```php PHP theme={null}
  <?php
  declare(strict_types=1);

  /**
   * Verify an HPP redirect signature. PHP 8.1+, no framework.
   *
   * Key: the FULL `rsec_...` secret STRING, used directly as the HMAC key
   * (this differs from the webhook secret, which is hex-decoded after
   * stripping its prefix -- see verify-webhook-signature).
   * Payload: "{checkout_id}|{status}|{transaction_id_or_empty}|{ts}".
   * `state` is NOT part of the signed payload.
   * Fail closed: if a secret is configured and `sig` is missing (or invalid),
   * treat the redirect as UNVERIFIED -- never as proof of payment. Always
   * confirm fulfilment via an authenticated GET or a webhook.
   *
   * `$ts` is `int|string|null` -- a non-numeric or missing value is rejected
   * rather than coerced, so a malformed query param never silently becomes 0.
   */
  function verifyRedirectSignature(
      string $secret,
      string $checkoutId,
      string $status,
      ?string $transactionId,
      mixed $ts,
      ?string $sig,
      string $storedCheckoutId,
      int $toleranceSeconds = 300,
      ?int $now = null
  ): bool {
      $now ??= time();

      // Fail closed: an empty, missing, or wrong-shaped secret is never a
      // valid signing key -- never fall through to HMAC-ing with an empty string.
      // `D` anchors `$` to the absolute end of the subject -- without it PCRE
      // lets `$` match just before a single trailing "\n", so a secret/signature
      // sourced from a file with a trailing newline would wrongly pass shape
      // validation here.
      if (!preg_match('/^rsec_[0-9a-fA-F]{48}$/D', $secret)) {
          return false;
      }
      if ($sig === null || $sig === '') {
          return false;
      }
      if ($checkoutId === '' || $storedCheckoutId === '' || $status === '') {
          return false;
      }
      if ($checkoutId !== $storedCheckoutId) {
          return false;
      }
      if (is_int($ts)) {
          $timestamp = $ts;
      } elseif (is_string($ts) && ctype_digit($ts)) {
          $timestamp = (int) $ts;
      } else {
          return false;
      }
      if (abs($now - $timestamp) > $toleranceSeconds) {
          return false;
      }
      if (!preg_match('/^[0-9a-fA-F]{64}$/D', $sig)) {
          return false;
      }

      $payload = "{$checkoutId}|{$status}|" . ($transactionId ?? '') . "|{$timestamp}";
      $expectedHex = hash_hmac('sha256', $payload, $secret);

      return hash_equals($expectedHex, strtolower($sig));
  }

  // Example: verifying a success_url visit.
  // $ok = verifyRedirectSignature(
  //     getenv('RADIUMONE_REDIRECT_SECRET'),
  //     $_GET['checkout_id'] ?? '',
  //     $_GET['status'] ?? '',
  //     $_GET['transaction_id'] ?? null,
  //     $_GET['ts'] ?? null,
  //     $_GET['sig'] ?? null,
  //     $sessionRecord['checkout_id'],
  // );
  // // The signature is a UX hint only. Always confirm amount + status via
  // // GET /api/v1/checkout/sessions/{id} (X-Api-Key) or a gateway webhook.
  ```

  ```java Java theme={null}
  import java.nio.charset.StandardCharsets;
  import java.security.MessageDigest;
  import java.util.regex.Pattern;
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;

  /**
   * Verify an HPP redirect signature. Java 17+, no framework
   * ({@code javax.crypto.Mac} only).
   *
   * Key: the FULL {@code rsec_...} secret STRING, used directly as the HMAC
   * key (this differs from the webhook secret, which is hex-decoded after
   * stripping its prefix -- see verify-webhook-signature).
   * Payload: {@code "{checkout_id}|{status}|{transaction_id_or_empty}|{ts}"}.
   * {@code state} is NOT part of the signed payload.
   * Fail closed: if a secret is configured and {@code sig} is missing (or
   * invalid), treat the redirect as UNVERIFIED -- never as proof of payment.
   * Always confirm fulfilment via an authenticated GET or a webhook.
   *
   * {@code ts} is passed as a {@code String} so a malformed/non-numeric
   * timestamp can be rejected instead of failing {@code Long.parseLong} at
   * the caller.
   */
  public final class VerifyRedirectSignature {

      private static final Pattern SECRET_PATTERN = Pattern.compile("^rsec_[0-9a-fA-F]{48}$");
      private static final Pattern SIGNATURE_HEX_PATTERN = Pattern.compile("^[0-9a-fA-F]{64}$");
      private static final Pattern TIMESTAMP_PATTERN = Pattern.compile("^-?\\d+$");

      private VerifyRedirectSignature() {
      }

      public static boolean verify(
              String secret,
              String checkoutId,
              String status,
              String transactionId,
              String ts,
              String sig,
              String storedCheckoutId,
              int toleranceSeconds,
              Long now) {
          long nowSeconds = now != null ? now : System.currentTimeMillis() / 1000L;

          // Fail closed: an empty, missing, or wrong-shaped secret is never a
          // valid signing key -- never fall through to HMAC-ing with an empty string.
          if (secret == null || !SECRET_PATTERN.matcher(secret).matches()) {
              return false;
          }
          if (sig == null || sig.isEmpty()) {
              return false;
          }
          if (checkoutId == null || checkoutId.isEmpty() || storedCheckoutId == null || storedCheckoutId.isEmpty()
                  || status == null || status.isEmpty()) {
              return false;
          }
          if (!checkoutId.equals(storedCheckoutId)) {
              return false;
          }
          if (ts == null || !TIMESTAMP_PATTERN.matcher(ts).matches()) {
              return false;
          }
          long timestamp;
          try {
              timestamp = Long.parseLong(ts);
          } catch (NumberFormatException e) {
              return false;
          }
          if (Math.abs(nowSeconds - timestamp) > toleranceSeconds) {
              return false;
          }
          if (!SIGNATURE_HEX_PATTERN.matcher(sig).matches()) {
              return false;
          }

          String payload = checkoutId + "|" + status + "|" + (transactionId != null ? transactionId : "") + "|" + timestamp;

          byte[] expected;
          try {
              Mac mac = Mac.getInstance("HmacSHA256");
              mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
              expected = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
          } catch (Exception e) {
              return false;
          }

          byte[] actual = hexToBytes(sig);
          return MessageDigest.isEqual(expected, actual);
      }

      private static byte[] hexToBytes(String hex) {
          byte[] out = new byte[hex.length() / 2];
          for (int i = 0; i < out.length; i++) {
              out[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
          }
          return out;
      }

      // Example: verifying a success_url visit (plain servlet).
      // boolean ok = VerifyRedirectSignature.verify(
      //     System.getenv("RADIUMONE_REDIRECT_SECRET"),
      //     request.getParameter("checkout_id"),
      //     request.getParameter("status"),
      //     request.getParameter("transaction_id"),
      //     request.getParameter("ts"),
      //     request.getParameter("sig"),
      //     sessionRecord.getCheckoutId(),
      //     300,
      //     null);
      // // The signature is a UX hint only. Always confirm amount + status via
      // // GET /api/v1/checkout/sessions/{id} (X-Api-Key) or a gateway webhook.
  }
  ```
</CodeGroup>

**Fail closed.** If a redirect secret is configured for your account and a redirect arrives with a missing or invalid `sig`, treat it as unverified — never as confirmation of payment. Also confirm that `checkout_id` in the URL matches the ID your server stored for that order before trusting anything else in the URL. See [Reject invalid redirect signatures](/hosted-checkout/handle-failures/invalid-redirect-signature) for the full walkthrough.

## Decision table

| Situation | What to do |
| - | - |
| Redirect with a valid `sig` | Still confirm via webhook or authenticated `GET` before fulfilling |
| Redirect with no `sig` (no secret configured) | Confirm via webhook or authenticated `GET` — the URL alone proves nothing |
| Redirect with an invalid/stale `sig` | Treat as unverified; confirm via webhook or authenticated `GET`; investigate if it recurs |
| `postMessage` event received (embedded mode) | Confirm via webhook or authenticated `GET` before navigating the shopper onward |
| Webhook received | Authoritative — check `order_reference` and amount, then fulfil |
| Authenticated `GET` shows `status: "completed"` | Authoritative — check `order_reference` and amount, then fulfil |
| Authenticated `GET` shows `status: "failed"` | The payment was declined — see [Handle declined hosted checkout payments](/hosted-checkout/handle-failures/payment-declined) |

## Redirect secret: rotate, don't delete

A redirect secret can be [rotated](/payments-api/reference/merchant-settings/rotate-redirect-secret) or [deleted](/payments-api/reference/merchant-settings/disable-redirect-signing-for-this-merchant) from your server (via the gateway API, using a bearer access token):

<CodeGroup>
  ```bash cURL theme={null}
  #!/usr/bin/env bash
  # Rotate the redirect-signature secret. The plaintext is returned exactly
  # once — store it immediately. Old and new secrets both verify for a grace
  # window after rotation.
  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 with the merchant-secret-rotate scope}"

  curl -sS -X POST "$API_BASE/v1/merchant/redirect-secret/rotate" \
    -H "Authorization: Bearer $RADIUMONE_ACCESS_TOKEN"
  ```

  ```javascript Node.js theme={null}
  #!/usr/bin/env node
  // Rotate the redirect-signature secret. The plaintext is returned exactly
  // once — store it immediately. Node 18+ ESM fetch.
  // Env: RADIUMONE_ACCESS_TOKEN, RADIUMONE_API_BASE.
  const API_BASE = process.env.RADIUMONE_API_BASE || "https://api-sandbox.radiumone.io/gateway";
  const accessToken = process.env.RADIUMONE_ACCESS_TOKEN;

  async function rotateRedirectSecret() {
    const res = await fetch(`${API_BASE}/v1/merchant/redirect-secret/rotate`, {
      method: "POST",
      headers: { Authorization: `Bearer ${accessToken}` },
    });
    const payload = await res.json();
    if (!res.ok) {
      throw new Error(`redirect-secret rotate failed: ${payload.type ?? payload.code} (${res.status})`);
    }
    // Never log payload.data.redirect_secret — store it in your secret manager only.
    return payload;
  }

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

  ```python Python theme={null}
  #!/usr/bin/env python3
  """Rotate the redirect-signature secret. The plaintext is returned exactly
  once — store it immediately.
  """
  import json
  import os

  import requests

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


  def rotate_redirect_secret() -> dict:
      resp = requests.post(
          f"{API_BASE}/v1/merchant/redirect-secret/rotate",
          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"redirect-secret rotate failed: {code} ({resp.status_code})")
      # Never log payload["data"]["redirect_secret"] — store it in your secret manager only.
      return payload


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

The rotate response contains the new secret in plaintext — store it immediately. Rotating **replaces** the secret right away — there's no platform-side overlap window. But a checkout session signs its redirect with whichever secret was active when **that session was created**, for the session's whole life: a session created just before you rotate keeps signing with the old secret until it expires. Sessions last `ttl_minutes` (5–60 minutes — see [Session lifecycle](/hosted-checkout/session-lifecycle)). If you want those in-flight sessions' redirects to keep verifying, keep the old secret available in your own verifier for **at least 65 minutes** after you rotate (the 60-minute maximum session lifetime, plus the redirect signature's 5-minute timestamp tolerance), then remove it.

<Warning>
  **Deleting the redirect secret downgrades your protection.** Once deleted, redirect URLs no longer carry a `sig` at all, so the UX-hint layer disappears entirely — you're relying solely on the webhook/authenticated-`GET` layer (which you should already be doing). If you no longer want signed redirects, prefer leaving the secret in place and simply not depending on it, or contact support about your options — don't delete it as a routine operation.
</Warning>

## Next steps

<Columns cols={2}>
  <Card title="Webhook event types" icon="webhook" href="/payments-api/webhooks/event-types">
    The full authoritative payload reference.
  </Card>

  <Card title="Session lifecycle" icon="clock" href="/hosted-checkout/session-lifecycle">
    Statuses, TTL, and cancellation.
  </Card>

  <Card title="Handle failures" icon="triangle-alert" href="/hosted-checkout/handle-failures/overview">
    Ten common failure scenarios and what to do for each.
  </Card>

  <Card title="Redirect and signature errors" icon="octagon-alert" href="/hosted-checkout/errors/redirect-and-signature-errors">
    Every signal a return to your site can and can't carry.
  </Card>
</Columns>
