SandPay is the source of truth. Your app (Zana or any other consumer) is a
client. When something changes here, update your integration to match — don’t
fork the contract.
How to stay current (the update mode)
SandPay surfaces its version at runtime so you never have to guess what an instance supports:GET /v1/meta
Unauthenticated. Returns the API version, supported capabilities, the
minimum recommended SDK version, and a link back to this page.
X-SandPay-Api-Version
Every
/v1/* response carries this header — so any call already tells you
which contract you’re talking to.The upgrade procedure (every time an entry says “action required”)
Bump the SDK
npm i @sandpay/node@latest (≥ the min_sdk_version from /v1/meta). If you
hand-rolled a client (e.g. a Deno/Supabase Edge function), mirror the new
methods/fields manually.Migrate a self-hosted instance
If you run your own SandPay instance, apply DB migrations:
corepack pnpm --filter sandpay-app run db:migrate. (Not needed against the
hosted api.sandpay.dev.)Apply the code change
Do whatever the entry’s Action required? line says (e.g. branch your
webhook handler on
event). Additive-only entries need nothing.Compatibility policy. Within
v1, changes are additive — new
endpoints, new optional fields, new webhook event names. Existing fields and
the meaning of existing values never change under v1. A breaking change would
ship under a new major path (v2) with this page calling it out and a
deprecation window on v1.Changelog
2026-05-31 — Canonical reference format api_version: 2026-05-31
A recommended shape for the reference idempotency key. Additive.
- New convention:
referencefollowsORIGIN-OP-TS-CODE(e.g.ZANA-PAY-20260531221015-8E2884A47B1C) —ORIGIN= your app slug (SPfor SandPay),OP∈PAY(collection) /REF(refund) /DIS(disbursement) /ABO(subscription),TS= compact UTC datetimeYYYYMMDDHHMMSS(sortable),CODE= 12-char uppercase hex (random for a fresh attempt, or derived from a stable id — with a stableTS— for idempotent retries). Capability:reference_format. Parsing is lenient — pre-TSrefs still recognized. - Changed (additive): when you omit
referenceon a payout, SandPay’s auto-generated default now uses this format (SP-REF-…/SP-DIS-…, a refund inheriting its parent’sORIGIN) instead of the oldRFND_…/DISB_…. Only affects callers that don’t send their ownreference. - SDK:
buildReference(),referenceForType(),deriveRefCode(),generateRefCode(),parseReference(),isCanonicalReference()exported from@sandpay/node0.2.1.
Action required: none.
reference is still your idempotency key and any
string still works — this is a recommended convention, not an enforced one.
Adopting it makes the dashboard consistent and references self-describing. It’s
independent of order_ref (the grouping key) — keep sending that too. See
Integration Guide §5 → Reference format.2026-05-30 — Refunds & disbursements api_version: 2026-05-30
The outbound money direction (merchant → customer). Additive.
- New:
POST /v1/payments/{id}/refund— refund all/part of aSUCCESScollection (linked viaparentTxId). - New:
POST /v1/disbursements— free-form payout to an msisdn. - New: transactions carry
type(collection|refund|disbursement) andparentTxId; filter withGET /v1/payments?type=…. - New webhook events:
payment.refunded,disbursement.completed(each carries the terminalstatus). Collections still firepayment.completedunchanged. - New:
GET /v1/meta+X-SandPay-Api-Versionheader (this update mode). - New (order linkage): optional
order_ref+order_urlonPOST /v1/payments(and/v1/disbursements).order_refis the grouping key that ties a collection, its refunds, and any recurring charges together; a refund inherits its parent’s. Both are echoed onPayment(orderRef/orderUrl) and the webhook (order_ref/order_url). The dashboard gains an Orders page (group by order) + anorderfilter on History. Capability:order_linkage. Optionalis_url_protected(boolean) marksorder_urlas permission-protected → a ”⚠ protected URL” hint in the dashboard. - SDK:
sandpay.payments.refund(),sandpay.disbursements.*,sandpay.meta(),PaymentInput.order_ref/order_url. Min SDK:0.2.0.
2026-05-15 — Configurable commission split
- New (optional) fields on
Payment+ the webhook:commission,netAmount,customerTotal,merchantAbsorptionPct,merchantShare,customerShare,commissionMode. - Per-environment commission rate (
commission_bps) + merchant-absorption %.
Action required: reconcile your merchant ledger against
netAmount,
not amount. With the default config (merchant absorbs 100%) customerTotal
still equals amount, so existing reconciliation that read amount is not
wrong — but netAmount is the correct field. See
Integration Guide §4.2026-05-01 — Native operator raw payload
- New (optional) field
rawonPayment+ the webhook — the operator-native response shape (synthesized in sandbox with_simulated: true; verbatim in production). Additive — no action required.
2026-04-20 — List & retrieve payments
- New:
GET /v1/payments(cursor pagination) andGET /v1/payments/{id}. Additive — no action required.
See also
Upgrade & (re)install
Step-by-step runbook to install, reinstall, or upgrade from your app.
Integration Guide
The full, authoritative integration reference.
Integration FAQ
Common pitfalls + tips.