Developer documentation

v1.10.0

ZRA Gateway API

Fiscalise ZRA Smart Invoice sales from your POS, ERP or till software. A working integration is one endpoint and one credential — start here, and reach for the rest only when something brings you to it.

Getting started

Five steps from nothing to a fiscalised invoice. This is the whole minimum integration.

  1. 01

    Point at the gateway

    One base URL, HTTPS only. Every path in these docs is absolute and carries its own surface prefix.

    https://gateway.mikenyambe.com

  2. 02

    Get a token

    Sign in and complete Go live first — your TPIN, branch and device serial. A token alone cannot file under your TPIN: until that device is initialised against ZRA, POST /api/v1/sales answers 422 No initialized ZRA device. Go-live is reviewed by an operator, so allow time for it. Then Developers → API tokens — copy the token, or view it again later from the same page after confirming your password. Send it on every request:

    Authorization: Bearer <your token>

    Check you are ready with GET /api/v1/me: the branch you file on must show "initialized": true.

    Integrating on behalf of many merchants, or already running against a legacy VSDC gateway? Those use a different credential — see “Other ways in” below.

  3. 03

    Post the sale

    One endpoint fiscalises: POST /api/v1/sales. Four fields per line item are required — everything else has a default.

    POST /api/v1/sales The minimum request
    curl -X POST https://gateway.mikenyambe.com/api/v1/sales \
      -H "Authorization: Bearer <your token>" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: sale-1042" \
      -d '{
        "items": [{
          "name": "Maize meal 25kg",
          "classificationCode": "50221204",
          "quantity": 2,
          "price": 350.00
        }]
      }'

    What a field you leave out becomes

    vatCategory Standard rated (A) — 16% VAT.
    paymentMethod Cash (01).
    taxInclusive true — the price you send already contains the VAT.
    occurredAt Now — which is what a till at the counter means.
    customer Omitted. Send a name and TPIN when the buyer needs to claim the VAT.

    Prices are in kwacha — the gateway converts nothing. classificationCode is ZRA's: any one valid code gets you started, and the full catalogue is a download for when you map your product list.

  4. 04

    Read status, then print

    The one field your code must branch on. uploaded means ZRA has the document and the fiscal block is a legal receipt.

    201 Created Response
    {
      "data": {
        "id": 1042,
        "invoiceNo": 180,
        "reference": "180",
        "status": "uploaded",
        "totals": { "taxable": 603.45, "vat": 96.55, "total": 700.00 },
        "fiscal": {
          "sdcId": "SDC0010000001",
          "receiptSignature": "KJH2-8DJF-2LKD-9982",
          "qrCodeUrl": "https://.../verify/..."
        }
      }
    }

    Pending is not filed

    A 2xx does not always mean ZRA has it

    When ZRA is unreachable the sale is still accepted and queued, and the response comes back with "status": "pending" and every fiscal.* field null. Mark that receipt provisional — it is not a fiscal receipt until the document uploads. Only uploaded may be printed as fiscal. Read the sale again later with GET /api/v1/sales/{id} — or, if you hold a partner key, let a webhook tell you instead.

  5. 05

    Handle three errors

    Everything else is a 4xx you can read. These three are the ones a live till actually meets.

    422 Your payload. The message names the field and the values it accepts — show it to the developer, not the cashier. One exception: a 422 reading No initialized ZRA device is not about your payload — that branch has not finished go-live.
    402 The subscription is inactive or out of quota. Nothing your code can retry — surface it to the merchant.
    429 Rate limited. Wait the seconds given in Retry-After, then send it again.

    A rejection from ZRA itself carries resultCode — branch on the code, never the prose. The result-code table says which mean fix the data, which mean wait, and which mean stop the line.

    That is the integration. Send the sale, read status, handle those three. Everything below is optional until a specific need brings you to it.

Add these when you need them

Not part of a first integration. Each one answers a question you have not asked yet.

Refunds and corrections
POST /api/v1/sales/{sale}/credit-note reverses a sale you already filed — send the lines coming back and a ZRA reason code (01–07), which is required. POST /api/v1/sales/{sale}/debit-note charges more against one, for an omitted item or a wrong quantity; its reason codes are ZRA's separate list of four (01–04). Raise either on the branch that filed the original.
Stop polling pending sales
Register a webhook and we tell you when a queued sale reaches ZRA. Registering one needs a partner key — with only a merchant token, poll GET /api/v1/sales/{sale} for the pending → uploaded transition, or ask whoever provisioned you to register a receiver. Signing and retries →
Recover a lost response
Send Idempotency-Key on every sale, then GET /api/v1/sales?reference=<that same key> finds it again after a timeout — instead of filing it twice. An unmatched reference is an empty list, not a 404.
Map your whole product list
GET /api/v1/item-classes?search= looks codes up one at a time; the catalogue download is the right tool for a bulk mapping.
More than one branch
Send X-Branch: 002 to file against a specific branch device. Omitted, the token's own branch is used.

Other ways in

Open one only if it describes you.

I am a POS or ERP vendor onboarding many merchants

You hold a partner key and provision merchants with it. The partner key itself never fiscalises — step 5 is the one that lets a merchant trade.

  1. 01

    Get a partner key — see “Get access” below.

  2. 02

    Create the tenant — POST /api/v1/tenants with the merchant's name and TPIN.

  3. 03

    Register the branch device — POST /api/v1/tenants/{tenant}/devices.

  4. 04

    Initialise it against ZRA — POST …/devices/{branch}/initialize. This is one-shot per device: coordinate serials with ZRA first, because a burnt device cannot be re-initialised.

  5. 05

    Mint the merchant's token — POST /api/v1/tenants/{tenant}/tokens. Skip this and the merchant is fully provisioned and still cannot file an invoice.

  6. 06

    Fiscalise with that token — the quickstart above — and register a webhook so you learn about offline sales that upload later.

Scopes, rotation and IP allowlists for the key itself: the partner keys guide →

I am migrating from a legacy VSDC gateway

Same field names, different responses. A till on the modern /zm/api dialect keeps its request payload and its two auth headers. Replies come back bare though — there is no {success, message, data} wrapper, so data.signature becomes signature.

POST /zm/api/Invoice Compatibility surface
curl -X POST https://gateway.mikenyambe.com/zm/api/Invoice \
  -H "X-Api-Key: <your branch key>" \
  -H "X-SdcId: <your SDC id>" \
  -H "Content-Type: application/json" \
  -d '{
    "InvoiceNumber": "INV-1042",
    "IssuerName": "Cashier 1",
    "IssuerId": "104",
    "ReceiptTypeCode": "S",
    "PaymentTypeCode": "01",
    "currencyType": "ZMW",
    "invoiceItems": [{
      "ItemDesc": "Maize meal 25kg",
      "itemCode": "MM-25",
      "itemClassificationCode": "50221204",
      "TaxCodes": ["A"],
      "Quantity": 2,
      "UnitPrice": 350.00,
      "isTaxInclusive": true
    }]
  }'

ZMW only, ReceiptTypeCode: S only. The branch key that opens this surface comes from POST /api/v1/tenants/{tenant}/api-keys with a branch.

Also changing: discount fields and localPurchaseOrder are ignored rather than refused, tax labels outside A–E and RVAT are rejected, and Purchase, Stock and Import have no endpoint here. Full migration guide

Which credential do I need?

Three exist. They are not interchangeable, and each one opens exactly one surface.

Credential Send as Opens How you get one
Tenant token Authorization: Bearer 12|… /api/v1/sales, /me, /item-classes This is the one that fiscalises. Merchant: Developers → API tokens. Partner, on a merchant's behalf: POST /api/v1/tenants/{tenant}/tokens.
Partner key Authorization: Bearer sk_live_… Provisioning: tenants, devices, keys, webhooks Scoped — each endpoint declares what it needs. Request one from support (below). Once you hold one, mint further least-privilege keys yourself.
Branch key X-Api-Key + X-SdcId /zm/api/… only One key per branch device. POST /api/v1/tenants/{tenant}/api-keys with a branch.

Warning

A branch key will not fiscalise on /api/v1

The keys minted by /api-keys authenticate the /zm/api surface only, and answer 401 against POST /api/v1/sales, which takes a tenant token instead. Both forms of key work on /zm/api: minted with a branch it is bound to that one device, and minted without it files through whichever branch the till sends as X-SdcId — so sdcId comes back null and the till reads its options from GET /zm/api/Branches.

Guides

The parts that are a sequence or a warning rather than a schema.

Get access

Merchants can self-serve. Vendors integrating many merchants need a partner key from us.

Merchants: start a free trial, then mint a token under Developers → API tokens. Nothing to request.

POS/ERP vendors: email support@zragateway.zm with your company name, the software you are integrating, and roughly how many merchants you expect to onboard. We will issue a partner key scoped to what you need.

Warning

“Sandbox” keys file real invoices

A key named sk_test_ is a naming and privilege tier, not a separate environment. It fiscalises against real ZRA infrastructure using the tenant's real TPIN. There is no throwaway sandbox — test against a tenant you control, and coordinate device initialisation with ZRA before going live.

Full API reference

Every endpoint with complete request and response schemas, field descriptions and examples.

Import either document into Postman, Insomnia or your client generator.