Warning
Interactive reference needs JavaScript
Below is the endpoint index. For full schemas and examples, download the OpenAPI document.
Three authenticated surfaces
Two of them share the /api/v1 prefix and differ by which credential you send.
Merchant
/api/v1
Fiscalise and read one tenant's own sales. Tenant bearer token.
Partner
/api/v1
Provision and manage the merchants you fiscalise for. Partner key with scopes.
Compatibility
/zm/api
Header-auth (X-Api-Key + X-SdcId) compatibility surface for legacy VSDC gateways. Request field names carry over; responses are returned bare.
Base URLs
https://gateway.mikenyambe.com
Production
Endpoint paths are absolute — append them to a base URL exactly as documented.
Authentication
Scheme
Transport
Used by
PartnerKey
Authorization: Bearer <token>
Partner secret key, e.g. sk_live_… / sk_test_…. Note that sk_test_ is a privilege tier, not a separate environment — it files real fiscal invoices with ZRA. See "Get access" on /docs.
TenantToken
Authorization: Bearer <token>
Tenant API token (Sanctum). Every token carries two abilities, `invoices:read` and `invoices:write`, whether it was minted on the merchant's own Developers screen or through `POST /api/v1/tenants/{tenant}/tokens`.
**Those abilities are enforced.** Reads require `invoices:read` and writes require `invoices:write`; a token lacking the one a route needs is answered **403**, not 401. Every token issued to date carries both, so nothing that works today is affected — but the scope shown beside a token on the Developers screen is now the scope the routes actually check. Before 2026-09-09 it was not: no ability middleware was registered, and a token was reachable on every route regardless of what its abilities said. See the 1.7.1 changelog entry.
ZmApiKey
X-Api-Key: <value>
The fiscalisation key. Either a branch key, bound to one device, or a tenant key, which files through whichever of that taxpayer's branches the `X-SdcId` names. Sent with `X-SdcId` on every `/zm/api` call except `GET /zm/api/Branches`, which is how a till learns which SDC ids this key accepts.
ZmSdcId
X-SdcId: <value>
The SDC id ZRA issued for the branch device, as returned by device initialisation and listed by `GET /zm/api/Branches`. It must name a branch the `X-Api-Key` may file through — its own device for a branch key, any branch of the taxpayer for a tenant key — and anything else is answered 401 rather than resolved to a default. `Terminal-Id` is accepted as a legacy alias.
Fiscalise a sale
Sales
The one endpoint that fiscalises, plus reading and crediting what you have filed.
**Rate limits.** Every endpoint is throttled per tenant at a ceiling set by the tenant plan (per minute): Free Trial 60, Starter 120, Growth 300, Enterprise 600. Without a plan value the defaults are 120/min on `/api/v1` and 300/min on `/zm/api`. Exceeding it returns **429** with `Retry-After` and `X-RateLimit-*`. Size an integration for the plan of the merchant it runs for, not the plan it was tested against — and drain offline backlogs smoothly, because a queue flush is exactly the traffic shape that hits the ceiling.
Method
Endpoint
Description
Scopes
GET
/api/v1/sales
Find a sale again by your own reference
—
POST
/api/v1/sales
Fiscalise a sale
—
GET
/api/v1/sales/{sale}
Check whether a sale reached ZRA
—
POST
/api/v1/sales/{sale}/credit-note
Refund or cancel a fiscalised sale
—
POST
/api/v1/sales/{sale}/debit-note
Charge more against a fiscalised sale
—
Account
Confirm the token works and see which branches it may file on. `initialized: true` on the branch you bill through is the precondition behind the 422 that stops most otherwise-correct first requests.
Method
Endpoint
Description
Scopes
GET
/api/v1/me
Check this token can file, and on which branches
—
Customers
The people and businesses a merchant sells to. Two separate things happen here and they fail independently: **verification** is what ZRA says about a TPIN, and **registration** is ZRA accepting the customer onto that branch's list. A customer can be one without the other.
A customer with no TPIN is a local record and is never sent to ZRA — that is the ordinary walk-in, not a degraded case.
Method
Endpoint
Description
Scopes
GET
/api/v1/customers
Find a customer, usually by TPIN
—
POST
/api/v1/customers
Add a customer, and register it with ZRA
—
GET
/api/v1/customers/{customer}
Read one customer, and how far its registration got
—
Items
The catalogue a merchant sells from. Every ledger after this one names an `itemCode`, so a stock movement, a purchase line and an import declaration all point here.
**Read `registration.state` before you assume anything was sent.** It carries a value the other resources do not: `draft`. A draft is a row the gateway created for you — from a sale line naming a product it did not hold, or from your own history — and it has deliberately NOT been sent to ZRA. Its classification was inferred, and `itemClsCd` is what ZRA taxes against, so releasing it is a decision a person makes. A draft is not a registration running late: polling one for `registered` waits forever.
Method
Endpoint
Description
Scopes
GET
/api/v1/items
The catalogue, or the drafts waiting to be reviewed
—
POST
/api/v1/items
Add an item, and register it with ZRA
—
GET
/api/v1/items/{item}
One item, by id or by ZRA item code
—
PATCH
/api/v1/items/{item}
Amend an item, and tell ZRA
—
POST
/api/v1/items/{item}/release
Release a reviewed draft to ZRA
—
Imports
Customs declarations ZRA has addressed to the merchant's branch, and the answer to each line. Read and answer only — **there is no way to create one**, and that is deliberate: a declaration comes from ASYCUDA through ZRA, the merchant is not its author, and a posted one would be a record ZRA could never match against a real declaration. Rows appear when the gateway's hourly pull finds them.
**Approving needs an `itemCode`; rejecting does not.** ZRA refuses an approval that does not say what the merchant calls the goods — the declaration names them the way customs does, and the stock movement is filed against your own code. A rejection carries no such requirement, because refusing goods you never received should not oblige you to invent a product record for them.
**`decision` and `filing` are separate facts and fail separately.** `decision` is what the merchant said, and exists the moment they answer. `filing` is whether ZRA has accepted that answer, and can be `sending` or `failed` long afterwards. A failed filing is not a reversed decision.
Method
Endpoint
Description
Scopes
GET
/api/v1/import-items
Customs declaration lines addressed to this taxpayer
—
GET
/api/v1/import-items/{importItem}
Read one declaration line
—
POST
/api/v1/import-items/{importItem}/approve
Accept the goods on a declaration line
—
POST
/api/v1/import-items/{importItem}/reject
Refuse the goods on a declaration line
—
Webhook endpoints
Register and debug the endpoints we deliver to. The events themselves — the payloads you implement — are under **Webhooks** at the end of this document.
Method
Endpoint
Description
Scopes
GET
/api/v1/webhooks
List the partner's webhook endpoints
webhooks:read
POST
/api/v1/webhooks
Be told when a sale reaches ZRA, instead of polling
webhooks:write
GET
/api/v1/webhooks/{id}
Retrieve one endpoint
webhooks:read
DELETE
/api/v1/webhooks/{id}
Delete an endpoint
webhooks:write
POST
/api/v1/webhooks/{id}/enable
Re-enable an auto-disabled endpoint
webhooks:write
GET
/api/v1/webhooks/{id}/deliveries
The delivery log for an endpoint (paginated, newest first)
webhooks:read
POST
/api/v1/webhooks/deliveries/{id}/resend
Re-queue a delivery for immediate redelivery
webhooks:write
Catalogue
ZRA item classification codes. For bulk mapping download `/docs/zra-item-classes.csv.gz` — one credential-free request instead of roughly 500 paged ones.
Since **1.6.0** a `classificationCode` you send is checked against ZRA's published catalogue before the invoice is filed. Do not rely on ZRA to catch a wrong one: `saveSales` accepts any string, so an invented or mistyped code was previously filed and answered `000`, and only surfaced later when the item it names could not be registered.
Method
Endpoint
Description
Scopes
GET
/api/v1/item-classes
ZRA item classification codes
—
Move an existing till
Zambia (compat)
The `/zm/api/…` compatibility surface. Authenticate with two headers — `X-Api-Key` (the branch fiscalisation key) and `X-SdcId` (that branch SDC id, also accepted as `Terminal-Id`); there is no bearer token and no request signing.
Request field names are the legacy ones, so a till already speaking the modern `/zm/api` dialect keeps its payload almost unchanged. Responses come back **bare** by default, with no `{success, message, data}` wrapper around them — the one difference that breaks a legacy till on its first call.
**Since 1.9.0 that is a choice, not a constraint.** A key can be minted with `dialect: wrapped`, and every reply to it is then wrapped in the old envelope: a success as `{success: true, message: "", data: {…}}`, a failure as `{success: false, httpCode, …}` keeping every field the bare error had. Request bodies and field names are identical in both, and **the HTTP status code is unchanged** — a wrapped error is still a 4xx, so status remains the reliable thing to branch on. Rewriting a till to read bare responses is still the better end state; the envelope is for when that rewrite is not available to you.
The surface is also `ReceiptTypeCode: S`-only, and 402/403/429 can occur on every operation. Read `/docs/migrating-a-till` before pointing a live till here.
Method
Endpoint
Description
Scopes
POST
/api/ApiKey/getactive
Fetch a till's key and branches with a till login
—
GET
/zm/api/Branches
List the branches this key may file through
—
GET
/zm/api/TaxCodes
List the Zambia tax categories and rates
—
POST
/zm/api/Invoice
Fiscalise a sale
—
GET
/zm/api/Invoice/{clientInvoicenumber}
Retrieve a fiscalised invoice by your own invoice number
—
GET
/zm/api/Signature/{invoiceNumber}
Reprint — identical payload to GET /zm/api/Invoice/{n}
—
Provision merchants (partners)
Tenants
Partner-provisioned merchants. Only needed if you fiscalise for businesses other than your own.
Method
Endpoint
Description
Scopes
GET
/api/v1/tenants
List the partner's tenants
tenants:read
POST
/api/v1/tenants
Provision a new merchant tenant
tenants:write
GET
/api/v1/tenants/{tenant}
Retrieve one tenant (with its devices)
tenants:read
Devices
Branch devices under a tenant. Registration is local and freely correctable; initialisation is a one-time handshake with ZRA against the device serial, and ZRA will not hand out that first response twice.
Method
Endpoint
Description
Scopes
POST
/api/v1/tenants/{tenant}/devices
Register a branch device (local only; not yet initialized)
devices:write
POST
/api/v1/tenants/{tenant}/devices/{branch}/initialize
Initialise a branch with ZRA — one-shot, irreversible
devices:write
Merchant tokens
The credential that actually fiscalises. Mint one per tenant once that tenant branch device is initialised.
Method
Endpoint
Description
Scopes
GET
/api/v1/tenants/{tenant}/tokens
List a tenant's merchant bearer tokens
keys:write
POST
/api/v1/tenants/{tenant}/tokens
Mint the merchant token that fiscalises
keys:write
DELETE
/api/v1/tenants/{tenant}/tokens/{token}
Revoke a merchant bearer token
keys:write
Branch keys
Branch fiscalisation keys (`pk_…`) for the `/zm/api` surface. These do **not** authenticate `/api/v1/sales` — that needs a tenant bearer token from `POST /api/v1/tenants/{tenant}/tokens`.
Method
Endpoint
Description
Scopes
GET
/api/v1/tenants/{tenant}/api-keys
List a tenant's fiscalisation keys (masked)
keys:write
POST
/api/v1/tenants/{tenant}/api-keys
Mint a fiscalisation key (plaintext returned once)
keys:write
DELETE
/api/v1/tenants/{tenant}/api-keys/{apiKey}
Revoke a fiscalisation key
keys:write
Partner Keys
A partner self-service management of its own secret keys
Method
Endpoint
Description
Scopes
GET
/api/v1/partner/keys
List the partner's own secret keys (masked)
keys:write
POST
/api/v1/partner/keys
Mint a least-privilege key
keys:write
GET
/api/v1/partner/keys/{id}/secret
Read a key's plaintext back
keys:write
POST
/api/v1/partner/keys/{id}/rotate
Rotate a key (mint replacement, sunset the old one after a grace window)
keys:write
DELETE
/api/v1/partner/keys/{id}
Revoke one of the partner's own keys
keys:write