RANTANDocs
Open RANTAN
Hosted Checkout

Webhooks

Receive signed payment, checkout, and refund events on your backend.

RANTAN sends signed HTTPS events to your backend when a hosted payment, checkout, or refund changes state. Webhooks are asynchronous signals; verify and fulfill each event idempotently.

#Register an endpoint

Register an HTTPS endpoint with the seller JWT. Private, loopback, local-network, credential-bearing, redirecting, and non-standard-port destinations are rejected.

Create endpoint
curl -X POST https://app.rantan.xyz/api/v2/webhooks/endpoints/ \
  -H "Authorization: Bearer $RANTAN_SELLER_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://merchant.example/webhooks/rantan",
    "description": "Production",
    "subscribed_events": [
      "payment.succeeded",
      "checkout.completed",
      "checkout.failed",
      "refund.completed"
    ]
  }'
201 Created
{
  "id": "7d97ad12-7091-43f6-b9bc-707cab54ae29",
  "url": "https://merchant.example/webhooks/rantan",
  "description": "Production",
  "subscribed_events": [
    "payment.succeeded",
    "checkout.completed",
    "checkout.failed",
    "refund.completed"
  ],
  "is_active": true,
  "created_at": "2026-07-30T10:00:00Z",
  "updated_at": "2026-07-30T10:00:00Z",
  "signing_secret": "whsec_store-this-once"
}

#Event types

EventWhen it is sent
payment.succeededThe x402 settlement is recorded durably.
checkout.completedRANTAN fulfillment completed successfully.
checkout.failedCheckout reached a terminal failed state.
checkout.canceledThe session or linked purchase was canceled.
checkout.expiredThe session or linked purchase expired.
refund.completedThe refund completed and was recorded.

#Event payload

Every event has a stable id. Use it as a unique key before changing access, inventory, credits, or order state.

payment.succeeded
{
  "id": "d22f4de0-4e92-4dc9-af9f-aaadbbdad801",
  "type": "payment.succeeded",
  "api_version": "2026-07-30",
  "created_at": "2026-07-30T10:04:32Z",
  "data": {
    "object": {
      "checkout_session_id": "c8a9d48e-68b2-48dd-a754-75635cae71d6",
      "client_reference_id": "order_20260730_001",
      "product_id": 123,
      "purchase_id": "bfaf2aa4-dd86-4356-92db-ae0431806867",
      "purchase_status": "payment_confirmed",
      "completed": false,
      "buyer_wallet_address": "0x1234...abcd",
      "quantity": 1,
      "currency": "USDC",
      "total_amount": "12.50000000",
      "amount_atomic": "12500000"
    },
    "payment": {
      "provider": "x402",
      "transaction_hash": "0xabcd...",
      "amount_atomic": "12500000",
      "recipient": "0x9876...fedc"
    }
  }
}

#Verify the signature

Rantan-Signature contains t={unix timestamp},v1={HMAC-SHA256}. Compute the digest over the exact raw body as `${t}.${rawBody}`, reject timestamps older than five minutes, and compare in constant time.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

export async function handleRantanWebhook(req, res) {
  // Read the raw bytes before JSON parsing.
  const rawBody = await readRawBody(req);
  const signatureHeader = req.headers["rantan-signature"];
  if (typeof signatureHeader !== "string") {
    return res.status(400).end();
  }
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((part) => part.split("=")),
  );

  const timestamp = Number(parts.t);
  if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) {
    return res.status(400).end();
  }

  const expected = createHmac(
    "sha256",
    process.env.RANTAN_WEBHOOK_SECRET,
  )
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest("hex");

  const received = Buffer.from(parts.v1 ?? "", "hex");
  const calculated = Buffer.from(expected, "hex");
  if (
    received.length !== calculated.length ||
    !timingSafeEqual(received, calculated)
  ) {
    return res.status(401).end();
  }

  const event = JSON.parse(rawBody.toString("utf8"));
  await processEventOnce(event.id, event.type, event.data);
  return res.status(204).end();
}

#Retries and delivery history

Return any 2xx status within 10 seconds. Redirects are not followed. Failed deliveries retry after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 24 hours, and 48 hours, then become failed after eight attempts.

#Manual retry

Inspect up to 100 recent deliveries and manually replay a delivered or failed event. The event id stays the same, so your handler must remain idempotent.

Replay delivery
curl -X POST \
  https://app.rantan.xyz/api/v2/webhooks/deliveries/$DELIVERY_ID/retry/ \
  -H "Authorization: Bearer $RANTAN_SELLER_JWT"

#Webhook endpoints

POST/api/v2/webhooks/endpoints/

Create an endpoint and receive its initial signing_secret.

GET/api/v2/webhooks/endpoints/

List the seller’s endpoints.

PATCH/api/v2/webhooks/endpoints/{endpointId}/

Update the URL, subscriptions, or active state.

GET/api/v2/webhooks/endpoints/{endpointId}/

Read one endpoint configuration.

DELETE/api/v2/webhooks/endpoints/{endpointId}/

Disable an endpoint and stop future deliveries.

POST/api/v2/webhooks/endpoints/{endpointId}/rotate-secret/

Invalidate the old secret and receive a new signing_secret.

GET/api/v2/webhooks/deliveries/

List delivery status and response history.

POST/api/v2/webhooks/deliveries/{deliveryId}/retry/

Replay a delivery with the same event id.