@sandpay/node, or a hand-rolled client in Deno / Python / Go via raw
HTTP).
SandPay is the source of truth. Pin your behaviour to what
GET /v1/meta
reports and what the Integration Changelog says —
never to a hardcoded assumption. This runbook stays valid across versions
because every version-specific detail comes from those two places.Which track are you on?
Track A — Upgrade
You already have a working integration on an older API version and want to
move to the current one (adopt refunds/disbursements, etc.).
Track B — Install / reinstall
You’re integrating from scratch, or tearing out the old integration and
redoing it cleanly.
Track A — Upgrade an existing integration
Take stock: current vs target version
Ask the instance what it supports. The version it reports is your target:Then read the Integration Changelog from your
current version down to the target — note every “Action required?” line.
Bump the client to ≥ min_sdk_version
- Node:
npm i @sandpay/node@latest(must be ≥min_sdk_version). - Hand-rolled client (Deno/Python/Go): mirror the new methods/fields manually (the new endpoints + response fields are in the changelog and the Integration Guide).
Migrate the database — only if you self-host SandPay
Against the hosted
api.sandpay.dev: nothing to do. If you run your own
instance: corepack pnpm --filter sandpay-app run db:migrate.Apply each 'Action required' change
Do exactly what the changelog entries say. As of
2026-05-30 the only one
that affects existing code is: branch your webhook handler on event
(so refunds/disbursements aren’t mistaken for collections) — see
Webhook receiver below. Everything else is
additive and needs no change.Track B — Install (or reinstall) from scratch
Step 0 — Prerequisites
- A SandPay account + an API key (
sp_sk_test_…). - At least one operator environment configured in the dashboard
(Settings → Countries & operators), e.g.
MTN / RW / RWF. - A webhook signing secret (shown in the dashboard webhook settings).
Step 1 — Environment variables (server-side only)
Step 2 — Install the client
Authorization: Bearer $SANDPAY_API_KEY
against ${SANDPAY_BASE_URL}/api/v1/… (hosted: /v1/…).
Step 3 — Smoke-test connectivity + capabilities
api_version, min_sdk_version, and that capabilities lists what you
plan to use (payments, and refunds/disbursements if you need payouts).
Feature-detect against this rather than assuming.
Step 4 — Create a payment (collection: customer → merchant)
id (your provider_ref). Map all 11 statuses; only
PENDING is non-terminal. USER_CANCELLED is terminal (don’t poll forever).
See Status vocabulary.
Step 5 — Get final status (poll + webhook)
GET /v1/payments/{id} (~every 4s) as the resilient fallback,
and the webhook (below) as the instant push. Reconcile your merchant ledger on
net_amount, not amount (operator commission). See
Commission & settlement.
Step 6 — Webhook receiver
A small endpoint on your backend that SandPay POSTs to. Configure its URL in the dashboard. It must:- Verify the HMAC signature in constant time, fail closed if the secret
is missing. The header is
X-SandPay-Signature: sha256=<hex>over the raw body. - Be deployed without JWT auth (authenticity comes from the signature).
- Match the transaction by
tx_id(SandPay does not echo yourreference). - Branch on
event— this is the one thing that bites people:
status — check it, not just the event name.
See Webhooks.
Step 7 — Payouts (optional: refunds & disbursements)
Only ifcapabilities includes them (gate at runtime):
Step 8 — Local development
Your webhook URL must be publicly reachable, and SandPay dispatches webhooks via Inngest. So locally:- Expose your backend with a stable tunnel (ngrok reserved domain) and set
SANDPAY_BASE_URL/webhook URL once. See the tunnel setup. - Run SandPay’s Inngest dev server or rely on polling (Step 5) — see Inngest local vs cloud.
Step 9 — Final checklist
-
SANDPAY_*in server env only; base URL has no trailing slash. - Operator env enabled in the dashboard.
- Collection create → store
id; all 11 statuses mapped;USER_CANCELLEDterminal. - Status resolved by polling + webhook; ledger reconciled on
net_amount. - Webhook:
--no-verify-jwt, constant-time HMAC, match bytx_id, branch onevent. - Webhook URL + secret set in the dashboard (identical secret both sides).
- Payouts gated on
capabilities(if used). - Local dev: tunnel + (Inngest dev server or polling).
-
GET /v1/metareports the expected version after deploy.
See also
Integration Guide
The full, authoritative reference.
Integration Changelog
What changed per version — and what to do about it.
Integration FAQ
Common pitfalls + tips.
Quickstart
First payment in 10 minutes.