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.
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: open → paid / 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
Self-service, from the Rox merchant (POS) app. The merchant owner opens the POS app → taps the storefront icon on the New payment screen → Store details → Integrations → Issue API key. The app shows both secrets once, runs a live connection test, and can rotate the key at any time. (Larger partners can also be provisioned by Rox directly.)
| Credential | Looks like | Use |
|---|---|---|
| API key | pk_… | Auth for every server call, in the X-Api-Key header. Shown once at creation. |
| Webhook secret | whsec_… | Verify webhook signatures. Shown with the key, and returned on every session create. |
§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.
| Thing | What it is |
|---|---|
| Merchant | Your business as a Rox merchant account. Must be KYC-approved with a provisioned wallet before it can receive payouts. |
| Merchant ID | The 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 wallet | The 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.
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.
# 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.
§Ready-made plugins — install, don’t integrate
Already running a store platform? Skip the API entirely: install the plugin, paste your two credentials from the POS app, and you’re accepting Rox — QR and Rox card (3-D Secure) — with automatic order confirmation and refunds from your own admin.
| Platform | Status | Get it |
|---|---|---|
| WooCommerce (WordPress) | Available | Download rox-payments.zip · install guide ↓ |
| Odoo POS (17) | Available | Download pos_rox.zip · install guide ↓ |
| Shopify | Code-ready | Offsite provider app built; awaiting Shopify Payments-Partner approval to register it. Interim: manual payment method + Rox pay link. |
| Anything else | — | Any platform that can make an HTTP call can integrate in an afternoon — start at the Quickstart. |
§WooCommerce — install in 5 minutes
The plugin adds a “Rox — app or card” payment method to your WooCommerce checkout. Shoppers are redirected to the hosted Rox page, pay by QR or Rox card, and land back on your order-received page. The order flips to Processing automatically.
Install
- Get credentials — Rox POS app → storefront icon → Store details → Integrations →
Issue API key. Copy the
pk_…API key andwhsec_…webhook secret (shown once). - Upload the plugin — WordPress admin → Plugins → Add New → Upload Plugin →
choose
rox-payments.zip→ Install Now → Activate. - Configure — WooCommerce → Settings → Payments → Rox → paste the API key and webhook secret → Save changes.
- Test — place a small order, pay it from a Rox app, and watch the order flip to Processing on its own.
What you get
- QR and Rox-card payments (card entry is on the Rox-hosted page — card data never touches your WordPress server)
- Automatic order confirmation by signed webhook, with a status-poll fallback when the shopper returns
- Refunds from the normal WooCommerce order screen (partial for QR payments; card payments refund in full)
- No webhook setup needed — the plugin registers its own endpoint (
/wc-api/rox_webhook) per payment
https:// URLs). Store currency is treated 1:1 as EGPR.§Odoo POS — install at the counter
The addon adds a Rox payment method to the Odoo 17 register with two collection modes: QR on screen (customer scans the register) or Rox SoftPOS terminal (the charge pops up on the merchant’s Rox POS phone — customer taps or scans there). Either way the Odoo sale completes automatically when Rox confirms the payment.
Install
- Get credentials — Rox POS app → storefront icon → Store details → Integrations →
Issue API key. Copy the
pk_…API key. - Add the addon — unzip
pos_rox.zipinto your Odooaddonspath, restart Odoo, update the apps list, and install Rox Payments (POS). - Create the payment method — Point of Sale → Configuration → Payment Methods → New: set Use a Payment Terminal to Rox, paste the API key, and pick the collection mode (QR on screen / Rox SoftPOS terminal).
- Enable it on your POS config, open a session, and tender a sale with Rox.
- The API key stays on the Odoo server — the register’s browser never sees it
- No JS dependencies — the QR renders with Odoo’s built-in barcode engine
- One payment method can be QR-on-screen, another SoftPOS — mix per register
§SoftPOS terminal — collect in person
For a physical counter, your POS/ERP doesn’t have to show the payment itself: create the
session with "channel": "softpos" and it appears as an incoming charge on the
merchant’s Rox POS phone. The customer taps their phone/card on it or scans the QR there —
then the same checkout.paid webhook closes the sale in your system.
channel: "softpos"checkout.paid → sale closesEverything else is identical to a hosted session: same create call (just add the
channel field), same status poll, same webhook, same refunds. Tap payments are
verified server-side against the session (amount + payee) before the webhook fires — the phone
is never trusted with the confirmation.
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
| Field | Type | Notes |
|---|---|---|
amountMinor required | string | EGPR minor units, integer > 0. "1500000" = 1.50 EGPR. |
currency | string | Default EGPR. |
orderRef | string | Your order id — echoed on the webhook & poll. Use it to reconcile. |
successUrl / cancelUrl | url | Where the hosted page sends the shopper after success / cancel. |
webhookUrl | url | Overrides your partner-default webhook for this session. |
channel | string | hosted (default) or softpos — route the charge to the merchant’s Rox POS phone instead of the hosted page. See SoftPOS terminal. |
expiresInSec | int | 60–86400. Default 1800 (30 min). |
metadata | object | Free-form; stored and returned to you. |
Response
{
"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_…"
}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.
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:
| Result | Meaning |
|---|---|
{ 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). |
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.
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);
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:
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:
{ "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).
{ "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.
| Header | Value |
|---|---|
X-Rox-Signature | sha256=<hex> |
X-Rox-Timestamp | unix seconds |
X-Rox-Event | checkout.<status> |
{ "id":"…", "status":"paid", "orderRef":"ORDER-1042",
"amountMinor":"1500000", "currency":"EGPR",
"paymentRef":"<onchain sig / auth id>", "paidAt":"…", "refundedMinor":"0" }Signature scheme
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
webhookUrlat 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-Keyreturns401. You only ever see your own sessions. - Validation — bad body (e.g.
amountMinor ≤ 0, missing fields) returns400with 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/orderRefand 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.
| Method | Does |
|---|---|
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). |
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.