Rox Checkout · Integration Guide
v1REST + WebhooksCards · Rox App QR

Accept Rox payments on your store

Rox Checkout lets an online store take payments on the Rox network with a single hosted page. Create a payment session on your server, send the shopper to a Rox-hosted checkout, and they pay with the Rox app (QR) or a Rox card. You get a signed webhook the moment it’s paid, and the funds settle to your Rox merchant wallet.

1 · CREATESessionServer-side, with your secret key
2 · REDIRECTHosted pageShopper pays by QR or card
3 · PAYQR / card + 3-DSOn the Rox rail
4 · WEBHOOKcheckout.paidSigned, verify & fulfil
5 · SETTLEMerchant walletRefund & reconcile via API
Two base URLs. API calls go to https://api.roxcards.com. The shopper-facing hosted page lives at https://checkout.roxcards.com — you never build a payment form yourself, so card data never touches your servers (it’s captured in a PCI iframe on the Rox page).

Money format. All amounts are EGPR minor units — integers with 6 decimals, passed as strings. 1500000 = 1.50 EGPR. Never use floats.

§How it works

Checkout is a thin acquiring layer over the Rox card/QR payment rail. When a session is paid, funds move from the shopper (their Rox wallet or Rox card) to your linked Rox merchant settlement wallet. Checkout never touches custody, minting, or the shopper’s keys.

  • Server ↔ Rox — you call the REST API with a secret key (X-Api-Key) to create sessions, poll status, refund, and pull settlement totals.
  • Shopper ↔ Rox — the hosted page + card capture are public (per-IP rate-limited); the Rox app pays the QR with the cardholder’s own session.
  • Rox → you — a signed webhook on every terminal status change, plus a poll endpoint as a fallback.

A session moves through: openpaid / expired / cancelled / failed, and then refunded / partially_refunded. Capture is idempotent and atomic — a session pays at most once, even under concurrent attempts.

§Get your API key

Before you write code, Rox provisions you as a Partner and links you to a Rox merchant (your settlement wallet). Onboarding gives you two secrets — keep both server-side only:

CredentialLooks likeUse
API keypk_live_… / pk_test_…Auth for every server call, in the X-Api-Key header. Shown once at creation.
Webhook secretwhsec_…Verify webhook signatures. Returned on every session create.
Never expose the API key. It authorizes refunds and settlement reads. Keep it in server env, never in client-side code, mobile apps, or a public repo. Contact Rox to rotate a leaked key.

§Settlement account (where the money lands)

Every partner is bound to exactly one Rox merchant. When a checkout is paid, funds settle to that merchant’s settlement wallet — the Rox wallet of the merchant’s owner. Your pk_… key resolves the payout automatically; you never pass a destination.

ThingWhat it is
MerchantYour business as a Rox merchant account. Must be KYC-approved with a provisioned wallet before it can receive payouts.
Merchant IDThe identifier your partner (API key) is attached to. A UUID, e.g. f8ab5d03-0747-…. This is the link between your key and where money goes.
Settlement walletThe merchant owner’s Rox wallet address — the actual destination of every paid checkout (net of MDR).

Find your Merchant ID. The merchant owner opens the Rox merchant (POS) app → taps the storefront icon on the New payment screen → Store details, which shows the Merchant ID and settlement wallet (both copyable). A bank admin can also see/set it in admin.roxcards.com under Partner services.

Linking is done by Rox at onboarding. You give Rox the Merchant ID your key should pay out to (or Rox creates the merchant for you). To repoint an existing key at a different merchant, send Rox the new Merchant ID. A partner with no linked merchant can authenticate but its first checkout returns this partner has no linked merchant — it cannot receive payouts yet.

§Quickstart

Three moving parts: create a session, redirect, handle the webhook. Here it is end to end.

create + redirect
# 1) create a session when the shopper clicks "Pay"
curl -X POST https://api.roxcards.com/v1/checkout/sessions \
  -H "X-Api-Key: pk_live_xxx" -H "content-type: application/json" \
  -d '{
    "amountMinor": "1500000",
    "currency":    "EGPR",
    "orderRef":    "ORDER-1042",
    "successUrl":  "https://shop.example.com/thanks?order=1042",
    "cancelUrl":   "https://shop.example.com/cart",
    "webhookUrl":  "https://shop.example.com/webhooks/rox"
  }'
# → { "checkoutUrl": "https://checkout.roxcards.com/checkout/…", "id": "…", "webhookSecret": "whsec_…" }
# 2) redirect the shopper to checkoutUrl
import { RoxCheckout } from "@rox/checkout-sdk";
const rox = new RoxCheckout({ apiKey: process.env.ROX_API_KEY }); // pk_…

app.post("/pay", async (req, res) => {
  const session = await rox.createSession({
    amountMinor: "1500000",
    orderRef:    "ORDER-1042",
    successUrl:  "https://shop.example.com/thanks",
    webhookUrl:  "https://shop.example.com/webhooks/rox",
  });
  // persist session.id ↔ your order, and session.webhookSecret
  res.redirect(session.checkoutUrl);
});
// PHP — no SDK needed, it's a plain POST
$ch = curl_init("https://api.roxcards.com/v1/checkout/sessions");
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => ["X-Api-Key: pk_live_xxx", "content-type: application/json"],
  CURLOPT_POSTFIELDS => json_encode([
    "amountMinor" => "1500000", "orderRef" => "ORDER-1042",
    "successUrl" => "https://shop.example.com/thanks",
    "webhookUrl" => "https://shop.example.com/webhooks/rox",
  ]),
  CURLOPT_RETURNTRANSFER => true,
]);
$s = json_decode(curl_exec($ch), true);
header("Location: " . $s["checkoutUrl"]); // redirect shopper

Then verify + handle the checkout.paid webhook (see step 4) and you’re taking payments.

1Create a checkout session

Do this on your server the moment the shopper commits to pay. One session = one payment for one cart total. Store the returned id against your order.

POST/v1/checkout/sessions  ·  auth X-Api-Key

FieldTypeNotes
amountMinor requiredstringEGPR minor units, integer > 0. "1500000" = 1.50 EGPR.
currencystringDefault EGPR.
orderRefstringYour order id — echoed on the webhook & poll. Use it to reconcile.
successUrl / cancelUrlurlWhere the hosted page sends the shopper after success / cancel.
webhookUrlurlOverrides your partner-default webhook for this session.
expiresInSecint60–86400. Default 1800 (30 min).
metadataobjectFree-form; stored and returned to you.

Response

201 · application/json
{
  "id": "b99514d9-…",
  "status": "open",
  "amountMinor": "1500000",
  "currency": "EGPR",
  "orderRef": "ORDER-1042",
  "checkoutUrl": "https://checkout.roxcards.com/checkout/b99514d9-…",
  "qr": "https://checkout.roxcards.com/checkout/b99514d9-…",
  "expiresAt": "2026-07-08T21:09:54.367Z",
  "webhookSecret": "whsec_…"
}
Store webhookSecret. The first create mints it and it’s returned on every create. You need it to verify webhook signatures.

2Redirect to the hosted page

Send the shopper to checkoutUrl — a full-page redirect from your server, or open it in the browser. The page is self-contained and Rox-branded; it shows the amount, a QR to pay with the Rox app, and a card form. You don’t render or style anything.

express
res.redirect(session.checkoutUrl);
// or return it to your frontend and set window.location = checkoutUrl

When the payment finishes, the page sends the shopper to your successUrl (or cancelUrl). Treat the redirect as a UX signal only — the source of truth is the webhook (a shopper can close the tab before redirect). Fulfil on the webhook, not the redirect.

3Payment methods

Both methods run on the hosted page — you don’t implement either. For reference:

Pay with the Rox app (QR)

The shopper scans the on-page QR in their Rox app and confirms; funds move wallet → your merchant. Behind the scenes the app calls POST /v1/checkout/:id/pay with the cardholder’s JWT.

Pay with a Rox card

The shopper enters card number / expiry (MM/YYYY) / CVV in the PCI iframe. The page calls POST /v1/checkout/public/:id/pay-card, which returns one of:

ResultMeaning
{ status:"paid", paymentRef }Captured. Webhook fires.
{ status:"requires_action", reason:"3ds_required", authorizationId }3-D Secure step-up — the shopper approves in their Rox app; the page keeps polling until paid or declined.
{ status:"declined", reason }Rejected (funds / risk / bad card).
You handle none of this. 3-DS, retries, and card capture all live on the Rox page. Your only integration surface is create → webhook → fulfil.

4Handle the webhook

On every terminal status change Rox POSTs a JSON body to your webhookUrl. Verify the signature over the raw body before trusting it, then act on the status.

webhook handler
import { verifyWebhookSignature } from "@rox/checkout-sdk";

// IMPORTANT: raw body, not parsed JSON
app.post("/webhooks/rox", express.raw({ type: "*/*" }), async (req, res) => {
  const raw = req.body.toString("utf8");
  const ok = await verifyWebhookSignature({
    rawBody:   raw,
    signature: req.get("X-Rox-Signature"),
    timestamp: req.get("X-Rox-Timestamp"),
    secret:    process.env.ROX_WEBHOOK_SECRET,   // whsec_…
  });
  if (!ok) return res.sendStatus(400);

  const evt = JSON.parse(raw);
  if (evt.status === "paid") await fulfilOrder(evt.orderRef, evt);
  res.sendStatus(200);   // 2xx = delivered; anything else is retried
});
$raw = file_get_contents("php://input");
$ts  = $_SERVER["HTTP_X_ROX_TIMESTAMP"];
$sig = $_SERVER["HTTP_X_ROX_SIGNATURE"];
$expected = "sha256=" . hash_hmac("sha256", "$ts.$raw", getenv("ROX_WEBHOOK_SECRET"));

if (!hash_equals($expected, $sig) || abs(time() - (int)$ts) > 300) {
  http_response_code(400); exit;
}
$evt = json_decode($raw, true);
if ($evt["status"] === "paid") fulfil_order($evt["orderRef"], $evt);
http_response_code(200);
Verify over the raw bytes, parse after. Reject deliveries whose X-Rox-Timestamp is more than 5 minutes from now, and compare signatures in constant time. See Webhooks for the full scheme.

5Confirm & fulfil

On a verified checkout.paid: mark the order paid idempotently (you may receive a webhook more than once — key on id / orderRef), then fulfil / credit the shopper. If the webhook never arrives (endpoint down), poll the authoritative status:

poll fallback
curl https://api.roxcards.com/v1/checkout/sessions/$ID \
  -H "X-Api-Key: pk_live_xxx"
// → { "status":"paid", "paymentRef":"…", "paidAt":"…", "refundedMinor":"0", … }

Reconcile a shipment/credit against the money by matching your orderRef to the settlement report (below).

§API reference

Base https://api.roxcards.com. Server endpoints require X-Api-Key: pk_….

POST/v1/checkout/sessions — create

See step 1 for the body and response.

GET/v1/checkout/sessions/:id — retrieve

Authoritative status for one of your sessions. Lazily flips an expired open session to expired (and fires its webhook). Returns:

200
{ "id":"…", "status":"paid", "amountMinor":"1500000", "currency":"EGPR",
  "orderRef":"ORDER-1042", "paymentRef":"…", "paymentMethod":"card",
  "refundedMinor":"0", "paidAt":"…", "expiresAt":"…" }

POST/v1/checkout/sessions/:id/refund — refund

Body { "amountMinor": "500000" } — omit for a full refund of the remaining balance. Never more than captured − already-refunded.

  • QR / wallet payments — full or partial; reverses merchant → payer wallet.
  • Card payments — full only; reverses merchant → cardholder over the card rail.

Emits checkout.refunded / checkout.partially_refunded. Returns { id, status, refundedMinor, amountMinor, paymentRef }.

GET/v1/checkout/settlement?from=&to= — reconciliation

Totals over paid sessions in the window (ISO dates; default last 30 days).

200
{ "from":"…", "to":"…", "currency":"EGPR", "feeBps":150,
  "count":42, "grossMinor":"63000000", "feesMinor":"945000",
  "refundsMinor":"1500000", "netMinor":"60555000" }

net = gross − fees(MDR) − refunds. The merchant absorbs the MDR; the rate comes from the acquirer bank (or a per-merchant override).

§Webhooks

Sent on every terminal status change: paid, failed, expired, refunded, partially_refunded. Delivery retries with backoff; if it never succeeds, poll GET /v1/checkout/sessions/:id.

HeaderValue
X-Rox-Signaturesha256=<hex>
X-Rox-Timestampunix seconds
X-Rox-Eventcheckout.<status>
body
{ "id":"…", "status":"paid", "orderRef":"ORDER-1042",
  "amountMinor":"1500000", "currency":"EGPR",
  "paymentRef":"<onchain sig / auth id>", "paidAt":"…", "refundedMinor":"0" }

Signature scheme

verify
signedPayload = `${X-Rox-Timestamp}.${rawRequestBody}`
expected      = "sha256=" + hex( HMAC_SHA256( webhookSecret, signedPayload ) )

Recompute the HMAC over ${timestamp}.${rawBody} with your webhookSecret, compare to X-Rox-Signature in constant time, and reject timestamps > 5 min old. The SDK’s verifyWebhookSignature() does all three.

§Refunds & reconciliation

Refund with POST /v1/checkout/sessions/:id/refund (full or, for QR payments, partial). Pull period totals with GET /v1/checkout/settlement and reconcile each shipment/credit to a paid session by orderRef. Refunds emit their own webhook so your ledger stays in sync automatically.

§Testing

  • Use your pk_test_… key against the same endpoints to create test sessions without moving real funds.
  • Drive a full pass: create → open checkoutUrl → pay (test card or the Rox app on a test wallet) → confirm your webhook fires and you fulfilled once.
  • Point webhookUrl at a tunnel (e.g. a request-bin / ngrok URL) while developing, then switch to your real endpoint for go-live.
  • Test the unhappy paths too: let a session expire, trigger a declined card, and issue a refund — verify each webhook and your idempotency.

§Errors & idempotency

  • Auth — a missing/invalid X-Api-Key returns 401. You only ever see your own sessions.
  • Validation — bad body (e.g. amountMinor ≤ 0, missing fields) returns 400 with details.
  • Capture is idempotent — a session pays at most once; a racing second capture is rejected. Expired / cancelled / already-paid sessions can’t be paid.
  • Webhooks may repeat — always key fulfilment on id/orderRef and make it safe to run twice.
  • Return 2xx from your webhook to acknowledge; any other status (or a timeout) triggers a retry.

§Go-live checklist

  • Swap pk_test_…pk_live_…; keys live in server env only.
  • Amounts are integer strings in EGPR minor units (6 decimals) — no floats.
  • Webhook endpoint verifies the signature over the raw body + enforces the 5-min timestamp window.
  • Fulfilment is idempotent (keyed on id/orderRef) and returns 2xx.
  • You fulfil on the webhook, not the browser redirect; poll as a fallback.
  • Refund + settlement flows tested; you reconcile by orderRef.
  • Unhappy paths handled: expired, declined, partially refunded.

§SDK

@rox/checkout-sdk — dependency-free ESM for Node 18+ (any fetch + WebCrypto runtime). Keep the API key server-side.

MethodDoes
new RoxCheckout({ apiKey, baseUrl? })Client.
createSession(input)Create a session → { checkoutUrl, id, … }.
retrieveSession(id)Authoritative status.
refund(id, { amountMinor? })Full or partial refund.
settlement({ from?, to? })Period reconciliation totals.
verifyWebhookSignature({ rawBody, signature, timestamp, secret, toleranceSec? })Verify a webhook (HMAC + timestamp + constant-time).
Rox Checkout · v1 · API api.roxcards.com · Hosted page checkout.roxcards.com. Need a partner key or a sandbox? Contact your Rox integration manager. Keep your API key and webhook secret confidential.