> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sandpay.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Integration Changelog

> Dated, versioned record of every change to the SandPay integration contract — and exactly what (if anything) you must do to stay current.

This page is the **source of truth** for how the SandPay integration contract
evolves. Every entry is dated, carries an **API version**, and tells you plainly
whether **action is required** on your side.

<Note>
  **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.
</Note>

## How to stay current (the update mode)

SandPay surfaces its version at runtime so you never have to guess what an
instance supports:

<CardGroup cols={2}>
  <Card title="GET /v1/meta" icon="circle-info">
    Unauthenticated. Returns the API version, supported capabilities, the
    minimum recommended SDK version, and a link back to this page.
  </Card>

  <Card title="X-SandPay-Api-Version" icon="tag">
    Every `/v1/*` response carries this header — so any call already tells you
    which contract you're talking to.
  </Card>
</CardGroup>

```bash theme={null}
curl https://api.sandpay.dev/v1/meta
# or, against a local/self-hosted instance:
curl $SANDPAY_BASE_URL/api/v1/meta
```

```json theme={null}
{
  "api_version": "2026-05-31",
  "capabilities": ["payments", "refunds", "disbursements", "webhooks", "test_clients", "multi_environment", "commission_split", "order_linkage", "reference_format"],
  "min_sdk_version": "0.2.0",
  "changelog_url": "https://docs.sandpay.dev/integration-changelog"
}
```

**Feature-detect instead of assuming.** Before calling a newer endpoint, check
the capability is present — your code then works against older instances too:

```ts theme={null}
const { capabilities } = await sandpay.meta();
if (capabilities.includes("disbursements")) {
  await sandpay.disbursements.create({ /* … */ });
}
```

### The upgrade procedure (every time an entry says "action required")

<Steps>
  <Step title="Read the entry">
    Find the latest dated entry below and its **Action required?** line.
  </Step>

  <Step title="Bump the SDK">
    `npm i @drwintech/sandpay@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.
  </Step>

  <Step title="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`.)
  </Step>

  <Step title="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.
  </Step>

  <Step title="Verify">
    Re-run your integration tests against the new version. `GET /v1/meta` should
    report the version you expect.
  </Step>
</Steps>

<Info>
  **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`.
</Info>

***

## Changelog

### 2026-05-31 — Canonical reference format `api_version: 2026-05-31`

A recommended shape for the `reference` idempotency key. **Additive.**

* **New convention:** `reference` follows `ORIGIN-OP-TS-CODE` (e.g.
  `ZANA-PAY-20260531221015-8E2884A47B1C`) — `ORIGIN` = your app slug (`SP` for
  SandPay), `OP` ∈ `PAY` (collection) / `REF` (refund) / `DIS` (disbursement) /
  `ABO` (subscription), `TS` = compact UTC datetime `YYYYMMDDHHMMSS` (sortable),
  `CODE` = 12-char uppercase hex (random for a fresh attempt, or derived from a
  stable id — with a stable `TS` — for idempotent retries). Capability:
  `reference_format`. Parsing is lenient — pre-`TS` refs still recognized.
* **Changed (additive):** when you **omit** `reference` on a payout, SandPay's
  auto-generated default now uses this format (`SP-REF-…` / `SP-DIS-…`, a refund
  inheriting its parent's `ORIGIN`) instead of the old `RFND_…` / `DISB_…`. Only
  affects callers that don't send their own `reference`.
* **SDK:** `buildReference()`, `referenceForType()`, `deriveRefCode()`,
  `generateRefCode()`, `parseReference()`, `isCanonicalReference()` exported from
  `@drwintech/sandpay` **`0.2.1`**.

<Note>
  **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](/en/integration-guide#reference-format-origin-op-ts-code).
</Note>

### 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 a `SUCCESS`
  collection (linked via `parentTxId`).
* **New:** `POST /v1/disbursements` — free-form payout to an msisdn.
* **New:** transactions carry `type` (`collection` | `refund` | `disbursement`)
  and `parentTxId`; filter with `GET /v1/payments?type=…`.
* **New webhook events:** `payment.refunded`, `disbursement.completed` (each
  carries the terminal `status`). Collections still fire `payment.completed`
  unchanged.
* **New:** `GET /v1/meta` + `X-SandPay-Api-Version` header (this update mode).
* **New (order linkage):** optional `order_ref` + `order_url` on
  `POST /v1/payments` (and `/v1/disbursements`). `order_ref` is the grouping key
  that ties a collection, its refunds, and any recurring charges together; a
  refund inherits its parent's. Both are echoed on `Payment`
  (`orderRef`/`orderUrl`) and the webhook (`order_ref`/`order_url`). The
  dashboard gains an **Orders** page (group by order) + an `order` filter on
  History. Capability: `order_linkage`. Optional `is_url_protected` (boolean)
  marks `order_url` as 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`.**

<Warning>
  **Action required (webhook consumers): branch on `event`.** Once you adopt
  refunds/disbursements, your endpoint will receive `payment.refunded` /
  `disbursement.completed` in addition to `payment.completed`. Switch on the
  `event` (or `type`) field before treating a delivery as a collection
  settlement, so a refund isn't mistaken for a new payment. If you never call the
  payout endpoints, no payout webhooks fire and **no action is needed yet** —
  but adding the branch now is the safe move. See
  [Integration Guide §5](/en/integration-guide#5-refunds--disbursements).
</Warning>

See [§5 of the Integration Guide](/en/integration-guide#5-refunds--disbursements).

### 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 %.

<Note>
  **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](/en/integration-guide#4-commission--settlement).
</Note>

### 2026-05-01 — Native operator `raw` payload

* **New (optional) field** `raw` on `Payment` + 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) and `GET /v1/payments/{id}`.
  **Additive — no action required.**

## See also

<CardGroup cols={2}>
  <Card title="Upgrade & (re)install" icon="rotate" href="/integration-upgrade">
    Step-by-step runbook to install, reinstall, or upgrade from your app.
  </Card>

  <Card title="Integration Guide" icon="book" href="/integration-guide">
    The full, authoritative integration reference.
  </Card>

  <Card title="Integration FAQ" icon="circle-question" href="/integration-faq">
    Common pitfalls + tips.
  </Card>
</CardGroup>
