{
    "openapi": "3.1.0",
    "info": {
        "title": "ZRA Gateway API",
        "version": "1.10.0",
        "description": "ZRA Smart Invoice fiscalisation for Zambia. One call fiscalises a sale: `POST /api/v1/sales`. Everything else in this document is optional.\n\nEvery path is absolute, so the prefix tells you which surface you are on — `/api/v1/…` is the modern JSON surface (tenant bearer token to fiscalise, partner secret key to provision), `/zm/api/…` is the header-authenticated compatibility surface for tills already wired to a legacy VSDC gateway — request field names match, responses and limits differ, and the exact deltas are in the migration guide at `/docs/migrating-a-till`. All responses are JSON. A blocked subscription or exhausted quota returns **402** on every surface.\n\nThe sequences and warnings that are not schemas — webhook signature verification, partner key management, the ZRA result-code table and the changelog — live at `/docs`.\n",
        "contact": {
            "name": "ZRA Gateway support",
            "email": "support@zragateway.zm"
        },
        "license": {
            "name": "Proprietary",
            "url": "https://gateway.mikenyambe.com/terms"
        }
    },
    "externalDocs": {
        "description": "Developer guides — onboarding, webhooks, partner keys, ZRA result codes",
        "url": "https://gateway.mikenyambe.com/docs"
    },
    "servers": [
        {
            "url": "https://gateway.mikenyambe.com",
            "description": "Production"
        }
    ],
    "security": [
        {
            "PartnerKey": []
        }
    ],
    "webhooks": {
        "invoice.fiscalised": {
            "post": {
                "summary": "An invoice reached ZRA",
                "description": "Fired when a document is accepted by ZRA — including a document that was queued offline and has just uploaded, which is the case that makes this webhook the alternative to polling. `data.invoice.status` is `uploaded` and the `fiscal` block is complete.\n",
                "parameters": [
                    {
                        "name": "X-SmartInvoicing-Signature",
                        "in": "header",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "`t=<unix>,v1=<hex>` where `v1 = hex(HMAC_SHA256(secret, t + \".\" + raw_body))`. Verify before trusting the payload."
                    },
                    {
                        "name": "X-Webhook-Event",
                        "in": "header",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "enum": [
                                "invoice.fiscalised",
                                "invoice.failed"
                            ]
                        }
                    },
                    {
                        "name": "X-Webhook-Id",
                        "in": "header",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "The event ULID, equal to the payload `id`. Delivery is at-least-once — dedupe on this."
                    },
                    {
                        "name": "X-Webhook-Delivery",
                        "in": "header",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "This attempt's id, usable with the delivery log and resend endpoints."
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/WebhookEvent"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Return any 2xx within 10 seconds — acknowledge first, process after. A non-2xx, a timeout, or a redirect counts as a failure and is retried (60s, 5m, 30m, 2h, 6h; 6 attempts). Ten consecutive exhausted deliveries disable the endpoint until it is re-enabled via `POST /api/v1/webhooks/{id}/enable`; events fired while disabled are not queued.\n"
                    }
                }
            }
        },
        "invoice.failed": {
            "post": {
                "summary": "ZRA rejected an invoice",
                "description": "Fired when ZRA refuses a document — typically one that was queued offline and was rejected on upload, which no synchronous response could have told you about. `data.invoice.status` is `failed`, `data.invoice.resultCode` carries ZRA's code (see `/docs/result-codes`) and `lastError` the reason; the `fiscal` fields are null.\n",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/WebhookEvent"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Same delivery contract as `invoice.fiscalised`."
                    }
                }
            }
        }
    },
    "tags": [
        {
            "name": "Sales",
            "description": "The one endpoint that fiscalises, plus reading and crediting what you have filed.\n\n**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.\n"
        },
        {
            "name": "Account",
            "description": "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.\n"
        },
        {
            "name": "Webhook endpoints",
            "description": "Register and debug the endpoints we deliver to. The events themselves — the payloads you implement — are under **Webhooks** at the end of this document.\n"
        },
        {
            "name": "Catalogue",
            "description": "ZRA item classification codes. For bulk mapping download `/docs/zra-item-classes.csv.gz` — one credential-free request instead of roughly 500 paged ones.\n\nSince **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.\n"
        },
        {
            "name": "Customers",
            "description": "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.\n\nA customer with no TPIN is a local record and is never sent to ZRA — that is the ordinary walk-in, not a degraded case.\n"
        },
        {
            "name": "Items",
            "description": "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.\n\n**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.\n"
        },
        {
            "name": "Imports",
            "description": "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.\n\n**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.\n\n**`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.\n"
        },
        {
            "name": "Zambia (compat)",
            "description": "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.\n\nRequest 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.\n\n**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.\n\nThe 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.\n"
        },
        {
            "name": "Tenants",
            "description": "Partner-provisioned merchants. Only needed if you fiscalise for businesses other than your own."
        },
        {
            "name": "Devices",
            "description": "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.\n"
        },
        {
            "name": "Merchant tokens",
            "description": "The credential that actually fiscalises. Mint one per tenant once that tenant branch device is initialised.\n"
        },
        {
            "name": "Branch keys",
            "description": "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`.\n"
        },
        {
            "name": "Partner Keys",
            "description": "A partner self-service management of its own secret keys"
        }
    ],
    "x-tagGroups": [
        {
            "name": "Fiscalise a sale",
            "tags": [
                "Sales",
                "Account",
                "Customers",
                "Items",
                "Imports",
                "Webhook endpoints",
                "Catalogue"
            ]
        },
        {
            "name": "Move an existing till",
            "tags": [
                "Zambia (compat)"
            ]
        },
        {
            "name": "Provision merchants (partners)",
            "tags": [
                "Tenants",
                "Devices",
                "Merchant tokens",
                "Branch keys",
                "Partner Keys"
            ]
        }
    ],
    "paths": {
        "/api/v1/tenants": {
            "get": {
                "operationId": "listTenants",
                "description": "Every merchant provisioned under this partner, paginated at 20. A tenant created through the dashboard by the merchant themselves is not listed here — this is the partner's own book.\n",
                "tags": [
                    "Tenants"
                ],
                "summary": "List the partner's tenants",
                "security": [
                    {
                        "PartnerKey": [
                            "tenants:read"
                        ]
                    }
                ],
                "parameters": [
                    {
                        "$ref": "#/components/parameters/Page"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A page of tenants (20 per page)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/TenantCollection"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            },
            "post": {
                "operationId": "createTenant",
                "description": "Creates the merchant record only. It cannot fiscalise yet: register a device, initialise it against ZRA, and mint a tenant token — in that order — before the first sale.\n",
                "tags": [
                    "Tenants"
                ],
                "summary": "Provision a new merchant tenant",
                "security": [
                    {
                        "PartnerKey": [
                            "tenants:write"
                        ]
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/NewTenant"
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "Tenant created",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/TenantEnvelope"
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationError"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/tenants/{tenant}": {
            "parameters": [
                {
                    "name": "tenant",
                    "in": "path",
                    "required": true,
                    "schema": {
                        "type": "integer"
                    },
                    "description": "The tenant id from the create or list response."
                }
            ],
            "get": {
                "operationId": "getTenant",
                "description": "One tenant, with its devices — the quickest way to see how far through provisioning a merchant is and which branches are initialised.\n",
                "tags": [
                    "Tenants"
                ],
                "summary": "Retrieve one tenant (with its devices)",
                "security": [
                    {
                        "PartnerKey": [
                            "tenants:read"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The tenant",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/TenantEnvelope"
                                }
                            }
                        }
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/tenants/{tenant}/devices": {
            "parameters": [
                {
                    "name": "tenant",
                    "in": "path",
                    "required": true,
                    "schema": {
                        "type": "integer"
                    },
                    "description": "The tenant id from the create or list response."
                }
            ],
            "post": {
                "operationId": "registerDevice",
                "description": "Records the branch device locally. Nothing is sent to ZRA yet — that is the initialize step — so this is safe to call and correct freely.\n",
                "tags": [
                    "Devices"
                ],
                "summary": "Register a branch device (local only; not yet initialized)",
                "security": [
                    {
                        "PartnerKey": [
                            "devices:write"
                        ]
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/NewDevice"
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "Device registered",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/DeviceEnvelope"
                                }
                            }
                        }
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "422": {
                        "description": "**Two shapes.** A field rule returns Laravel's `{message, errors}`; re-registering a `branch` this tenant already has returns `{error: {code: branch_exists}}` instead. There is no update endpoint, so `branch_exists` is not a no-op — the existing device keeps the serial and `vsdcBaseUrl` it was registered with.\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "oneOf": [
                                        {
                                            "$ref": "#/components/schemas/ValidationErrorBody"
                                        },
                                        {
                                            "$ref": "#/components/schemas/Error"
                                        }
                                    ]
                                },
                                "examples": {
                                    "fieldValidation": {
                                        "summary": "A rule refused the payload",
                                        "value": {
                                            "message": "The device serial field is required.",
                                            "errors": {
                                                "deviceSerial": [
                                                    "The device serial field is required."
                                                ]
                                            }
                                        }
                                    },
                                    "branchExists": {
                                        "summary": "This tenant already has that branch",
                                        "value": {
                                            "error": {
                                                "code": "branch_exists",
                                                "message": "Branch 000 is already registered for this tenant."
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/tenants/{tenant}/devices/{branch}/initialize": {
            "parameters": [
                {
                    "name": "tenant",
                    "in": "path",
                    "required": true,
                    "schema": {
                        "type": "integer"
                    },
                    "description": "The tenant id from the create or list response."
                },
                {
                    "name": "branch",
                    "in": "path",
                    "required": true,
                    "schema": {
                        "type": "string",
                        "example": "000"
                    },
                    "description": "The bhfId of the device being initialised."
                }
            ],
            "post": {
                "operationId": "initializeDevice",
                "description": "Performs the handshake with ZRA and captures the branch's SDC id and invoice sequences.\n\n**Calling it again on a branch you have already initialised is safe.** ZRA answers \"device already installed\", and we return the branch we already hold, unchanged — so this is the right call to retry after a timeout, and the right call to make when you are unsure whether an earlier attempt landed.\n\nThe hazard is narrower than \"don't call twice\", and it is about the *serial*, not the call: **ZRA returns the initialisation payload once, for the first branch that claims a serial.** If that serial is already installed at ZRA and this gateway holds no record of it — it was initialised elsewhere, or registered here under a different tenant or branch — then nothing can fetch that payload again, and this endpoint answers 422 `initialization_failed` forever. Recovery is manual, from the original first-init response, so give every branch its own serial and initialise it here first.\n",
                "tags": [
                    "Devices"
                ],
                "summary": "Initialise a branch with ZRA — one-shot, irreversible",
                "security": [
                    {
                        "PartnerKey": [
                            "devices:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Device initialized (SDC id + sequence captured)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/DeviceEnvelope"
                                }
                            }
                        }
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "422": {
                        "description": "The handshake reached ZRA and failed. **This endpoint takes no request body**, so this is never a field-validation error and never carries an `errors` map — it is always `{error: {code: initialization_failed, message}}`, where `message` is ZRA's own text.\n\nThe usual cause is a serial ZRA has already installed under a different branch or taxpayer. That is not retryable: pick it apart with ZRA before calling again. Contrast **503**, which means the VSDC was unreachable and the call is safe to repeat.\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "example": {
                                    "error": {
                                        "code": "initialization_failed",
                                        "message": "Device initialization failed [902]: Device already installed"
                                    }
                                }
                            }
                        }
                    },
                    "503": {
                        "$ref": "#/components/responses/VsdcUnreachable"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/tenants/{tenant}/api-keys": {
            "parameters": [
                {
                    "name": "tenant",
                    "in": "path",
                    "required": true,
                    "schema": {
                        "type": "integer"
                    },
                    "description": "The tenant id from the create or list response."
                }
            ],
            "get": {
                "operationId": "listApiKeys",
                "description": "The tenant's fiscalisation keys, without their plaintext. This surface never returns a fiscalisation key's plaintext; an operator can read one back from the merchant page in the console.\n",
                "tags": [
                    "Branch keys"
                ],
                "summary": "List a tenant's fiscalisation keys (masked)",
                "security": [
                    {
                        "PartnerKey": [
                            "keys:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Keys",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ApiKeyCollection"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            },
            "post": {
                "operationId": "mintApiKey",
                "description": "Mints a `pk_…` fiscalisation key for the `/zm/api` surface. Both forms are usable credentials, and the choice is how tightly the key is bound: **with `branch`** it is a branch key that can only ever file through that one device, and its `X-SdcId` comes back in `sdcId`; **without `branch`** it is a tenant key that files through whichever of this tenant's branches the till sends as `X-SdcId`, so `sdcId` comes back null and the till reads the ids it may use from `GET /zm/api/Branches`. Prefer the tenant key when a POS setup screen expects to be given a URL and a key and to choose its own branch; prefer a branch key for a till that will never move. Neither opens `/api/v1/sales` — that takes a tenant token (`POST …/tokens`). Tenant keys authenticated nowhere before 1.9.0; see the changelog.\n",
                "tags": [
                    "Branch keys"
                ],
                "summary": "Mint a fiscalisation key (plaintext returned once)",
                "security": [
                    {
                        "PartnerKey": [
                            "keys:write"
                        ]
                    }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/NewApiKey"
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "Key minted",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/MintedApiKey"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Either the tenant is not yours, or — the case worth handling separately — `branch` names a branch this tenant has not registered. The second answers `{error: {code: device_not_found}}`, so a provisioning script can tell \"register the device first\" apart from \"wrong tenant id\" without guessing.\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "oneOf": [
                                        {
                                            "$ref": "#/components/schemas/Error"
                                        },
                                        {
                                            "$ref": "#/components/schemas/FrameworkMessage"
                                        }
                                    ]
                                },
                                "examples": {
                                    "deviceNotFound": {
                                        "summary": "The branch is not registered under this tenant",
                                        "value": {
                                            "error": {
                                                "code": "device_not_found",
                                                "message": "Branch 007 is not registered for this tenant."
                                            }
                                        }
                                    },
                                    "tenantNotYours": {
                                        "summary": "The tenant is not on this partner's book",
                                        "value": {
                                            "message": "No query results for model [App\\Models\\Tenant] 91."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationError"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/tenants/{tenant}/api-keys/{apiKey}": {
            "parameters": [
                {
                    "name": "tenant",
                    "in": "path",
                    "required": true,
                    "schema": {
                        "type": "integer"
                    },
                    "description": "The tenant id from the create or list response."
                },
                {
                    "name": "apiKey",
                    "in": "path",
                    "required": true,
                    "schema": {
                        "type": "integer"
                    },
                    "description": "The key id from the list or mint response — never the key itself."
                }
            ],
            "delete": {
                "operationId": "revokeApiKey",
                "description": "Immediate. The till holding this key stops fiscalising on its next request.\n",
                "tags": [
                    "Branch keys"
                ],
                "summary": "Revoke a fiscalisation key",
                "security": [
                    {
                        "PartnerKey": [
                            "keys:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Revoked",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "revoked": {
                                            "type": "boolean",
                                            "example": true
                                        },
                                        "id": {
                                            "type": "integer"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/tenants/{tenant}/tokens": {
            "parameters": [
                {
                    "name": "tenant",
                    "in": "path",
                    "required": true,
                    "schema": {
                        "type": "integer"
                    },
                    "description": "The tenant id from the create or list response."
                }
            ],
            "get": {
                "operationId": "listTenantTokens",
                "tags": [
                    "Merchant tokens"
                ],
                "summary": "List a tenant's merchant bearer tokens",
                "description": "Without their plaintext. This surface never returns a token's plaintext; the merchant can view it again in their own developers area, behind their password.\n",
                "security": [
                    {
                        "PartnerKey": [
                            "keys:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The tenant's live tokens",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/TenantTokenCollection"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            },
            "post": {
                "operationId": "mintTenantToken",
                "tags": [
                    "Merchant tokens"
                ],
                "summary": "Mint the merchant token that fiscalises",
                "description": "**This is the credential that fiscalises.** It is a different thing from the fiscalisation keys minted by `/api-keys`, and the distinction decides whether a provisioned merchant can actually trade:\n\n* `POST …/api-keys` mints a `pk_…` key. That key opens the `/zm/api`\n  compatibility surface, paired with `X-SdcId`. It is **not** accepted by\n  `/api/v1/sales` and will answer 401 there.\n* `POST …/tokens` — this endpoint — mints a bearer token for\n  `/api/v1/sales` and the rest of the merchant surface.\n\nWithout this call a partner can create a tenant, register its device and initialise it against ZRA, and still have handed the merchant nothing that can file an invoice on the canonical surface.\n\nThe token is granted `invoices:read` and `invoices:write` — exactly what the merchant's own developer UI grants, so provisioning on their behalf is a convenience and not a privilege escalation.\n",
                "security": [
                    {
                        "PartnerKey": [
                            "keys:write"
                        ]
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "name"
                                ],
                                "properties": {
                                    "name": {
                                        "type": "string",
                                        "maxLength": 60,
                                        "description": "How this token will be identified in listings. Name it after the till or system that will hold it.",
                                        "example": "Katema Stores — POS terminal 1"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "Token minted. **The plaintext is in this response and nowhere else, ever again** — store it before you discard the response.\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/CreatedTenantToken"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationError"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/tenants/{tenant}/tokens/{token}": {
            "parameters": [
                {
                    "name": "tenant",
                    "in": "path",
                    "required": true,
                    "schema": {
                        "type": "integer"
                    },
                    "description": "The tenant id from the create or list response."
                },
                {
                    "name": "token",
                    "in": "path",
                    "required": true,
                    "schema": {
                        "type": "integer"
                    },
                    "description": "The token id from the list or mint response — not the plaintext."
                }
            ],
            "delete": {
                "operationId": "revokeTenantToken",
                "tags": [
                    "Merchant tokens"
                ],
                "summary": "Revoke a merchant bearer token",
                "description": "Immediate. A token id belonging to another merchant matches nothing and answers 404 rather than revoking somebody else's credential.\n",
                "security": [
                    {
                        "PartnerKey": [
                            "keys:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Revoked",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "revoked": {
                                            "type": "boolean",
                                            "example": true
                                        },
                                        "id": {
                                            "type": "integer"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/me": {
            "get": {
                "operationId": "getMe",
                "tags": [
                    "Account"
                ],
                "summary": "Check this token can file, and on which branches",
                "description": "Identifies the tenant behind the bearer token and lists its devices. The cheapest way to confirm a token works and to discover the legal values for the `X-Branch` header — `initialized: false` means that branch has not been initialised against ZRA and cannot fiscalise yet.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "$ref": "#/components/parameters/XBranch"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The authenticated tenant and its devices",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Me"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/sales": {
            "get": {
                "operationId": "listSales",
                "tags": [
                    "Sales"
                ],
                "summary": "Find a sale again by your own reference",
                "description": "Paginated at 20 per page.\n\n**Recovering a lost response.** If a fiscalisation response never reached you, you still know the reference *you* chose — pass it as `?reference=` to find the document without the gateway id. A reference that matches nothing returns an empty list rather than a 404, so an unknown reference and a broken URL stay distinguishable.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "reference",
                        "in": "query",
                        "schema": {
                            "type": "string"
                        },
                        "description": "Your own reference for the document — the `clientInvoiceNo` you sent. Matched against both columns a caller-chosen reference can land in.\n",
                        "example": "INV-1042"
                    },
                    {
                        "$ref": "#/components/parameters/Page"
                    },
                    {
                        "$ref": "#/components/parameters/XBranch"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A page of sales (20 per page)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/SaleCollection"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            },
            "post": {
                "operationId": "fiscaliseSale",
                "tags": [
                    "Sales"
                ],
                "summary": "Fiscalise a sale",
                "description": "**Offline tolerant — a 2xx does not mean ZRA has accepted the sale.** When the VSDC or ZRA is unreachable the sale is still accepted, given an invoice number and queued; it is uploaded in invoice order once connectivity returns. In that case the response carries `status: pending` and every `fiscal.*` field is `null`.\n\nCheck `status` before printing. Only `uploaded` may be printed as a fiscal receipt; a `pending` document must be marked provisional and reprinted, or completed, once it uploads. Subscribe to the `invoice.fiscalised` webhook rather than polling for that transition.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "Idempotency-Key",
                        "in": "header",
                        "schema": {
                            "type": "string"
                        },
                        "description": "Retry-safe key; a repeat returns the original invoice."
                    },
                    {
                        "$ref": "#/components/parameters/XBranch"
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/NewSale"
                            },
                            "examples": {
                                "minimum": {
                                    "summary": "The whole integration — four fields per line",
                                    "description": "Everything else has a default: customer becomes \"Walk-in Customer\", paymentMethod 01 (cash), vatCategory A (standard 16%), taxInclusive true, occurredAt now.\n",
                                    "value": {
                                        "items": [
                                            {
                                                "name": "Maize meal 25kg",
                                                "classificationCode": "50221204",
                                                "quantity": 2,
                                                "price": 350
                                            }
                                        ]
                                    }
                                },
                                "typical": {
                                    "summary": "A named customer paying by mobile money",
                                    "description": "What a real till usually sends once it is past the first invoice.",
                                    "value": {
                                        "customer": {
                                            "name": "Katema Stores Ltd",
                                            "tpin": "2002200000"
                                        },
                                        "paymentMethod": "06",
                                        "items": [
                                            {
                                                "name": "Maize meal 25kg",
                                                "classificationCode": "50221204",
                                                "vatCategory": "A",
                                                "quantity": 2,
                                                "price": 350
                                            }
                                        ]
                                    }
                                },
                                "export": {
                                    "summary": "A zero-rated export",
                                    "description": "Category C1 requires a destination country and may carry no other VAT category on the same invoice.\n",
                                    "value": {
                                        "customer": {
                                            "name": "Katanga Mining Supplies SARL"
                                        },
                                        "destinationCountryCode": "CD",
                                        "items": [
                                            {
                                                "name": "Copper cathode 99.99%",
                                                "classificationCode": "50221204",
                                                "vatCategory": "C1",
                                                "quantity": 5,
                                                "price": 18500
                                            }
                                        ]
                                    }
                                }
                            }
                        }
                    }
                },
                "x-codeSamples": [
                    {
                        "lang": "shell",
                        "label": "curl",
                        "source": "curl -X POST https://gateway.mikenyambe.com/api/v1/sales \\\n  -H \"Authorization: Bearer $TENANT_TOKEN\" \\\n  -H \"Idempotency-Key: INV-1042\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"customer\": { \"name\": \"Katema Stores Ltd\", \"tpin\": \"2002200000\" },\n    \"paymentMethod\": \"06\",\n    \"items\": [{\n      \"name\": \"Maize meal 25kg\",\n      \"classificationCode\": \"50221204\",\n      \"vatCategory\": \"A\",\n      \"quantity\": 2,\n      \"price\": 350.00\n    }]\n  }'\n"
                    },
                    {
                        "lang": "php",
                        "label": "PHP (Guzzle)",
                        "source": "$client = new \\GuzzleHttp\\Client(['base_uri' => 'https://gateway.mikenyambe.com']);\n\n$response = $client->post('/api/v1/sales', [\n    'headers' => [\n        'Authorization' => 'Bearer '.$tenantToken,\n        'Idempotency-Key' => 'INV-1042',\n    ],\n    'json' => [\n        'customer' => ['name' => 'Katema Stores Ltd', 'tpin' => '2002200000'],\n        'paymentMethod' => '06', // mobile money\n        'items' => [[\n            'name' => 'Maize meal 25kg',\n            'classificationCode' => '50221204',\n            'vatCategory' => 'A',\n            'quantity' => 2,\n            'price' => 350.00,\n        ]],\n    ],\n]);\n\n$sale = json_decode((string) $response->getBody(), true)['data'];\n\n// A 201 is not ZRA's acceptance. Only print a fiscal receipt when\n// the document has actually uploaded; 'pending' means queued\n// offline — print it PROVISIONAL and finish it on the\n// invoice.fiscalised webhook.\nif ($sale['status'] === 'uploaded') {\n    printReceipt($sale['fiscal']); // signature, qrCodeUrl, sdcId, mrcNo\n} else {\n    printProvisionalReceipt($sale);\n}\n"
                    },
                    {
                        "lang": "javascript",
                        "label": "Node",
                        "source": "const res = await fetch('https://gateway.mikenyambe.com/api/v1/sales', {\n  method: 'POST',\n  headers: {\n    'Authorization': `Bearer ${tenantToken}`,\n    'Idempotency-Key': 'INV-1042',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    customer: { name: 'Katema Stores Ltd', tpin: '2002200000' },\n    paymentMethod: '06', // mobile money\n    items: [{\n      name: 'Maize meal 25kg',\n      classificationCode: '50221204',\n      vatCategory: 'A',\n      quantity: 2,\n      price: 350.00,\n    }],\n  }),\n});\n\nconst { data: sale } = await res.json();\n\n// A 201 is not ZRA's acceptance — check status before printing.\nif (sale.status === 'uploaded') {\n  printReceipt(sale.fiscal); // signature, qrCodeUrl, sdcId, mrcNo\n} else {\n  printProvisionalReceipt(sale); // queued offline; finish it on the webhook\n}\n"
                    },
                    {
                        "lang": "python",
                        "label": "Python",
                        "source": "import requests\n\nres = requests.post(\n    'https://gateway.mikenyambe.com/api/v1/sales',\n    headers={\n        'Authorization': f'Bearer {tenant_token}',\n        'Idempotency-Key': 'INV-1042',\n    },\n    json={\n        'customer': {'name': 'Katema Stores Ltd', 'tpin': '2002200000'},\n        'paymentMethod': '06',  # mobile money\n        'items': [{\n            'name': 'Maize meal 25kg',\n            'classificationCode': '50221204',\n            'vatCategory': 'A',\n            'quantity': 2,\n            'price': 350.00,\n        }],\n    },\n)\n\nsale = res.json()['data']\n\n# A 201 is not ZRA's acceptance — check status before printing.\nif sale['status'] == 'uploaded':\n    print_receipt(sale['fiscal'])  # signature, qrCodeUrl, sdcId, mrcNo\nelse:\n    print_provisional_receipt(sale)  # queued offline; finish on webhook\n"
                    }
                ],
                "responses": {
                    "201": {
                        "description": "Accepted for fiscalisation. `status` is `uploaded` when ZRA has already taken it, or `pending` when it was queued offline. Both bodies are shown below — a till has to handle each.\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/SaleEnvelope"
                                },
                                "examples": {
                                    "uploaded": {
                                        "summary": "ZRA has it — printable as a fiscal receipt",
                                        "value": {
                                            "data": {
                                                "id": 4821,
                                                "status": "uploaded",
                                                "invoiceNo": 424,
                                                "receiptNo": 424,
                                                "reference": "424",
                                                "totals": {
                                                    "taxable": 603.45,
                                                    "vat": 96.55,
                                                    "total": 700
                                                },
                                                "taxBreakdown": {
                                                    "A": {
                                                        "taxbl": 603.45,
                                                        "tax": 96.55,
                                                        "rate": 16
                                                    },
                                                    "B": {
                                                        "taxbl": 0,
                                                        "tax": 0,
                                                        "rate": 0
                                                    },
                                                    "C1": {
                                                        "taxbl": 0,
                                                        "tax": 0,
                                                        "rate": 0
                                                    },
                                                    "C2": {
                                                        "taxbl": 0,
                                                        "tax": 0,
                                                        "rate": 0
                                                    },
                                                    "C3": {
                                                        "taxbl": 0,
                                                        "tax": 0,
                                                        "rate": 0
                                                    },
                                                    "D": {
                                                        "taxbl": 0,
                                                        "tax": 0,
                                                        "rate": 0
                                                    },
                                                    "RVAT": {
                                                        "taxbl": 0,
                                                        "tax": 0,
                                                        "rate": 0
                                                    },
                                                    "E": {
                                                        "taxbl": 0,
                                                        "tax": 0,
                                                        "rate": 0
                                                    }
                                                },
                                                "fiscal": {
                                                    "sdcId": "SDC0010000941",
                                                    "mrcNo": "MRC0010000941",
                                                    "receiptSignature": "WVRE-M3BF-QPXT",
                                                    "internalData": "3PGT-XKQZ-7YHD-2WMR",
                                                    "vsdcDate": "20260901101422",
                                                    "qrCodeUrl": "https://gateway.mikenyambe.com/verify/424"
                                                }
                                            }
                                        }
                                    },
                                    "pending": {
                                        "summary": "Queued offline — mark it PROVISIONAL, do not print as fiscal",
                                        "description": "Every `fiscal.*` field is null until the queue drains. The document already has its invoice number and will upload in invoice order; subscribe to `invoice.fiscalised` rather than polling.\n",
                                        "value": {
                                            "data": {
                                                "id": 4822,
                                                "status": "pending",
                                                "invoiceNo": 425,
                                                "receiptNo": null,
                                                "reference": "425",
                                                "totals": {
                                                    "taxable": 603.45,
                                                    "vat": 96.55,
                                                    "total": 700
                                                },
                                                "taxBreakdown": {
                                                    "A": {
                                                        "taxbl": 603.45,
                                                        "tax": 96.55,
                                                        "rate": 16
                                                    },
                                                    "B": {
                                                        "taxbl": 0,
                                                        "tax": 0,
                                                        "rate": 0
                                                    },
                                                    "C1": {
                                                        "taxbl": 0,
                                                        "tax": 0,
                                                        "rate": 0
                                                    },
                                                    "C2": {
                                                        "taxbl": 0,
                                                        "tax": 0,
                                                        "rate": 0
                                                    },
                                                    "C3": {
                                                        "taxbl": 0,
                                                        "tax": 0,
                                                        "rate": 0
                                                    },
                                                    "D": {
                                                        "taxbl": 0,
                                                        "tax": 0,
                                                        "rate": 0
                                                    },
                                                    "RVAT": {
                                                        "taxbl": 0,
                                                        "tax": 0,
                                                        "rate": 0
                                                    },
                                                    "E": {
                                                        "taxbl": 0,
                                                        "tax": 0,
                                                        "rate": 0
                                                    }
                                                },
                                                "fiscal": {
                                                    "sdcId": null,
                                                    "mrcNo": null,
                                                    "receiptSignature": null,
                                                    "internalData": null,
                                                    "vsdcDate": null,
                                                    "qrCodeUrl": null
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "200": {
                        "description": "Idempotent replay — the invoice this key already raised, unchanged.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/SaleEnvelope"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "402": {
                        "$ref": "#/components/responses/PaymentRequired"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "422": {
                        "description": "Two distinct failures share this status. Field validation returns the `errors` map; a document the engine or ZRA refused returns the flat shape, with `resultCode` present when the verdict is ZRA's — see `/docs/result-codes` for what to do with each code.\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "oneOf": [
                                        {
                                            "$ref": "#/components/schemas/ValidationErrorBody"
                                        },
                                        {
                                            "$ref": "#/components/schemas/FiscalisationError"
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/sales/{sale}": {
            "get": {
                "operationId": "getSale",
                "tags": [
                    "Sales"
                ],
                "summary": "Check whether a sale reached ZRA",
                "description": "By the gateway id returned when it was fiscalised. If you only have your own reference, use `GET /api/v1/sales?reference=…` instead.\n\nPoll this to watch a `pending` document become `uploaded`, or subscribe to the `invoice.fiscalised` webhook and avoid polling entirely.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "sale",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "The gateway id of the document."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The sale or credit note",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/SaleEnvelope"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "404": {
                        "description": "No such sale for this tenant"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/sales/{sale}/credit-note": {
            "post": {
                "operationId": "createCreditNote",
                "tags": [
                    "Sales"
                ],
                "summary": "Refund or cancel a fiscalised sale",
                "description": "The fiscal document a refund is. Sent to ZRA as rcptTyCd `R` against the original invoice, so the VAT declared on the sale is given back instead of left standing. A refund that exists only in a point of sale's own database is a sale ZRA still believes happened.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "sale",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Id of the sale being credited. It must be a sale, not itself a credit note."
                    },
                    {
                        "name": "Idempotency-Key",
                        "in": "header",
                        "schema": {
                            "type": "string"
                        },
                        "description": "Retry-safe key; a repeat returns the credit note already raised. It matters more here than on a sale — a duplicated refund gives the money back twice on a VAT return.\n"
                    },
                    {
                        "$ref": "#/components/parameters/XBranch"
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/NewCreditNote"
                            },
                            "example": {
                                "reason": "03",
                                "items": [
                                    {
                                        "name": "Maize meal 25kg",
                                        "classificationCode": "50221204",
                                        "vatCategory": "A",
                                        "quantity": 1,
                                        "price": 350
                                    }
                                ]
                            }
                        }
                    }
                },
                "x-codeSamples": [
                    {
                        "lang": "shell",
                        "label": "curl",
                        "source": "SALE_ID=1042   # the gateway id from the sale's response, or GET /api/v1/sales?reference=...\ncurl -X POST \"https://gateway.mikenyambe.com/api/v1/sales/$SALE_ID/credit-note\" \\\n  -H \"Authorization: Bearer $TENANT_TOKEN\" \\\n  -H \"Idempotency-Key: refund-INV-1042\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"reason\": \"03\",\n    \"items\": [{\n      \"name\": \"Maize meal 25kg\",\n      \"classificationCode\": \"50221204\",\n      \"vatCategory\": \"A\",\n      \"quantity\": 1,\n      \"price\": 350.00\n    }]\n  }'\n"
                    },
                    {
                        "lang": "javascript",
                        "label": "Node",
                        "source": "// Credit one of the two bags on invoice 1042. Partial refunds send\n// fewer lines or smaller quantities; the running total credited may\n// never exceed the original.\nconst res = await fetch(`https://gateway.mikenyambe.com/api/v1/sales/${saleId}/credit-note`, {\n  method: 'POST',\n  headers: {\n    'Authorization': `Bearer ${tenantToken}`,\n    'Idempotency-Key': `refund-${reference}`, // a duplicated refund refunds twice\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    reason: '03', // damaged goods\n    items: [{ name: 'Maize meal 25kg', classificationCode: '50221204', vatCategory: 'A', quantity: 1, price: 350.00 }],\n  }),\n});\n\nconst { data: note } = await res.json();\n// Same offline contract as a sale: only print as fiscal when uploaded.\nconsole.log(note.type, note.status, note.originalInvoiceNo);\n"
                    }
                ],
                "responses": {
                    "201": {
                        "description": "Credit note accepted. `type` is `credit-note` and `originalInvoiceNo` points at the invoice it reverses. Offline tolerant in the same way as a sale — check `status` before printing.\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/SaleEnvelope"
                                }
                            }
                        }
                    },
                    "200": {
                        "description": "Idempotent replay — the credit note this key already raised.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/SaleEnvelope"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "402": {
                        "$ref": "#/components/responses/PaymentRequired"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "404": {
                        "description": "No such sale for this tenant"
                    },
                    "409": {
                        "description": "The document cannot be credited from here yet, for one of two reasons the body's `message` distinguishes: the sale is still queued offline (`pending`) and has no fiscal invoice to credit — try again once it uploads — or it belongs to a different branch than this request resolved to, in which case `error` names the branch to send as `X-Branch`.\n"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/sales/{sale}/debit-note": {
            "post": {
                "operationId": "createDebitNote",
                "tags": [
                    "Sales"
                ],
                "summary": "Charge more against a fiscalised sale",
                "description": "The fiscal document an undercharge is. Sent to ZRA as rcptTyCd `D` against the original invoice, so VAT on the additional value is declared rather than going unreported. Use it for an omitted item, a wrong quantity, or a price billed too low — never to re-issue a whole invoice.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "sale",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Id of the sale being adjusted. It must be a sale, not itself a credit or debit note."
                    },
                    {
                        "name": "Idempotency-Key",
                        "in": "header",
                        "schema": {
                            "type": "string"
                        },
                        "description": "Retry-safe key; a repeat returns the debit note already raised. A duplicated debit note bills the customer twice on a document ZRA has.\n"
                    },
                    {
                        "$ref": "#/components/parameters/XBranch"
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/NewDebitNote"
                            },
                            "example": {
                                "reason": "03",
                                "items": [
                                    {
                                        "name": "Maize meal 25kg",
                                        "classificationCode": "50221204",
                                        "vatCategory": "A",
                                        "quantity": 1,
                                        "price": 350
                                    }
                                ]
                            }
                        }
                    }
                },
                "x-codeSamples": [
                    {
                        "lang": "shell",
                        "label": "curl",
                        "source": "SALE_ID=1042   # the gateway id from the sale's response\ncurl -X POST \"https://gateway.mikenyambe.com/api/v1/sales/$SALE_ID/debit-note\" \\\n  -H \"Authorization: Bearer $TENANT_TOKEN\" \\\n  -H \"Idempotency-Key: debit-INV-1042\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"reason\": \"03\",\n    \"note\": \"Second bag delivered but not invoiced\",\n    \"items\": [{\n      \"name\": \"Maize meal 25kg\",\n      \"classificationCode\": \"50221204\",\n      \"vatCategory\": \"A\",\n      \"quantity\": 1,\n      \"price\": 350.00\n    }]\n  }'\n"
                    }
                ],
                "responses": {
                    "201": {
                        "description": "Debit note accepted. `type` is `debit-note` and `originalInvoiceNo` points at the invoice it adjusts. Offline tolerant in the same way as a sale — check `status` before printing.\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/SaleEnvelope"
                                }
                            }
                        }
                    },
                    "200": {
                        "description": "Idempotent replay — the debit note this key already raised.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/SaleEnvelope"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "402": {
                        "$ref": "#/components/responses/PaymentRequired"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "404": {
                        "description": "No such sale for this tenant"
                    },
                    "409": {
                        "description": "The document cannot be adjusted from here yet: the sale is still queued offline (`pending`) and has no fiscal invoice to reference, or it belongs to a different branch than this request resolved to, in which case `error` names the branch to send as `X-Branch`.\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "message": {
                                            "type": "string",
                                            "example": "That sale belongs to a different branch."
                                        },
                                        "error": {
                                            "type": "string",
                                            "example": "Invoice 180 was fiscalised on branch 001, but this request resolved to branch 000. Send X-Branch: 001 to credit it."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Field validation (the `errors` map), or a refusal by the engine or ZRA (the flat shape, with `resultCode` when the verdict is ZRA's).\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "oneOf": [
                                        {
                                            "$ref": "#/components/schemas/ValidationErrorBody"
                                        },
                                        {
                                            "$ref": "#/components/schemas/FiscalisationError"
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/items": {
            "get": {
                "operationId": "listItems",
                "tags": [
                    "Items"
                ],
                "summary": "The catalogue, or the drafts waiting to be reviewed",
                "description": "This tenant's items, newest first, paginated at 20 per page and listed across every branch — `branch` on each row says which item master ZRA holds it in.\n\n**`?state=draft` is the query that matters operationally.** It returns the rows the gateway created on your behalf and has not sent: unknown item codes on sales it fiscalised anyway, and anything the historic backfill produced. Those are what a merchant has to review.\n\n`?itemCode=` is an exact, whole-value match. `?search=` matches on name and is the human's way in.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "itemCode",
                        "in": "query",
                        "schema": {
                            "type": "string"
                        },
                        "description": "Return only the item with this code. Exact, whole-value match.",
                        "example": "ZM2NTBA0000012"
                    },
                    {
                        "name": "state",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "draft",
                                "pending",
                                "registered",
                                "failed"
                            ]
                        },
                        "description": "Filter by registration state. `draft` is the one to poll for review work.\n"
                    },
                    {
                        "name": "search",
                        "in": "query",
                        "schema": {
                            "type": "string"
                        },
                        "description": "Partial, case-insensitive match on the item name.",
                        "example": "sugar"
                    },
                    {
                        "$ref": "#/components/parameters/Page"
                    },
                    {
                        "$ref": "#/components/parameters/XBranch"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A page of items (20 per page)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ItemCollection"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            },
            "post": {
                "operationId": "createItem",
                "tags": [
                    "Items"
                ],
                "summary": "Add an item, and register it with ZRA",
                "description": "Creates the catalogue row and queues its registration (`/items/saveItem`). **The registration is queued, not awaited**: unlike a sale there is nobody at a till waiting, so a VSDC outage delays it rather than rejecting the item. A 201 means the gateway holds the item, not that ZRA does — watch `registration.state`.\n\n**Send `draft: true` to hold it back.** That is what a bulk import wants: the rows land locally, nothing is sent, and a person releases them once the classifications have been checked. There is no create-and-release in one call, because releasing is the review decision.\n\nOnly three fields are required — `name`, `classificationCode` and `defaultPrice` — even though ZRA demands more. The rest are defaulted (finished product, Zambian origin, unit packaging and quantity) because demanding five ZRA codes to add \"Sugar 1kg\" is how a catalogue stays empty. `classificationCode` is not defaulted and never will be: it is what ZRA taxes the item against.\n\n**`itemCode` is yours if you want it.** Omit it and the gateway generates ZRA's section 6.16 code. Send one and it is honoured, provided it is unique within the branch — if it is not, you get a 409 rather than a silently different code, because a code you did not choose is one you will never look the item up by again.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "$ref": "#/components/parameters/XBranch"
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/ItemRequest"
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "The item as stored. `registration.state` says what happens next.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/Item"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "404": {
                        "description": "This account has no ZRA device for the branch, so it cannot hold items."
                    },
                    "409": {
                        "description": "The `itemCode` you asked for is already registered at this branch. Amend that item with PATCH, or omit `itemCode` to be given one.\n"
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationError"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/items/{item}": {
            "get": {
                "operationId": "getItem",
                "tags": [
                    "Items"
                ],
                "summary": "One item, by id or by ZRA item code",
                "description": "`{item}` accepts either the gateway's own id or the ZRA `itemCode`, so a caller holding the code it sent does not have to store ours.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "item",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "The gateway id, or the ZRA item code."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The item.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/Item"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "404": {
                        "description": "No such item for this tenant."
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            },
            "patch": {
                "operationId": "updateItem",
                "tags": [
                    "Items"
                ],
                "summary": "Amend an item, and tell ZRA",
                "description": "Amends the fields you send and leaves the rest. The item is re-queued — as `/items/updateItem` once ZRA has accepted it once — because a change nobody tells ZRA about leaves the filed master describing a product the merchant no longer sells.\n\n**Amending a draft leaves it a draft.** Editing an unreviewed row is part of reviewing it, not a substitute for releasing it.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "item",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "The gateway id, or the ZRA item code."
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/ItemRequest"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "The amended item.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/Item"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "404": {
                        "description": "No such item for this tenant."
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationError"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/items/{item}/release": {
            "post": {
                "operationId": "releaseItem",
                "tags": [
                    "Items"
                ],
                "summary": "Release a reviewed draft to ZRA",
                "description": "Turns a `draft` into a `pending` registration and queues it. This is the only route from one to the other, and it is deliberately a separate call from PATCH: amending a draft is part of reviewing it, releasing it is the decision at the end.\n\nReleasing an item that is not a draft changes nothing and is not an error — a bulk release over a mixed selection should not fail on the rows that were already sent.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "item",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "The gateway id, or the ZRA item code."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The item, now queued unless it was never a draft.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/Item"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "404": {
                        "description": "No such item for this tenant."
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/customers": {
            "get": {
                "operationId": "listCustomers",
                "tags": [
                    "Customers"
                ],
                "summary": "Find a customer, usually by TPIN",
                "description": "This tenant's customers, newest first, paginated at 20 per page and listed across every branch — `branch` on each row says which branch list ZRA holds it in.\n\n**`?tpin=` is the lookup this endpoint exists for.** Before putting a TPIN on an invoice, ask whether the merchant already holds that customer and what ZRA said about it: `verification.state` answers both. The match is on the whole TPIN, never a prefix — a partial TPIN is not a shorter TPIN, it is a different one. A TPIN that matches nothing returns an empty page rather than a 404, so \"no such customer\" and \"no such URL\" stay distinguishable.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "tpin",
                        "in": "query",
                        "schema": {
                            "type": "string"
                        },
                        "description": "Return only the customer holding this TPIN. Exact, whole-value match.\n",
                        "example": "2002200000"
                    },
                    {
                        "$ref": "#/components/parameters/Page"
                    },
                    {
                        "$ref": "#/components/parameters/XBranch"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A page of customers (20 per page)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/CustomerCollection"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            },
            "post": {
                "operationId": "createCustomer",
                "tags": [
                    "Customers"
                ],
                "summary": "Add a customer, and register it with ZRA",
                "description": "Creates the local record and, when the customer has a TPIN, queues its registration with ZRA (`saveBrancheCustomers`). **The registration is queued, not awaited**: unlike a sale there is nobody at a till waiting on the answer, so a VSDC outage delays the registration rather than rejecting the customer. A 201 therefore means the gateway holds the customer, not that ZRA does — watch `registration.state` for that.\n\n**A customer with no TPIN is never sent to ZRA.** ZRA has nothing to register such a record against, so it is a local record by design — the walk-in a till needs — and it comes back as `registration.state: local-only` rather than as a registration that is merely late. Do not poll one for `registered`.\n\n**`verify: true` checks the TPIN in the same round trip** and answers with a `tpinCheck` object. It is a lookup and nothing else: it never changes whether the customer is created or queued, and a lookup that cannot be made does not fail the request.\n\nThe customer is filed under the branch this request acts through (`X-Branch`, or the tenant default). ZRA keeps customer lists per branch, so the same taxpayer may legitimately exist once under each branch a merchant trades from, and a TPIN already held on **this** branch is a 422 rather than a silent update — the stored name and the name ZRA holds are two different claims, and overwriting the first would erase the disagreement.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "$ref": "#/components/parameters/XBranch"
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/NewCustomer"
                            },
                            "examples": {
                                "walkIn": {
                                    "summary": "A named account with no TPIN — local only",
                                    "description": "Most retail customers. Stored, searchable, printable on an invoice, and never transmitted to ZRA.\n",
                                    "value": {
                                        "name": "J Banda",
                                        "phone": "0977123456"
                                    }
                                },
                                "registered": {
                                    "summary": "A registered taxpayer — queued for ZRA",
                                    "description": "With a TPIN present the record is queued for `saveBrancheCustomers` as soon as it is created, and `customerNo` becomes required because Smart Invoice refuses a registration without it.\n",
                                    "value": {
                                        "name": "Katema Stores Ltd",
                                        "tpin": "2002200000",
                                        "customerNo": "0977123456",
                                        "address": "Plot 24, Cairo Road, Lusaka",
                                        "email": "accounts@katema.example"
                                    }
                                },
                                "verified": {
                                    "summary": "Check the TPIN in the same round trip",
                                    "description": "Adds a synchronous lookup and a `tpinCheck` object to the answer. It does not change what is created or queued.\n",
                                    "value": {
                                        "name": "Katema Stores Ltd",
                                        "tpin": "2002200000",
                                        "customerNo": "0977123456",
                                        "verify": true
                                    }
                                }
                            }
                        }
                    }
                },
                "x-codeSamples": [
                    {
                        "lang": "shell",
                        "label": "curl",
                        "source": "curl -X POST https://gateway.mikenyambe.com/api/v1/customers \\\n  -H \"Authorization: Bearer $TENANT_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"Katema Stores Ltd\",\n    \"tpin\": \"2002200000\",\n    \"customerNo\": \"0977123456\",\n    \"verify\": true\n  }'\n"
                    }
                ],
                "responses": {
                    "201": {
                        "description": "The customer as stored. Registration, if any, is queued.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/CreatedCustomer"
                                },
                                "examples": {
                                    "registered": {
                                        "summary": "Queued for registration with ZRA",
                                        "value": {
                                            "data": {
                                                "id": 91,
                                                "branch": "000",
                                                "name": "Katema Stores Ltd",
                                                "tpin": "2002200000",
                                                "customerNo": "0977123456",
                                                "type": null,
                                                "address": "Plot 24, Cairo Road, Lusaka",
                                                "phone": null,
                                                "email": "accounts@katema.example",
                                                "remark": null,
                                                "active": true,
                                                "verification": {
                                                    "state": "unchecked",
                                                    "resultCode": null,
                                                    "registeredName": null,
                                                    "checkedAt": null
                                                },
                                                "registration": {
                                                    "state": "pending",
                                                    "attempts": 0,
                                                    "lastError": null,
                                                    "registeredAt": null
                                                },
                                                "createdAt": "2026-09-08T09:14:22+00:00"
                                            }
                                        }
                                    },
                                    "walkIn": {
                                        "summary": "No TPIN — a local record, never sent to ZRA",
                                        "value": {
                                            "data": {
                                                "id": 92,
                                                "branch": "000",
                                                "name": "J Banda",
                                                "tpin": null,
                                                "customerNo": null,
                                                "type": null,
                                                "address": null,
                                                "phone": "0977123456",
                                                "email": null,
                                                "remark": null,
                                                "active": true,
                                                "verification": {
                                                    "state": "unchecked",
                                                    "resultCode": null,
                                                    "registeredName": null,
                                                    "checkedAt": null
                                                },
                                                "registration": {
                                                    "state": "local-only",
                                                    "attempts": 0,
                                                    "lastError": null,
                                                    "registeredAt": null
                                                },
                                                "createdAt": "2026-09-08T09:14:22+00:00"
                                            }
                                        }
                                    },
                                    "verified": {
                                        "summary": "Created with verify — Smart Invoice already holds the TPIN",
                                        "description": "`data.verification` is what was stored; `tpinCheck` is what the lookup answered. Note the registered name differs from the name the merchant trades under, which is the disagreement the two fields exist to show.\n",
                                        "value": {
                                            "data": {
                                                "id": 93,
                                                "branch": "000",
                                                "name": "Katema Stores Ltd",
                                                "tpin": "2002200000",
                                                "customerNo": "0977123456",
                                                "type": null,
                                                "address": null,
                                                "phone": null,
                                                "email": null,
                                                "remark": null,
                                                "active": true,
                                                "verification": {
                                                    "state": "known",
                                                    "resultCode": "000",
                                                    "registeredName": "KATEMA STORES LIMITED",
                                                    "checkedAt": "2026-09-08T09:14:22+00:00"
                                                },
                                                "registration": {
                                                    "state": "pending",
                                                    "attempts": 0,
                                                    "lastError": null,
                                                    "registeredAt": null
                                                },
                                                "createdAt": "2026-09-08T09:14:22+00:00"
                                            },
                                            "tpinCheck": {
                                                "status": "known",
                                                "resultCode": "000",
                                                "message": "Smart Invoice holds TPIN 2002200000 as \"KATEMA STORES LIMITED\"."
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "422": {
                        "description": "Two distinct failures share this status. Field validation returns the `errors` map — including a `tpin` already held on this branch. The flat shape means no initialised device resolved for the request, so there is no branch to file the customer under: check `X-Branch` against the branches `GET /api/v1/me` lists.\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "oneOf": [
                                        {
                                            "$ref": "#/components/schemas/ValidationErrorBody"
                                        },
                                        {
                                            "$ref": "#/components/schemas/FiscalisationError"
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/import-items": {
            "get": {
                "operationId": "listImportItems",
                "tags": [
                    "Imports"
                ],
                "summary": "Customs declaration lines addressed to this taxpayer",
                "description": "Newest declaration first. Each row is one line of one declaration — ZRA sends a flat list rather than a header with children, so the declaration's own references travel on every line under `declaration`.\n\n**To poll for work, ask for `status=2`.** That is ZRA's own class-26 code for *Waiting*, the state every pulled line arrives in and the only one that needs a person. The filter takes ZRA's code rather than a word of ours, so it cannot drift from what the gateway stores: `1` Unsent, `2` Waiting, `3` Approved, `4` Cancelled.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "status",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "maxLength": 5,
                            "enum": [
                                "1",
                                "2",
                                "3",
                                "4"
                            ]
                        },
                        "description": "ZRA's import item status code (class 26)."
                    },
                    {
                        "name": "declaration",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "maxLength": 100
                        },
                        "description": "Exact match on the customs declaration number, its reference, or ZRA's task code — whichever a clearing agent quoted.\n"
                    },
                    {
                        "name": "branch",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "maxLength": 10
                        },
                        "description": "Restrict to one branch. Unfiltered, every branch is listed."
                    },
                    {
                        "$ref": "#/components/parameters/Page"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Matching declaration lines",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ImportItemCollection"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/import-items/{importItem}": {
            "get": {
                "operationId": "getImportItem",
                "tags": [
                    "Imports"
                ],
                "summary": "Read one declaration line",
                "description": "A line belonging to another tenant answers 404, not 403 — the same id is indistinguishable from one that never existed, so this surface never confirms that somebody else's record exists.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "importItem",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "The gateway id of the declaration line."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The declaration line",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ImportItemEnvelope"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "404": {
                        "description": "No such declaration line for this tenant"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/import-items/{importItem}/approve": {
            "post": {
                "operationId": "approveImportItem",
                "tags": [
                    "Imports"
                ],
                "summary": "Accept the goods on a declaration line",
                "description": "Files `3 Approved` with ZRA and raises an incoming stock movement for the declared quantity, valued at what customs assessed.\n\n**The line must name an item in your catalogue.** Send `itemCode` here to match and approve in one call, or match it first — either way, ZRA refuses an approval without it. Two refusals come back as 422 rather than 500, and both are yours to fix: a line with no match, and one pointing at a **draft** item whose classification nobody has reviewed. Release the draft through `POST /api/v1/items/{item}/release` first.\n\nThe response's `filing.state` is `sending`, not `filed`: the write-back is queued with backoff, exactly as a customer registration is. Poll the line to watch it land.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "importItem",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "The gateway id of the declaration line."
                    }
                ],
                "requestBody": {
                    "required": false,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "itemCode": {
                                        "type": "string",
                                        "maxLength": 100,
                                        "description": "The code in your own catalogue for these goods. Optional only in the sense that the line may already be matched.\n",
                                        "example": "WIDGET1"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "The line as answered",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ImportItemEnvelope"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "404": {
                        "description": "No such declaration line for this tenant"
                    },
                    "422": {
                        "description": "The line is not matched to your catalogue, the code given does not exist at that branch, or it points at a draft item.\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ValidationErrorBody"
                                }
                            }
                        }
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/import-items/{importItem}/reject": {
            "post": {
                "operationId": "rejectImportItem",
                "tags": [
                    "Imports"
                ],
                "summary": "Refuse the goods on a declaration line",
                "description": "Files `4 Cancelled` with ZRA. No stock moves, and **no catalogue match is required** — ZRA accepts a rejection without one, and requiring a product record for goods that never arrived would be this gateway's rule rather than ZRA's.\n\n`reason` travels to ZRA as the line's `remark`, which is the only free-text field this endpoint carries. It is where \"two cartons short\" goes; without it the refusal reaches ZRA with no explanation at all.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "importItem",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "The gateway id of the declaration line."
                    }
                ],
                "requestBody": {
                    "required": false,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "reason": {
                                        "type": "string",
                                        "maxLength": 400,
                                        "description": "Why the goods are refused. Sent to ZRA.",
                                        "example": "Short delivery, 2 cartons missing"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "The line as answered",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ImportItemEnvelope"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "404": {
                        "description": "No such declaration line for this tenant"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/customers/{customer}": {
            "get": {
                "operationId": "getCustomer",
                "tags": [
                    "Customers"
                ],
                "summary": "Read one customer, and how far its registration got",
                "description": "By the gateway id returned when it was created. Poll this to watch `registration.state` move from `pending` to `registered`, and to read `registration.lastError` when it lands on `failed`.\n\nA customer belonging to another tenant answers 404, not 403 — the same id is indistinguishable from one that never existed, so this surface never confirms that somebody else's record exists.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "customer",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "The gateway id of the customer."
                    },
                    {
                        "$ref": "#/components/parameters/XBranch"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The customer",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/CustomerEnvelope"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "404": {
                        "description": "No such customer for this tenant"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/item-classes": {
            "get": {
                "operationId": "listItemClasses",
                "tags": [
                    "Catalogue"
                ],
                "summary": "ZRA item classification codes",
                "description": "Every item on a fiscal invoice must carry a classification code, so a catalogue has to be mapped against this list before it can be sold through. Served from the gateway's synced copy of ZRA's `selectItemsClass`, so it follows ZRA rather than going stale in each client. Codes withdrawn by ZRA (`useYn` = N) are never returned.\n\n**Built for lookup; enumerable when you must.** Paginated at up to 200 rows per page (`limit` is the page size), with `meta.total` and `links.next` so a partial listing can never masquerade as the whole — the full catalogue is roughly 101,000 codes. Use `search` to narrow to the item you are classifying. **To bulk-map a large catalogue, download the snapshot instead of paging**: `GET /docs/zra-item-classes.csv.gz` (public, no credential — gzipped CSV of the same catalogue) is one request where paging is ~500 into your rate limit.\n\nCallers on the Zambia (compat) surface cannot reach this endpoint — it needs a tenant bearer token, and `/zm/api` holds only a branch key. Use the snapshot, or a tenant token, to map codes into your catalogue before sending invoices.\n",
                "security": [
                    {
                        "TenantToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "search",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "maxLength": 100
                        },
                        "description": "Matches a code prefix or any part of a name."
                    },
                    {
                        "name": "level",
                        "in": "query",
                        "schema": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 9
                        },
                        "description": "ZRA's hierarchy depth. Unfiltered by default — the field is whatever ZRA sent, including null, so filtering is the caller's choice rather than a default that can silently empty the list.\n"
                    },
                    {
                        "name": "limit",
                        "in": "query",
                        "schema": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 200,
                            "default": 50
                        },
                        "description": "Page size."
                    },
                    {
                        "$ref": "#/components/parameters/Page"
                    }
                ],
                "x-codeSamples": [
                    {
                        "lang": "shell",
                        "label": "curl",
                        "source": "# Classify at the till: search, take the best-ranked match.\ncurl -H \"Authorization: Bearer $TENANT_TOKEN\" \\\n  \"https://gateway.mikenyambe.com/api/v1/item-classes?search=bread&limit=10\"\n\n# Bulk catalogue mapping: do NOT page this endpoint ~500 times —\n# download the full snapshot instead (public, no credential):\ncurl -LO https://gateway.mikenyambe.com/docs/zra-item-classes.csv.gz\n"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Matching classification codes",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ItemClassCollection"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Suspended"
                    },
                    "422": {
                        "description": "A query parameter is out of range — most often `limit` above 200, which is a **refusal, not a clamp**: a bulk mapper that asks for 1000 rows a page gets nothing back, rather than 200. Page at 200, or take the snapshot.\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ValidationErrorBody"
                                },
                                "example": {
                                    "message": "The limit field must not be greater than 200.",
                                    "errors": {
                                        "limit": [
                                            "The limit field must not be greater than 200."
                                        ]
                                    }
                                }
                            }
                        }
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/partner/keys": {
            "get": {
                "operationId": "listPartnerKeys",
                "description": "The partner's own secret keys, without their plaintext. Full lifecycle guidance — scoping down, rotation, IP allowlists, expiry — is in the partner-keys guide at `/docs/partner-keys`.\n",
                "tags": [
                    "Partner Keys"
                ],
                "summary": "List the partner's own secret keys (masked)",
                "security": [
                    {
                        "PartnerKey": [
                            "keys:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The partner's keys",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ApiKeyCollection"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            },
            "post": {
                "operationId": "mintPartnerKey",
                "tags": [
                    "Partner Keys"
                ],
                "summary": "Mint a least-privilege key",
                "description": "Scopes must be a subset of the calling key's; a sandbox key cannot mint production; a new key cannot outlive its creator; an IP-restricted key must set an allowlist. The plaintext is in the response; if you lose it, `GET /api/v1/partner/keys/{id}/secret` reads it back.\n",
                "security": [
                    {
                        "PartnerKey": [
                            "keys:write"
                        ]
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/NewPartnerKey"
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "Minted; the plaintext is here and re-readable from `/secret`",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/MintedApiKey"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Escalation blocked (scope/environment/expiry/ip)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationError"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/partner/keys/{id}/secret": {
            "get": {
                "operationId": "readPartnerKeySecret",
                "description": "Reads back the plaintext of a key you already hold, so a secret lost between the `201` and the deployment does not force a rotation of a credential that is working. Prefer this to rotating: a rotation your fleet has not picked up yet is an outage waiting on the grace window.\n\nReading a plaintext is exactly as powerful as minting a copy of the key, so it is bounded by the same least-privilege rules as minting: the calling key must hold every scope the target holds, be able to use its environment, and — where the caller is itself IP-restricted or expiring — the target must sit inside the caller's own allowlist and lifetime. A key can therefore read itself and anything narrower, and nothing broader.\n\nKeys issued before this endpoint existed have no stored plaintext, and a revoked key's copy is destroyed with it; both answer `404` `secret_unavailable`. Every read is recorded against the calling key.\n",
                "tags": [
                    "Partner Keys"
                ],
                "summary": "Read a key's plaintext back",
                "security": [
                    {
                        "PartnerKey": [
                            "keys:write"
                        ]
                    }
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "The partner key id from the list response — not the sk_… secret."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The plaintext, unchanged — whatever is already using this key keeps working.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "key": {
                                            "type": "string",
                                            "description": "The key's plaintext, exactly as it was issued.",
                                            "example": "sk_test_9f3c1d7a2b8e4f60a1c5d9e3b7f2a8c4d6e0b1f5"
                                        },
                                        "apiKey": {
                                            "$ref": "#/components/schemas/ApiKey"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "The acting key cannot hold this key's authority — it lacks one of the target's scopes or its environment (`insufficient_privilege`), or the target sits outside the acting key's IP allowlist (`ip_escalation`) or outlives it (`expiry_escalation`).\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "example": {
                                    "error": {
                                        "code": "insufficient_privilege",
                                        "message": "The current key cannot read a key more privileged than itself."
                                    }
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Either the key is not one of yours, or there is no plaintext to read (`secret_unavailable`) — it predates this endpoint, or it has been revoked. Rotate it to get a key whose plaintext can be read.\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "example": {
                                    "error": {
                                        "code": "secret_unavailable",
                                        "message": "This key has no readable plaintext — it was issued before keys became readable, or it has been revoked. Rotate it to get one that can be read."
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/partner/keys/{id}/rotate": {
            "post": {
                "operationId": "rotatePartnerKey",
                "description": "Issues a new plaintext for this key. The old plaintext keeps working through a short grace window so a fleet can be re-deployed without a hard cutover — see `/docs/partner-keys` for the window and the recommended rollout order.\n",
                "tags": [
                    "Partner Keys"
                ],
                "summary": "Rotate a key (mint replacement, sunset the old one after a grace window)",
                "security": [
                    {
                        "PartnerKey": [
                            "keys:write"
                        ]
                    }
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "The partner key id from the list response — not the sk_… secret."
                    }
                ],
                "responses": {
                    "201": {
                        "description": "Replacement minted. **The replacement inherits the rotated key's scopes, environment, IP allowlist and expiry deadline** — rotation preserves a key, it does not renew one. A key minted to expire on a date still expires on that date after rotating, so a key near the end of its life needs a fresh mint, not a rotation.\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "key": {
                                            "type": "string",
                                            "description": "The replacement plaintext. Also re-readable later from `GET /partner/keys/{id}/secret`."
                                        },
                                        "apiKey": {
                                            "$ref": "#/components/schemas/ApiKey"
                                        },
                                        "rotatedKeyExpiresAt": {
                                            "type": [
                                                "string",
                                                "null"
                                            ],
                                            "format": "date-time",
                                            "description": "When the OLD plaintext stops working — your deployment deadline. Normally now + the 7-day grace window, but an expiry already sooner than that is kept, so a key close to expiring gives you less than a week. Read this value rather than assuming seven days.\n"
                                        },
                                        "message": {
                                            "type": "string"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Either the acting key is less privileged than the one it is trying to rotate (`insufficient_privilege`), or this key has already been rotated (`already_rotated`) — rotation is once per key, and the replacement is the thing to rotate next time. Rotation returns a new plaintext, so the minting guardrails apply to the rotated key as well: an IP-restricted key cannot rotate a key whose allowlist is broader than its own (`ip_escalation`), and an expiring key cannot rotate a key that outlives it (`expiry_escalation`).\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "example": {
                                    "error": {
                                        "code": "already_rotated",
                                        "message": "This key has already been rotated."
                                    }
                                }
                            }
                        }
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/partner/keys/{id}": {
            "delete": {
                "operationId": "revokePartnerKey",
                "description": "Immediate, no grace window — this is the kill switch, rotation is the routine path. A key cannot revoke itself into a lockout you cannot fix: keep one admin key aside.\n",
                "tags": [
                    "Partner Keys"
                ],
                "summary": "Revoke one of the partner's own keys",
                "security": [
                    {
                        "PartnerKey": [
                            "keys:write"
                        ]
                    }
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "The partner key id from the list response — not the sk_… secret."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Revoked"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/webhooks": {
            "get": {
                "operationId": "listWebhookEndpoints",
                "description": "Every endpoint this partner has registered, newest first. Not paginated — the list is as long as the number of receivers you run.\n",
                "tags": [
                    "Webhook endpoints"
                ],
                "summary": "List the partner's webhook endpoints",
                "security": [
                    {
                        "PartnerKey": [
                            "webhooks:read"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The partner's endpoints",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/WebhookEndpointCollection"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            },
            "post": {
                "operationId": "createWebhookEndpoint",
                "tags": [
                    "Webhook endpoints"
                ],
                "summary": "Be told when a sale reaches ZRA, instead of polling",
                "description": "The signing `secret` is returned once in this response and never again. Store it — you need it to verify every delivery.\n\n## Verifying a delivery\n\nEach delivery carries `X-SmartInvoicing-Signature: t=<unix>,v1=<hex>`, where\n\n```\nv1 = hex(HMAC_SHA256(secret, \"<t>\" + \".\" + <raw request body>))\n```\n\nCompute it over the **raw bytes of the request body, before JSON parsing** — re-serialising the payload changes the bytes and the signature will not match. Compare with a constant-time equality check.\n\nThe timestamp is inside the signed material, so a captured delivery cannot be replayed under a new one. Reject deliveries whose `t` is more than **300 seconds** from your clock.\n\n## Delivery headers\n\n| Header | Meaning |\n| --- | --- |\n| `X-SmartInvoicing-Signature` | `t=<unix>,v1=<hex>` — verify this. |\n| `X-Webhook-Event` | The event name, e.g. `invoice.fiscalised`. |\n| `X-Webhook-Id` | The event ULID, equal to the payload `id`. Dedupe on it. |\n| `X-Webhook-Delivery` | The delivery row's id — the same on every retry of that delivery, so it identifies the delivery, not the attempt. Quote it in support requests and pass it to the resend endpoint; do not use it as an attempts-table primary key. |\n\n## Retries\n\nDelivery is **at-least-once** — dedupe on `X-Webhook-Id`. Any non-2xx, or no response within a 10 second timeout, is retried up to 6 attempts with backoff of 60s, 5m, 30m, 2h then 6h. Redirects are **not** followed; a 3xx counts as a failure. After 10 consecutive exhausted deliveries the endpoint is disabled; recover it — same secret, failure streak cleared — with `POST /api/v1/webhooks/{id}/enable`. Events fired while disabled are not queued and cannot be resent.\n\nFull guide, with runnable PHP and Node verifiers: `/docs/webhooks`.\n",
                "security": [
                    {
                        "PartnerKey": [
                            "webhooks:write"
                        ]
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/NewWebhook"
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "Endpoint created; secret shown once",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/CreatedWebhook"
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationError"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/webhooks/{id}": {
            "parameters": [
                {
                    "name": "id",
                    "in": "path",
                    "required": true,
                    "schema": {
                        "type": "integer"
                    },
                    "description": "The webhook endpoint id."
                }
            ],
            "get": {
                "operationId": "getWebhookEndpoint",
                "description": "One endpoint, including its `status`, `lastSuccessAt` and `lastFailureAt` — the place to look when deliveries seem to have stopped. The consecutive-failure count that drives auto-disable is not exposed here; read the trend from `GET /api/v1/webhooks/{id}/deliveries`.\n",
                "tags": [
                    "Webhook endpoints"
                ],
                "summary": "Retrieve one endpoint",
                "security": [
                    {
                        "PartnerKey": [
                            "webhooks:read"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The endpoint",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/WebhookEndpointEnvelope"
                                }
                            }
                        }
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            },
            "delete": {
                "operationId": "deleteWebhookEndpoint",
                "description": "Permanent. Deliveries stop, the delivery log for it becomes unreachable, and the signing secret dies with it — re-registering mints a new one. To pause a receiver temporarily, let auto-disable do it and re-enable later instead.\n",
                "tags": [
                    "Webhook endpoints"
                ],
                "summary": "Delete an endpoint",
                "security": [
                    {
                        "PartnerKey": [
                            "webhooks:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Deleted"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/webhooks/{id}/enable": {
            "post": {
                "operationId": "enableWebhookEndpoint",
                "tags": [
                    "Webhook endpoints"
                ],
                "summary": "Re-enable an auto-disabled endpoint",
                "description": "The way back from auto-disable. Re-enables the endpoint and clears its consecutive-failure count, keeping the same signing secret — no receiver redeploy. It does not replay the gap: events fired while the endpoint was disabled were never queued and cannot be resent, so reconcile with `GET /api/v1/sales` if the gap matters. Idempotent on an endpoint that is already enabled.\n",
                "security": [
                    {
                        "PartnerKey": [
                            "webhooks:write"
                        ]
                    }
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "The webhook endpoint id."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Enabled; deliveries resume from now",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "enabled": {
                                            "type": "boolean",
                                            "example": true
                                        },
                                        "endpoint": {
                                            "$ref": "#/components/schemas/WebhookEndpoint"
                                        },
                                        "message": {
                                            "type": "string"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/webhooks/{id}/deliveries": {
            "get": {
                "operationId": "listWebhookDeliveries",
                "description": "The attempt log for one endpoint, newest first: each row carries the event, the attempt number, your receiver's response code and when the next retry is due. This is the first place to debug a receiver.\n",
                "tags": [
                    "Webhook endpoints"
                ],
                "summary": "The delivery log for an endpoint (paginated, newest first)",
                "security": [
                    {
                        "PartnerKey": [
                            "webhooks:read"
                        ]
                    }
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "The webhook endpoint id — this lists that endpoint's deliveries, not a single delivery."
                    },
                    {
                        "$ref": "#/components/parameters/Page"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A page of deliveries (25 per page)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/WebhookDeliveryCollection"
                                }
                            }
                        }
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/webhooks/deliveries/{id}/resend": {
            "post": {
                "operationId": "resendWebhookDelivery",
                "description": "Re-queues this delivery for immediate redelivery with a fresh signature timestamp. Use it after fixing a receiver bug to replay a specific failed event without waiting for the retry ladder.\n",
                "tags": [
                    "Webhook endpoints"
                ],
                "summary": "Re-queue a delivery for immediate redelivery",
                "security": [
                    {
                        "PartnerKey": [
                            "webhooks:write"
                        ]
                    }
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "The DELIVERY id from a row of the delivery log — a different entity from the endpoint id in the paths beside this one."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Re-queued"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/PartnerSuspended"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/ApiKey/getactive": {
            "post": {
                "operationId": "getActiveApiKey",
                "tags": [
                    "Zambia (compat)"
                ],
                "summary": "Fetch a till's key and branches with a till login",
                "description": "The setup call a POS built against an older Zambian gateway already makes: the installer types a URL, an email and a password, and the till comes back holding its key and the branches it may file through. Served here with the same request body and the same response shape so that flow does not have to be rewritten.\n\n**The credential is a till login, not the merchant's dashboard password.** On the gateway this imitates the two are the same thing, which is what makes the API key it returns re-derivable by anyone holding the account. Here a till login is created against one key, is worth that key alone, and is withdrawn without disturbing it — and `users` is not consulted on this path at all. A merchant creates one under Developers → Till keys.\n\n**Every failure is a 400 carrying the same body.** A wrong password and an address nobody has registered are deliberately indistinguishable, so this endpoint cannot be used to find out who banks here. Branch on `success`. It is throttled to five attempts per fifteen minutes per address and per caller; a sixth is 429 even when the password is right, because the limit is on the account rather than on the guess.\n\nUnauthenticated by necessity — the caller has nothing to present yet — and always answered in the legacy envelope regardless of the key's own `dialect`, because a till calling this has not been configured yet. Added in 1.10.0.\n",
                "security": [],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/TillLogin"
                            },
                            "example": {
                                "email": "till@katema.co.zm",
                                "password": "a-long-till-password"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "The key and its branches",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ActiveApiKeyEnvelope"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "The body failed validation, or the email and password do not match a live till login. Both answer the same shape; only `message` differs, and it is not a reliable discriminator by design.\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/TillLoginError"
                                },
                                "examples": {
                                    "rejected": {
                                        "summary": "Wrong password, or no such login",
                                        "value": {
                                            "success": false,
                                            "httpCode": 400,
                                            "message": "Invalid email or password",
                                            "errorCode": null,
                                            "errors": null,
                                            "data": null
                                        }
                                    },
                                    "malformed": {
                                        "summary": "Missing fields",
                                        "value": {
                                            "success": false,
                                            "httpCode": 400,
                                            "message": "Validation failed",
                                            "errorCode": null,
                                            "errors": {
                                                "email": [
                                                    "The email field is required."
                                                ]
                                            },
                                            "data": null
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "The key is live but its recoverable copy is gone, so there is nothing to hand back. Mint a new key.\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/TillLoginError"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Too many attempts against this address or from this caller. Returned even for a correct password until the window clears.\n",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/TillLoginError"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/zm/api/Branches": {
            "get": {
                "operationId": "zmListBranches",
                "tags": [
                    "Zambia (compat)"
                ],
                "summary": "List the branches this key may file through",
                "description": "The first call a till makes, and the only one on this surface that does not need an `X-SdcId` — because this is where the `X-SdcId` comes from. A setup screen that asks an installer for a URL and an API key can fill the branch picker from here instead of having the SDC id read out to them over the phone.\n\nWhat comes back depends on the key, and that is the answer rather than a limitation. A **tenant key** lists every branch of its taxpayer; the till stores the `sdcId` of the one it was installed at and sends it on every later call. A **branch key** lists the single branch it is bound to — useful confirmation that the key cannot be pointed anywhere else.\n\n`initialized` is the field worth branching on. A branch that has not completed device initialisation with ZRA holds no signing keys, so a sale sent through it fails at the VSDC rather than here; offer it in the picker greyed out, or not at all.\n\nAdded in 1.9.0. Bare array, like `GET /zm/api/TaxCodes` — see \"Migrating a till\" for why nothing here is wrapped in an envelope.\n",
                "security": [
                    {
                        "ZmApiKey": []
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The branches this key may file through",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/ZmBranch"
                                    }
                                },
                                "example": [
                                    {
                                        "branch": "000",
                                        "name": "Head Office",
                                        "tpin": "1002101584",
                                        "sdcId": "SDC0060000514",
                                        "initialized": true,
                                        "lastInvoiceNo": 127
                                    },
                                    {
                                        "branch": "001",
                                        "name": "Kabwe",
                                        "tpin": "1002101584",
                                        "sdcId": "SDC0060000515",
                                        "initialized": false,
                                        "lastInvoiceNo": 0
                                    }
                                ]
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/ZmUnauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/ZmForbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/ZmPaymentRequired"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/zm/api/TaxCodes": {
            "get": {
                "operationId": "zmListTaxCodes",
                "tags": [
                    "Zambia (compat)"
                ],
                "summary": "List the Zambia tax categories and rates",
                "description": "The VAT categories this branch may use on a line item. `taxCode` is what you put in an invoice line's `TaxCodes` array. `taxableAmount`, `taxAmount` and `conversionRate` are present for contract compatibility and are always `0`, `0` and `1` here.\n\nEvery code listed here is accepted by `POST /zm/api/Invoice` and is charged the `rate` quoted beside it. Since 1.5.1 that is guaranteed rather than incidental: the list is the intersection of what the invoice endpoint accepts, what the pricer can rate, and what ZRA still publishes as in use. `TOT` (Turnover Tax) is therefore not listed — this gateway files VAT, and an invoice carrying `TOT` has always been refused.\n",
                "security": [
                    {
                        "ZmApiKey": [],
                        "ZmSdcId": []
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The branch's tax categories",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/ZmTaxCode"
                                    }
                                },
                                "example": [
                                    {
                                        "taxCode": "A",
                                        "categoryName": "Standard Rated",
                                        "rate": 16,
                                        "taxableAmount": 0,
                                        "taxAmount": 0,
                                        "conversionRate": 1
                                    },
                                    {
                                        "taxCode": "B",
                                        "categoryName": "Standard Rated (B)",
                                        "rate": 16,
                                        "taxableAmount": 0,
                                        "taxAmount": 0,
                                        "conversionRate": 1
                                    },
                                    {
                                        "taxCode": "C1",
                                        "categoryName": "Zero Rated (Export)",
                                        "rate": 0,
                                        "taxableAmount": 0,
                                        "taxAmount": 0,
                                        "conversionRate": 1
                                    },
                                    {
                                        "taxCode": "E",
                                        "categoryName": "Exempt",
                                        "rate": 0,
                                        "taxableAmount": 0,
                                        "taxAmount": 0,
                                        "conversionRate": 1
                                    }
                                ]
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/ZmUnauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/ZmForbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/ZmPaymentRequired"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/zm/api/Invoice": {
            "post": {
                "operationId": "zmFiscaliseInvoice",
                "tags": [
                    "Zambia (compat)"
                ],
                "summary": "Fiscalise a sale",
                "description": "Submits a sale to ZRA Smart Invoice and returns the fiscal details to print on the receipt (signature, QR code, internal data, invoice sequence and the tax breakdown).\n\n**Idempotent on `InvoiceNumber`.** Re-sending an invoice number this tenant has already fiscalised returns the original fiscal details without consuming another ZRA invoice number — safe to retry after a timeout.\n\n**Offline tolerant.** If ZRA cannot be reached the sale is still accepted and queued in invoice order; `signature`, `qrCode` and `internalData` come back `null` and are filled in once the queue drains. Listen for the `invoice.fiscalised` webhook instead of polling.\n\nOnly normal sales (`ReceiptTypeCode: \"S\"`) are supported today; credit (`R`) and debit (`D`) notes validate but are rejected with 422.\n",
                "security": [
                    {
                        "ZmApiKey": [],
                        "ZmSdcId": []
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/ZmInvoice"
                            },
                            "example": {
                                "InvoiceNumber": "INV-1042",
                                "IssuerName": "Cashier 1",
                                "IssuerId": "104",
                                "ReceiptTypeCode": "S",
                                "PaymentTypeCode": "01",
                                "currencyType": "ZMW",
                                "conversionRate": 1,
                                "CustomerName": "Katema Stores Ltd",
                                "CustomerTpin": "1002101584",
                                "invoiceItems": [
                                    {
                                        "ItemSequenceNumber": 1,
                                        "ItemDesc": "Maize meal 25kg",
                                        "itemCode": "MM-25",
                                        "itemClassificationCode": "50221204",
                                        "TaxCodes": [
                                            "A"
                                        ],
                                        "Quantity": 2,
                                        "UnitPrice": 350,
                                        "isTaxInclusive": true,
                                        "PackagingUnitCode": "BG",
                                        "QuantityUnitCode": "KG"
                                    }
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Fiscalised (or the original result for a repeated InvoiceNumber)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ZmFiscalDetails"
                                },
                                "example": {
                                    "invoiceNumber": "INV-1042",
                                    "taxPayerInvoiceNumber": "91",
                                    "invoiceSequence": 91,
                                    "vsdcdate": "20260730103012",
                                    "signature": "PGRWGD65",
                                    "qrCode": "https://portal.zra.org.zm/indexInvoiceData?Data=1002101584001PGRWGD65",
                                    "internalData": "RTBWQ-XY3KZ-9MNQP-2LDVF",
                                    "sdcId": "SDC0060000514",
                                    "receiptTotalCounter": 91,
                                    "normalReceiptTypeCounter": 91,
                                    "currencyType": "ZMW",
                                    "totalAmount": 700,
                                    "taxItems": [
                                        {
                                            "taxCode": "A",
                                            "rate": 16,
                                            "taxableAmount": 603.45,
                                            "taxAmount": 96.55
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/ZmUnauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/ZmForbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/ZmPaymentRequired"
                    },
                    "422": {
                        "$ref": "#/components/responses/ZmUnprocessable"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/zm/api/Invoice/{clientInvoicenumber}": {
            "get": {
                "operationId": "zmGetInvoice",
                "tags": [
                    "Zambia (compat)"
                ],
                "summary": "Retrieve a fiscalised invoice by your own invoice number",
                "description": "Looks up a sale by the `InvoiceNumber` you sent, scoped to the calling branch's tenant. Use it to recover fiscal details after a lost response, or to check whether a queued (offline) sale has been fiscalised yet.\n",
                "security": [
                    {
                        "ZmApiKey": [],
                        "ZmSdcId": []
                    }
                ],
                "parameters": [
                    {
                        "name": "clientInvoicenumber",
                        "in": "path",
                        "required": true,
                        "description": "The `InvoiceNumber` supplied when the sale was fiscalised.",
                        "schema": {
                            "type": "string",
                            "example": "INV-1042"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The invoice's fiscal details",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ZmFiscalDetails"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/ZmUnauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/ZmForbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/ZmPaymentRequired"
                    },
                    "404": {
                        "$ref": "#/components/responses/ZmNotFound"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/zm/api/Signature/{invoiceNumber}": {
            "get": {
                "operationId": "zmGetSignature",
                "tags": [
                    "Zambia (compat)"
                ],
                "summary": "Reprint — identical payload to GET /zm/api/Invoice/{n}",
                "description": "Identical payload to retrieving the invoice — provided under a separate path for tills that call it when reprinting a receipt.\n",
                "security": [
                    {
                        "ZmApiKey": [],
                        "ZmSdcId": []
                    }
                ],
                "parameters": [
                    {
                        "name": "invoiceNumber",
                        "in": "path",
                        "required": true,
                        "description": "The `InvoiceNumber` supplied when the sale was fiscalised.",
                        "schema": {
                            "type": "string",
                            "example": "INV-1042"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The invoice's fiscal details",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ZmFiscalDetails"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/ZmUnauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/ZmForbidden"
                    },
                    "402": {
                        "$ref": "#/components/responses/ZmPaymentRequired"
                    },
                    "404": {
                        "$ref": "#/components/responses/ZmNotFound"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        }
    },
    "components": {
        "securitySchemes": {
            "PartnerKey": {
                "type": "http",
                "scheme": "bearer",
                "description": "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.\n"
            },
            "TenantToken": {
                "type": "http",
                "scheme": "bearer",
                "description": "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`.\n\n**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.\n"
            },
            "ZmApiKey": {
                "type": "apiKey",
                "in": "header",
                "name": "X-Api-Key",
                "description": "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.\n"
            },
            "ZmSdcId": {
                "type": "apiKey",
                "in": "header",
                "name": "X-SdcId",
                "description": "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.\n"
            }
        },
        "parameters": {
            "XBranch": {
                "name": "X-Branch",
                "in": "header",
                "required": false,
                "schema": {
                    "type": "string"
                },
                "example": "000",
                "description": "Which branch (bhfId) this request acts through. Omit it and the tenant's default branch is used; if no default is set, the lowest-id initialised branch is. `?bhf=` is accepted as a query-string alias with equal standing. An unknown value is not itself an error — no device comes into context, and a fiscalisation then fails with 422. Legal values come from `GET /api/v1/me` (`devices[].branch`).\n\nOn a credit note this must be the branch that raised the original invoice — a mismatch is refused with 409 naming the right branch, because the note burns an invoice number from the branch it is fiscalised on.\n"
            },
            "Page": {
                "name": "page",
                "in": "query",
                "required": false,
                "schema": {
                    "type": "integer",
                    "minimum": 1,
                    "default": 1
                },
                "description": "Page number. The response's `meta.total` says how much there is in all."
            }
        },
        "responses": {
            "Unauthorized": {
                "description": "Missing or invalid credentials",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/Error"
                        }
                    }
                }
            },
            "Forbidden": {
                "description": "Authenticated but not permitted (scope, IP, or escalation blocked)",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/Error"
                        }
                    }
                }
            },
            "NotFound": {
                "description": "Not found — or found, but not owned by the credential that asked. The two are deliberately indistinguishable: a tenant, key or endpoint belonging to another partner answers 404 rather than 403, so this surface never confirms that somebody else's record exists.\n\n**Two body shapes answer this status,** because most 404s here are raised by the framework rather than by a controller. Branch on the status code; treat the body as diagnostic only.\n\n| Raised by | Body |\n| --- | --- |\n| Ownership guards, route-model binding, unmatched ids — the common case | `{\"message\": \"...\"}` — no `error` object |\n| `POST …/api-keys` when `branch` names an unregistered branch | `{\"error\": {\"code\": \"device_not_found\", \"message\": \"...\"}}` |\n| `POST …/devices/{branch}/initialize` on an unregistered branch | `{\"error\": {\"code\": \"device_not_found\", \"message\": \"...\"}}` |\n",
                "content": {
                    "application/json": {
                        "schema": {
                            "oneOf": [
                                {
                                    "$ref": "#/components/schemas/FrameworkMessage"
                                },
                                {
                                    "$ref": "#/components/schemas/Error"
                                }
                            ]
                        },
                        "examples": {
                            "notOwned": {
                                "summary": "The framework shape — every ownership and binding 404",
                                "value": {
                                    "message": "No query results for model [App\\Models\\Tenant] 91."
                                }
                            },
                            "deviceNotFound": {
                                "summary": "The controller shape — only the two device-branch lookups",
                                "value": {
                                    "error": {
                                        "code": "device_not_found",
                                        "message": "Branch 007 is not registered for this tenant."
                                    }
                                }
                            }
                        }
                    }
                }
            },
            "ValidationError": {
                "description": "Validation failed",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ValidationErrorBody"
                        }
                    }
                }
            },
            "PartnerSuspended": {
                "description": "Your **partner** account is not active — this is not a merchant's subscription. Raised by the partner authentication layer before any controller runs, so it can answer every operation on this surface, and the body is the `{error: {code, message}}` envelope rather than the merchant `PaymentRequired` shape. Nothing was created or changed.\n",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/Error"
                        },
                        "example": {
                            "error": {
                                "code": "partner_suspended",
                                "message": "This partner account is not active."
                            }
                        }
                    }
                }
            },
            "PaymentRequired": {
                "description": "Billing blocked the request — nothing was fiscalised. `error` is a stable machine-readable reason; `message` says it for a human.\n\nThis is the **merchant** subscription gate. A suspended *partner* account answers 402 too, with a different body — see `PartnerSuspended`.\n",
                "content": {
                    "application/json": {
                        "schema": {
                            "type": "object",
                            "properties": {
                                "message": {
                                    "type": "string",
                                    "example": "Your subscription is not active. Please renew to continue fiscalising invoices."
                                },
                                "error": {
                                    "type": "string",
                                    "enum": [
                                        "subscription_inactive",
                                        "quota_exceeded",
                                        "no_subscription"
                                    ],
                                    "description": "`subscription_inactive` — renew. `quota_exceeded` — the plan's hard invoice quota is used up; upgrade. `no_subscription` — the tenant has never had a plan; choose one.\n",
                                    "example": "subscription_inactive"
                                }
                            }
                        }
                    }
                }
            },
            "Suspended": {
                "description": "The tenant is suspended. Every merchant-surface request answers this until the suspension is lifted.",
                "content": {
                    "application/json": {
                        "schema": {
                            "type": "object",
                            "properties": {
                                "message": {
                                    "type": "string",
                                    "example": "Tenant is suspended."
                                }
                            }
                        }
                    }
                }
            },
            "RateLimited": {
                "description": "Rate limit exceeded. Honour `Retry-After` and drain queued work smoothly rather than in bursts.\n\nOn the merchant surface (tenant token) and on `/zm/api` the ceiling comes from the tenant's plan — see \"Rate limits\" under the Sales tag.\n\n**On the partner surface it does not.** A partner secret key resolves no tenant, so the ceiling is a flat **120/min per key**, and no merchant plan raises it. Size a bulk-provisioning sweep for that number: a run paced for 600/min will start throwing 429 partway through and can leave merchants half-provisioned, which matters most on device initialisation because that call is one-shot.\n",
                "headers": {
                    "Retry-After": {
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Seconds to wait before retrying."
                    },
                    "X-RateLimit-Limit": {
                        "schema": {
                            "type": "integer"
                        },
                        "description": "The requests-per-minute ceiling in force for this credential."
                    },
                    "X-RateLimit-Remaining": {
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Requests remaining in the current window."
                    }
                },
                "content": {
                    "application/json": {
                        "schema": {
                            "type": "object",
                            "properties": {
                                "message": {
                                    "type": "string",
                                    "example": "Too Many Attempts."
                                }
                            }
                        }
                    }
                }
            },
            "VsdcUnreachable": {
                "description": "The tenant's VSDC could not be reached (retryable)",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/Error"
                        }
                    }
                }
            },
            "ZmUnauthorized": {
                "description": "Missing, unknown or mismatched `X-Api-Key` / `X-SdcId`",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ZmError"
                        },
                        "example": {
                            "status": 401,
                            "message": "Invalid API key or SDC ID for this branch."
                        }
                    }
                }
            },
            "ZmNotFound": {
                "description": "No invoice with that number for this tenant",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ZmError"
                        },
                        "example": {
                            "status": 404,
                            "message": "No fiscalised invoice found for InvoiceNumber INV-1042."
                        }
                    }
                }
            },
            "ZmForbidden": {
                "description": "The key is real but not allowed to make this call — either the caller's IP is outside the key's allowlist, or the key lacks the `invoices:write` scope. That scope is checked in the middleware, so it is required on the read endpoints of this surface too, not just on `POST /zm/api/Invoice`.\n",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ZmError"
                        },
                        "example": {
                            "status": 403,
                            "message": "This API key may not be used from your IP address."
                        }
                    }
                }
            },
            "ZmPaymentRequired": {
                "description": "Billing blocked the call — the subscription is inactive, has no plan, or the plan's hard invoice quota is exhausted. Nothing was fiscalised.\n\nTwo body shapes reach a caller here. The tenant-inactive check runs in the middleware and returns `{status, message}` with **no** `error` field; the quota check runs in the controller and includes `error`. Branch on the status code, not on the presence of `error`.\n",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ZmError"
                        },
                        "examples": {
                            "quotaExhausted": {
                                "summary": "From the controller — the plan's invoice quota is used up",
                                "value": {
                                    "status": 402,
                                    "message": "You have reached your plan's invoice limit (5000 invoices). Upgrade your plan to fiscalise more invoices.",
                                    "error": "quota_exceeded"
                                }
                            },
                            "subscriptionInactive": {
                                "summary": "From the controller — the plan lapsed",
                                "value": {
                                    "status": 402,
                                    "message": "Your subscription is not active. Please renew to continue fiscalising invoices.",
                                    "error": "subscription_inactive"
                                }
                            },
                            "tenantInactive": {
                                "summary": "From the middleware — no `error` field to branch on",
                                "value": {
                                    "status": 402,
                                    "message": "Account is not active. Resolve your subscription."
                                }
                            }
                        }
                    }
                }
            },
            "ZmUnprocessable": {
                "description": "**Three different failures share this status, in two different body shapes.** The distinction matters because only one of them is worth retrying, and one of them is not a `ZmError` at all.\n\n| Failure | Shape | `status` present? | What to do |\n| --- | --- | --- | --- |\n| Field validation (a rule in the request) | Laravel's `{message, errors}` | **No** | Fix the payload. The `errors` map names the offending fields. |\n| Refused before submission (unsupported `ReceiptTypeCode`, an invoice rule) | `ZmError` | Yes | Fix the document. ZRA never saw it. |\n| ZRA refused it | `ZmError` + `resultCode` / `resultMessage` | Yes | Branch on `resultCode` — see `/docs/result-codes`. |\n\nA client that reads `body.status` to detect an error therefore reads `undefined` on the most common 422 of the three. **Branch on the HTTP status code**, then look for `errors` to tell a validation failure from a fiscalisation one.\n",
                "content": {
                    "application/json": {
                        "schema": {
                            "oneOf": [
                                {
                                    "$ref": "#/components/schemas/ValidationErrorBody"
                                },
                                {
                                    "$ref": "#/components/schemas/ZmError"
                                }
                            ]
                        },
                        "examples": {
                            "fieldValidation": {
                                "summary": "Laravel's shape — no `status`, but an `errors` map",
                                "value": {
                                    "message": "The invoiceItems.0.itemClassificationCode field is required.",
                                    "errors": {
                                        "invoiceItems.0.itemClassificationCode": [
                                            "The invoiceItems.0.itemClassificationCode field is required."
                                        ]
                                    }
                                }
                            },
                            "refusedBeforeSubmission": {
                                "summary": "The gateway refused it — ZRA never saw the document",
                                "value": {
                                    "status": 422,
                                    "message": "Only normal sales (ReceiptTypeCode \"S\") are supported yet; credit/debit notes are coming."
                                }
                            },
                            "rejectedByZra": {
                                "summary": "ZRA refused it — branch on `resultCode`",
                                "value": {
                                    "status": 422,
                                    "message": "Fiscalisation failed.",
                                    "error": "Sale failed [910]: Request parameter error",
                                    "resultCode": "910",
                                    "resultMessage": "Request parameter error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "schemas": {
            "Error": {
                "type": "object",
                "description": "The `{error: {code, message}}` envelope used by this API's own authentication, authorisation and provisioning failures. `code` is the stable machine-readable reason; branch on it rather than on `message`.\n",
                "properties": {
                    "error": {
                        "type": "object",
                        "properties": {
                            "code": {
                                "type": "string",
                                "example": "insufficient_scope"
                            },
                            "message": {
                                "type": "string"
                            }
                        }
                    }
                }
            },
            "FrameworkMessage": {
                "type": "object",
                "description": "Laravel's own error body, returned when the framework answers before any controller does — an ownership guard, route-model binding, a missing route, or an unhandled server error. It carries **no** `error` object and no machine-readable code, so a client that always reads `body.error.code` reads undefined here. Branch on the HTTP status.\n",
                "properties": {
                    "message": {
                        "type": "string",
                        "example": "No query results for model [App\\Models\\Tenant] 91."
                    }
                },
                "required": [
                    "message"
                ]
            },
            "ValidationErrorBody": {
                "type": "object",
                "description": "Laravel's validation shape: `errors` maps each offending field (dot notation for nested and array fields) to its messages.\n",
                "properties": {
                    "message": {
                        "type": "string",
                        "example": "The items.0.classificationCode field is required."
                    },
                    "errors": {
                        "type": "object",
                        "additionalProperties": {
                            "type": "array",
                            "items": {
                                "type": "string"
                            }
                        },
                        "example": {
                            "items.0.classificationCode": [
                                "The items.0.classificationCode field is required."
                            ],
                            "occurredAt": [
                                "ZRA will not accept a sale more than 180 days old, so this one cannot be fiscalised under its own date."
                            ]
                        }
                    }
                }
            },
            "FiscalisationError": {
                "type": "object",
                "description": "The document reached the fiscalisation engine and was refused — by a pre-submission invoice rule, or by ZRA itself. When ZRA refused it, `resultCode` / `resultMessage` carry ZRA's verdict as fields: branch on `resultCode` (see the result-codes guide at `/docs/result-codes`), and treat the prose in `error` as human-readable only.\n",
                "properties": {
                    "message": {
                        "type": "string",
                        "example": "ZRA rejected the sale."
                    },
                    "error": {
                        "type": "string",
                        "example": "Sale failed [910]: Request parameter error"
                    },
                    "resultCode": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "ZRA's result code, verbatim. Absent when the refusal happened before submission.",
                        "example": "910"
                    },
                    "resultMessage": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "ZRA's message for that code, verbatim.",
                        "example": "Request parameter error"
                    }
                }
            },
            "PaginationLinks": {
                "type": "object",
                "properties": {
                    "first": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "last": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "prev": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "next": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Null on the last page."
                    }
                }
            },
            "PaginationMeta": {
                "type": "object",
                "properties": {
                    "current_page": {
                        "type": "integer",
                        "example": 1
                    },
                    "from": {
                        "type": [
                            "integer",
                            "null"
                        ],
                        "example": 1
                    },
                    "last_page": {
                        "type": "integer",
                        "example": 3
                    },
                    "per_page": {
                        "type": "integer",
                        "example": 20
                    },
                    "to": {
                        "type": [
                            "integer",
                            "null"
                        ],
                        "example": 20
                    },
                    "total": {
                        "type": "integer",
                        "description": "Total matching records. Read this before assuming one page is all of them.",
                        "example": 54
                    }
                }
            },
            "SaleBody": {
                "type": "object",
                "required": [
                    "items"
                ],
                "description": "The fields a sale and a credit note share. A credit note is the same document with its sign reversed, so it accepts the same body — defined once here precisely so the two schemas cannot drift apart again (the credit note used to under-declare what it accepted).\n",
                "properties": {
                    "paymentMethod": {
                        "type": "string",
                        "default": "01",
                        "enum": [
                            "01",
                            "02",
                            "03",
                            "04",
                            "05",
                            "06",
                            "07",
                            "08"
                        ],
                        "description": "ZRA's payment method table, declared to ZRA verbatim: `01` Cash, `02` Credit, `03` Cash/Credit, `04` Bank cheque, `05` Debit & credit card, `06` Mobile money, `07` Other, `08` Bank transfer. An unknown code is refused with 422 rather than passed through.\n",
                        "example": "06"
                    },
                    "occurredAt": {
                        "type": "string",
                        "format": "date-time",
                        "description": "When the sale actually happened, for a caller that knows it is not now — a till that rang it up while it could not reach the gateway. Omitted means now, which is what a till at the counter means. The future is clamped to now; the past is floored at ZRA's 180-day rule, refused with 422 in words rather than through a result code. Also accepted on a credit note, for the moment of the refund.\n",
                        "example": "2026-08-30T18:42:00+02:00"
                    },
                    "lpoNumber": {
                        "type": "string",
                        "maxLength": 50,
                        "description": "Required when items use VAT category C2 (Zero-rating LPO), and rejected otherwise. A C2 invoice may carry no other category.\n"
                    },
                    "destinationCountryCode": {
                        "type": "string",
                        "minLength": 2,
                        "maxLength": 2,
                        "description": "Where the goods are being exported to, ISO 3166-1 alpha-2. **Required when items use VAT category C1 (Exports), and rejected otherwise** — a C1 invoice may carry no other category, and a domestic sale has no destination. Case-insensitive.\n\nThe code is validated as two letters here and by ZRA on arrival; an unknown country is refused by the VSDC, not by the gateway.\n",
                        "example": "CD"
                    },
                    "currency": {
                        "type": "string",
                        "enum": [
                            "ZMW",
                            "USD",
                            "GBP",
                            "EUR",
                            "CNY",
                            "ZAR"
                        ],
                        "description": "The currency this sale is invoiced in. Omitted means ZMW. ZRA publishes 179 currencies but Smart Invoice accepts these six.\n",
                        "example": "ZMW"
                    },
                    "exchangeRate": {
                        "type": "number",
                        "description": "How many kwacha one unit of `currency` buys, e.g. `26.5` for USD. Required for every currency except ZMW, where it must be 1 or omitted. A foreign currency at a rate of 1 is refused.\n",
                        "example": 26.5
                    },
                    "items": {
                        "type": "array",
                        "minItems": 1,
                        "items": {
                            "$ref": "#/components/schemas/NewSaleItem"
                        }
                    }
                }
            },
            "NewSale": {
                "allOf": [
                    {
                        "$ref": "#/components/schemas/SaleBody"
                    },
                    {
                        "type": "object",
                        "properties": {
                            "customer": {
                                "type": "object",
                                "properties": {
                                    "name": {
                                        "type": "string",
                                        "maxLength": 60,
                                        "example": "Walk-in Customer"
                                    },
                                    "tpin": {
                                        "type": "string",
                                        "pattern": "^[0-9]{10}$",
                                        "example": "1002101584"
                                    },
                                    "address": {
                                        "type": "string",
                                        "maxLength": 200,
                                        "description": "Printed on the tax invoice and never transmitted — ZRA's saveSales DTO has no address field, but the document must carry one. Accepted since the field existed; documented here only now.\n",
                                        "example": "Plot 24, Cairo Road, Lusaka"
                                    },
                                    "phone": {
                                        "type": "string",
                                        "maxLength": 20,
                                        "description": "The customer's mobile number, which enters them in ZRA's invoice reward draw. Optional, and only useful on a sale with no `tpin`: ZRA runs the draw for unidentified sales only, so a number sent alongside a `tpin` is filed and stored but enters nobody.\n\nOnly a Zambian mobile can be entered — 095, 096, 097, 075, 076 or 077 — in any of `0971234567`, `+260971234567`, `260971234567` or `971234567`. Anything else is refused with `422`, because ZRA discards a number it cannot use without reporting an error: the sale would succeed and the customer would never be entered. Whatever form you send is normalised to `0971234567` before it reaches ZRA, and that is the value read back on the sale as `customer.phone`.\n",
                                        "example": "0971234567"
                                    }
                                }
                            }
                        }
                    }
                ]
            },
            "NewCreditNote": {
                "description": "The goods coming back, described with the same fields that described them going out — everything `SaleBody` accepts (`paymentMethod`, `occurredAt`, `lpoNumber`) is accepted and acted on here too. `destinationCountryCode`, `currency` and `exchangeRate` are the exceptions: a credit note is an entry against the invoice it reverses, so it inherits all three and there is no need to resend them. Sending a `currency` or an `exchangeRate` that differs from the original's is refused with `422` rather than silently overridden — a note reverses the money the original moved, not the money that would move today. For the `items`: send fewer lines, or smaller quantities, for a partial refund; the total credited against one invoice — counting every credit note already raised against it — may not exceed what the invoice took.\n\nThere is deliberately no `customer`: a credit note names the customer of the invoice it reverses, and accepting a different one would credit one party for another party's purchase.\n",
                "allOf": [
                    {
                        "$ref": "#/components/schemas/SaleBody"
                    },
                    {
                        "type": "object",
                        "required": [
                            "reason"
                        ],
                        "properties": {
                            "reason": {
                                "type": "string",
                                "enum": [
                                    "01",
                                    "02",
                                    "03",
                                    "04",
                                    "05",
                                    "06",
                                    "07"
                                ],
                                "description": "ZRA's refund reason (rfdRsnCd): `01` wrong product, `02` wrong price, `03` damaged goods, `04` wrong customer, `05` duplicated invoice, `06` excess supplies, `07` other.\n\n**These are not the debit-note codes.** A debit note takes ZRA's separate four-code list — see `NewDebitNote`.\n",
                                "example": "03"
                            },
                            "note": {
                                "type": "string",
                                "maxLength": 200,
                                "description": "ZRA's `invcAdjustReason` — free text explaining the adjustment, worth sending with reason `07` (\"other\"). Printed on the note.\n",
                                "example": "Two of the six bags arrived split."
                            }
                        }
                    }
                ]
            },
            "NewDebitNote": {
                "description": "Value that should have been on the original invoice and was not — an undercharge, an omitted item, a wrong quantity. Sent to ZRA as rcptTyCd `D` against the original, so the additional VAT is declared instead of going unreported.\n\nEverything `SaleBody` accepts is accepted here too, on the same terms as a credit note: `destinationCountryCode`, `currency` and `exchangeRate` are inherited from the invoice being adjusted — sending a currency or rate that differs from the original's is refused with `422` — and there is deliberately no `customer`, because the note names the customer of the invoice it adjusts.\n\nUnlike a credit note there is **no cap**: a debit note adds value rather than returning it, so it is not bounded by the original's total.\n",
                "allOf": [
                    {
                        "$ref": "#/components/schemas/SaleBody"
                    },
                    {
                        "type": "object",
                        "required": [
                            "reason"
                        ],
                        "properties": {
                            "reason": {
                                "type": "string",
                                "enum": [
                                    "01",
                                    "02",
                                    "03",
                                    "04"
                                ],
                                "description": "ZRA's debit-note reason (dbtRsnCd), standard-code class 67: `01` wrong quantity invoiced, `02` wrong invoice amount, `03` omitted item, `04` other [specify].\n\n**Four codes, not the credit note's seven, and they are not interchangeable** — `04` is \"other\" here and \"wrong customer\" on a credit note. Sending a credit-note code is refused with 422 rather than passed through for ZRA to reject.\n",
                                "example": "03"
                            },
                            "note": {
                                "type": "string",
                                "maxLength": 200,
                                "description": "ZRA's `invcAdjustReason` — the \"[specify]\" half of reason `04`. Printed on the note.\n",
                                "example": "Freight omitted from the original invoice."
                            }
                        }
                    }
                ]
            },
            "NewSaleItem": {
                "type": "object",
                "required": [
                    "name",
                    "classificationCode",
                    "quantity",
                    "price"
                ],
                "properties": {
                    "name": {
                        "type": "string",
                        "maxLength": 200,
                        "example": "Maize meal 25kg"
                    },
                    "classificationCode": {
                        "type": "string",
                        "maxLength": 20,
                        "description": "ZRA's `itemClsCd` — the 8-digit code the line is taxed against, from `GET /api/v1/item-classes`. **Not** a customs tariff (HS) number: those are 10 digits and ZRA publishes none of them as classifications. Checked against ZRA's catalogue since 1.6.0; the 10-digit zero-padded form ZRA itself echoes back (`5022120400`) is accepted and normalised.\n",
                        "example": "50221204"
                    },
                    "vatCategory": {
                        "type": "string",
                        "default": "A",
                        "enum": [
                            "A",
                            "B",
                            "C",
                            "C1",
                            "C2",
                            "C3",
                            "D",
                            "E",
                            "RVAT"
                        ],
                        "description": "A = Standard rated (16%), B = Minimum taxable value (16%), C1 = Exports (0%, requires `destinationCountryCode` and may carry no other category), C2 = Zero-rating LPO (0%, requires `lpoNumber`), C3 = Zero-rated by nature (0%), D = Exempt, E = Disbursement, RVAT = Reverse VAT (16%). Codes are case-insensitive. Bare `C` is accepted for legacy callers and **normalised to C3**, so a sale sent as `C` comes back with a `C3` key in `taxBreakdown` and no `C` key. Anything outside this list is refused with 422 rather than defaulted — an unrecognised code used to bucket silently to Exempt, which is a misdeclaration, not a default.\n",
                        "example": "A"
                    },
                    "code": {
                        "type": "string",
                        "maxLength": 20,
                        "description": "Your own item code."
                    },
                    "quantity": {
                        "type": "number",
                        "exclusiveMinimum": 0,
                        "example": 3
                    },
                    "price": {
                        "type": "number",
                        "exclusiveMinimum": 0,
                        "description": "Unit price, VAT-inclusive unless `taxInclusive` is false.",
                        "example": 10
                    },
                    "taxInclusive": {
                        "type": "boolean",
                        "default": true,
                        "description": "Send false if you store net prices. The gateway grosses up in exact decimal arithmetic and reproduces your figures, rather than you rounding first and losing a ngwee to the conversion.\n"
                    },
                    "discount": {
                        "type": "number",
                        "minimum": 0,
                        "description": "ZRA's per-item discount (dcAmt), in the same convention as `price`. The line is worth `quantity × price − discount`. Send it whenever a line total is not `quantity × price` — a discounted total often cannot be reached by any unit price, and a fiscal invoice that disagrees with the receipt is the failure this field prevents. Must be less than the line's supply amount.\n",
                        "example": 2.1
                    }
                }
            },
            "Me": {
                "type": "object",
                "properties": {
                    "tenant": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "integer",
                                "example": 12
                            },
                            "name": {
                                "type": "string",
                                "example": "Katema Stores Ltd"
                            },
                            "tpin": {
                                "type": "string",
                                "example": "1002101584"
                            },
                            "status": {
                                "type": "string",
                                "description": "A suspended tenant is refused on every merchant route with 403.",
                                "example": "active"
                            }
                        }
                    },
                    "devices": {
                        "type": "array",
                        "description": "The branches this tenant can fiscalise through.",
                        "items": {
                            "type": "object",
                            "properties": {
                                "branch": {
                                    "type": "string",
                                    "description": "The bhfId. Pass it as X-Branch to fiscalise through this branch.",
                                    "example": "000"
                                },
                                "name": {
                                    "type": [
                                        "string",
                                        "null"
                                    ],
                                    "example": "Head Office"
                                },
                                "sdcId": {
                                    "type": [
                                        "string",
                                        "null"
                                    ],
                                    "example": "SDC0010000001"
                                },
                                "initialized": {
                                    "type": "boolean",
                                    "description": "False means this branch has not been initialised against ZRA and cannot fiscalise yet. Device initialisation is one-shot per device — coordinate serials with ZRA before initialising.\n",
                                    "example": true
                                },
                                "lastInvoiceNo": {
                                    "type": "integer",
                                    "description": "The last invoice number allocated on this device.",
                                    "example": 180
                                }
                            }
                        }
                    },
                    "activeBranch": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The branch this request resolved to, given the X-Branch header or the tenant default.",
                        "example": "000"
                    }
                }
            },
            "SaleEnvelope": {
                "type": "object",
                "properties": {
                    "data": {
                        "$ref": "#/components/schemas/Sale"
                    }
                }
            },
            "SaleCollection": {
                "type": "object",
                "description": "A paginated list of documents, in Laravel's collection envelope.",
                "properties": {
                    "data": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Sale"
                        }
                    },
                    "links": {
                        "$ref": "#/components/schemas/PaginationLinks"
                    },
                    "meta": {
                        "$ref": "#/components/schemas/PaginationMeta"
                    }
                }
            },
            "TenantTokenCollection": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "id": {
                                    "type": "integer",
                                    "description": "Pass this to the revoke endpoint.",
                                    "example": 7
                                },
                                "name": {
                                    "type": "string",
                                    "example": "Katema Stores — POS terminal 1"
                                },
                                "abilities": {
                                    "type": "array",
                                    "items": {
                                        "type": "string"
                                    },
                                    "example": [
                                        "invoices:read",
                                        "invoices:write"
                                    ]
                                },
                                "lastUsedAt": {
                                    "type": [
                                        "string",
                                        "null"
                                    ],
                                    "format": "date-time"
                                },
                                "createdAt": {
                                    "type": [
                                        "string",
                                        "null"
                                    ],
                                    "format": "date-time"
                                }
                            }
                        }
                    }
                }
            },
            "CreatedTenantToken": {
                "type": "object",
                "properties": {
                    "token": {
                        "type": "string",
                        "description": "The plaintext bearer token. Send it as `Authorization: Bearer <token>` on the merchant surface. **Shown once — it is not stored and cannot be retrieved again.**\n",
                        "example": "12|kJ8sdf9s0dfKJHsdf98sdfkjh23kjh423kjh4"
                    },
                    "tokenId": {
                        "type": "integer",
                        "description": "The id to revoke it by.",
                        "example": 7
                    },
                    "name": {
                        "type": "string",
                        "example": "Katema Stores — POS terminal 1"
                    },
                    "abilities": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "example": [
                            "invoices:read",
                            "invoices:write"
                        ]
                    },
                    "message": {
                        "type": "string",
                        "example": "Store this token now — this API does not return it again. The merchant can view it again in their developers area."
                    }
                }
            },
            "Sale": {
                "type": "object",
                "description": "A fiscalised document — a sale or a credit note — as the gateway holds it.\n\n**Check `status` before printing a receipt.** `uploaded` means ZRA has the document and every `fiscal.*` field is populated. `pending` means the document was accepted and queued because ZRA was unreachable: the fiscal fields are null, and the receipt must be marked provisional rather than printed as a fiscal receipt. It is uploaded automatically once connectivity returns, and the `invoice.fiscalised` webhook fires then. `failed` means ZRA rejected it — see `resultCode` and `lastError`.\n",
                "properties": {
                    "id": {
                        "type": "integer",
                        "description": "The gateway's own id for this document — what `GET /api/v1/sales/{sale}` and the credit-note path take.",
                        "example": 1042
                    },
                    "reference": {
                        "type": "string",
                        "description": "The document's reference: the `clientInvoiceNo` it was filed under, falling back to the gateway invoice number when there is none.\n\n**On `/api/v1` this is always the gateway invoice number**, because this surface has no `clientInvoiceNo` field to set — only a `/zm/api` document echoes its own `InvoiceNumber` here. To recover a lost response, search by the `Idempotency-Key` you sent (`GET /api/v1/sales?reference=<that key>`), not by the value returned in this field.\n",
                        "example": "424"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "uploaded",
                            "pending",
                            "failed"
                        ],
                        "description": "`uploaded` — ZRA has it. `pending` — queued offline, fiscal fields null. `failed` — rejected by ZRA.\n",
                        "example": "uploaded"
                    },
                    "invoiceNo": {
                        "type": "integer",
                        "description": "The invoice number allocated on this branch device. Sequential per device.",
                        "example": 180
                    },
                    "receiptNo": {
                        "type": [
                            "integer",
                            "null"
                        ],
                        "description": "ZRA's receipt number for this document, the second half of the printed `INV.../<receiptNo>` form. Null until ZRA has the document, so it is null on every `pending` and `failed` sale. It counts separately from `invoiceNo` and the two diverge.\n",
                        "example": 180
                    },
                    "type": {
                        "type": "string",
                        "enum": [
                            "sale",
                            "credit-note",
                            "debit-note"
                        ],
                        "description": "A client that shows a credit note as a sale reports a refund as revenue, and one that shows a debit note as a sale double-counts the original.\n",
                        "example": "sale"
                    },
                    "originalInvoiceNo": {
                        "type": [
                            "integer",
                            "null"
                        ],
                        "description": "The invoice this credit note reverses. Null on a sale.",
                        "example": null
                    },
                    "branch": {
                        "type": "string",
                        "description": "The bhfId this was fiscalised through.",
                        "example": "000"
                    },
                    "customer": {
                        "type": "object",
                        "properties": {
                            "name": {
                                "type": "string",
                                "example": "Katema Stores Ltd"
                            },
                            "tpin": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "example": "2002200000"
                            },
                            "address": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The customer's address as printed on the tax invoice. Never sent to ZRA — its saveSales DTO has no address field — but required on the face of the document.\n",
                                "example": "Plot 24, Cairo Road, Lusaka"
                            },
                            "phone": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The number this sale was entered in ZRA's reward draw under, normalised to the national form ZRA holds it in. A sale filed with `+260971234567` reads back as `0971234567`, because that is the number the draw was actually entered under. Null when the sale carried none.\n",
                                "example": "0971234567"
                            }
                        }
                    },
                    "lpoNumber": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The Local Purchase Order number this invoice was zero-rated under. Present only on a C2 invoice.\n",
                        "example": null
                    },
                    "destinationCountryCode": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Where an export was sold to, ISO 3166-1 alpha-2. Present only on a C1 (export) invoice, and mandatory on one.\n",
                        "example": null
                    },
                    "currency": {
                        "type": "string",
                        "enum": [
                            "ZMW",
                            "USD",
                            "GBP",
                            "EUR",
                            "CNY",
                            "ZAR"
                        ],
                        "description": "The currency this invoice was filed in. ZRA Smart Invoice accepts these six; a rate other than 1 accompanies any of them but ZMW.\n",
                        "example": "ZMW"
                    },
                    "exchangeRate": {
                        "type": "number",
                        "description": "How many kwacha one unit of `currency` buys, as filed with ZRA. Always 1 for ZMW, and never 1 for any other currency.\n",
                        "example": 1
                    },
                    "totals": {
                        "type": "object",
                        "properties": {
                            "taxable": {
                                "type": "number",
                                "description": "Net of VAT.",
                                "example": 603.45
                            },
                            "vat": {
                                "type": "number",
                                "example": 96.55
                            },
                            "total": {
                                "type": "number",
                                "description": "VAT-inclusive total.",
                                "example": 700
                            }
                        }
                    },
                    "taxBreakdown": {
                        "type": "object",
                        "description": "The VAT split by tax category, keyed by category code. A receipt has to show tax per category, not just a single total.\n\n**All eight header categories are always present**, including the ones this sale did not use — those read `{taxbl: 0, tax: 0, rate: 0}`. ZRA's VSDC reconciles every header category against the item lines and refuses the sale on a mismatch, so the gateway sends all eight and stores what it sent. Do not read the key set as \"the categories on this invoice\": filter on `taxbl > 0` for that, or iterate the lines. Note also that a zero bucket reports `rate: 0` rather than the category's real rate, so an unused `A` bucket is not a 16% row.\n\nA sale sent as bare `C` appears under `C3` — see `vatCategory`.\n",
                        "propertyNames": {
                            "enum": [
                                "A",
                                "B",
                                "C1",
                                "C2",
                                "C3",
                                "D",
                                "RVAT",
                                "E"
                            ]
                        },
                        "additionalProperties": {
                            "type": "object",
                            "properties": {
                                "taxbl": {
                                    "type": "number",
                                    "description": "Taxable amount in this category, net of VAT."
                                },
                                "tax": {
                                    "type": "number",
                                    "description": "VAT charged in this category."
                                },
                                "rate": {
                                    "type": "integer",
                                    "description": "The rate applied to this category on this sale, as a percentage. 0 on an unused category."
                                }
                            }
                        },
                        "example": {
                            "A": {
                                "taxbl": 603.45,
                                "tax": 96.55,
                                "rate": 16
                            },
                            "B": {
                                "taxbl": 0,
                                "tax": 0,
                                "rate": 0
                            },
                            "C1": {
                                "taxbl": 0,
                                "tax": 0,
                                "rate": 0
                            },
                            "C2": {
                                "taxbl": 0,
                                "tax": 0,
                                "rate": 0
                            },
                            "C3": {
                                "taxbl": 0,
                                "tax": 0,
                                "rate": 0
                            },
                            "D": {
                                "taxbl": 0,
                                "tax": 0,
                                "rate": 0
                            },
                            "RVAT": {
                                "taxbl": 0,
                                "tax": 0,
                                "rate": 0
                            },
                            "E": {
                                "taxbl": 0,
                                "tax": 0,
                                "rate": 0
                            }
                        }
                    },
                    "fiscal": {
                        "type": "object",
                        "description": "What ZRA returned, and what a compliant receipt must print. **Every field here is null while `status` is `pending`** — they are filled in when the queued document uploads.\n",
                        "properties": {
                            "sdcId": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The signing device id. Print it on the receipt.",
                                "example": "SDC0010000001"
                            },
                            "mrcNo": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The device MRC number. Print it on the receipt.",
                                "example": "WIS00000001"
                            },
                            "internalData": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "ZRA internal data block. Print it on the receipt, grouped as ZRA formats it.",
                                "example": "HFSD-KJHG-2SDF-9812"
                            },
                            "receiptSignature": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The fiscal signature. Print it on the receipt — this is what makes it fiscal.",
                                "example": "KJH2-8DJF-2LKD-9982"
                            },
                            "vsdcDate": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The timestamp ZRA stamped, in ZRA's own yyyyMMddHHmmss form. Not ISO 8601.",
                                "example": "20260901143000"
                            },
                            "qrCodeUrl": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "A URL to **render as a QR code** on the receipt — not to print as text. It resolves to ZRA's verification page for this invoice.\n",
                                "example": "https://api-sandbox.zra.org.zm/verify/..."
                            },
                            "rewardMessage": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "ZRA's invoice-lottery message, printed verbatim on the receipt. **Null on almost every invoice** — it is populated only when ZRA's draw awards this one a prize, and only while ZRA has the draw switched on. Do not synthesise or translate it: the wording is ZRA's and it is how the customer learns what they have won.\n",
                                "example": null
                            }
                        }
                    },
                    "resultCode": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "ZRA's own result code, unparsed. `000` is success. Null while the document is still queued. Anything else is a rejection — `lastError` carries the message.\n",
                        "example": "000"
                    },
                    "lastError": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The reason the last upload attempt failed, if it did.",
                        "example": null
                    },
                    "createdAt": {
                        "type": "string",
                        "format": "date-time",
                        "description": "When the gateway accepted the document — **not** when ZRA fiscalised it. On a sale queued offline the two are hours apart; read `fiscal.vsdcDate` for ZRA's own timestamp.\n",
                        "example": "2026-09-01T14:30:00+02:00"
                    }
                }
            },
            "NewCustomer": {
                "type": "object",
                "required": [
                    "name"
                ],
                "description": "A customer to store, and — if `tpin` is present — to register with ZRA.\n\nField lengths are ZRA's own, so a value too long for ZRA is refused here while somebody is still typing it rather than eight hours later inside a queued job.\n",
                "properties": {
                    "name": {
                        "type": "string",
                        "maxLength": 60,
                        "description": "The customer's name, as the merchant trades with them. Required — a customer record exists to name someone — and it is the name ZRA refuses a registration without.\n",
                        "example": "Katema Stores Ltd"
                    },
                    "tpin": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The customer's ZRA TPIN: exactly ten digits, or omitted.\n\n**Omit it for a walk-in.** Most retail customers have no TPIN, the record is perfectly valid without one, and it will simply never be transmitted to ZRA. Never send a placeholder: whatever is stored here is printed on that customer's tax invoices.\n\nThe VSDC's own request class narrows a TPIN further to `^[1-2]\\d{9}$`. This gateway does not enforce that — a rule stricter than ZRA's would cost a merchant a sale if ZRA ever issued outside it — so pre-check it yourself if you want to fail earlier than the registration does.\n",
                        "example": "2002200000"
                    },
                    "customerNo": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "minLength": 10,
                        "maxLength": 10,
                        "description": "ZRA's `custNo`. Their specification describes it as the customer's mobile number; merchants generally use it as their own account reference.\n\n**Required whenever `tpin` is present**, because Smart Invoice refuses a registration without it — `[<custNo> : may not be empty]`. Optional on a customer with no TPIN, which is never registered.\n\n**Exactly 10 characters** when given. Smart Invoice answers `[<custNo> : length must be between 10 and 10]` to anything else, and means it — 9 and 11 are refused alike. A Zambian mobile number is 10 digits, which is presumably where the length comes from; the check is on length only, so 10 letters pass.\n",
                        "example": "0977123456"
                    },
                    "type": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "maxLength": 5,
                        "description": "ZRA's customer type (standard code class 20): `01` Resident, `02` Non-resident. Validated against the synced code table when that table has been loaded, and left alone when it has not, so a fresh install cannot refuse every value.\n",
                        "example": "01"
                    },
                    "address": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "maxLength": 300,
                        "description": "The customer's address.",
                        "example": "Plot 24, Cairo Road, Lusaka"
                    },
                    "phone": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "maxLength": 20,
                        "description": "A contact telephone number. ZRA's `telNo`, and distinct from `customerNo`.",
                        "example": "0211123456"
                    },
                    "email": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "maxLength": 50,
                        "description": "The customer's email address.",
                        "example": "accounts@katema.example"
                    },
                    "remark": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "maxLength": 1000,
                        "description": "A free-text note carried to ZRA with the registration. Capped at 1000 here because that is the stored width, though the VSDC's own class allows 2000.\n",
                        "example": "Credit account, 30 days"
                    },
                    "active": {
                        "type": "boolean",
                        "default": true,
                        "description": "ZRA's `useYn`. Defaults to true, which is what creating a customer means; send false only to carry a closed account across from another system without it arriving open.\n",
                        "example": true
                    },
                    "verify": {
                        "type": "boolean",
                        "default": false,
                        "description": "Look the TPIN up at Smart Invoice before answering, and report the outcome in `tpinCheck`. Off by default because it is a synchronous call to a box that can be slow or unreachable.\n\nIt is a lookup and nothing more: it never changes whether the customer is created or queued for registration, and a lookup that fails does not fail the request.\n",
                        "example": false
                    }
                }
            },
            "Customer": {
                "type": "object",
                "description": "A customer as the gateway holds it, plus what ZRA has said about it.\n\n**`verification` and `registration` are separate facts and fail independently.** `verification` is the answer to a TPIN lookup — does ZRA recognise this taxpayer. `registration` is whether ZRA accepted the customer onto this branch's list. A customer can be verified and never registered, or registered and never verified.\n",
                "properties": {
                    "id": {
                        "type": "integer",
                        "description": "The gateway's own id for this customer — what `GET /api/v1/customers/{customer}` takes.",
                        "example": 91
                    },
                    "branch": {
                        "type": "string",
                        "description": "The bhfId whose customer list this row belongs to. ZRA holds customers per branch, so one taxpayer may appear once under each branch a merchant trades from.\n",
                        "example": "000"
                    },
                    "name": {
                        "type": "string",
                        "description": "The name the merchant trades with this customer under. Compare `verification.registeredName`.",
                        "example": "Katema Stores Ltd"
                    },
                    "tpin": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The customer's ZRA TPIN, or null for a walk-in.",
                        "example": "2002200000"
                    },
                    "customerNo": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "ZRA's `custNo` — the merchant's own reference for this customer.",
                        "example": "0977123456"
                    },
                    "type": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "ZRA's customer type code (class 20). Null when none was given.",
                        "example": "01"
                    },
                    "address": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The customer's address.",
                        "example": "Plot 24, Cairo Road, Lusaka"
                    },
                    "phone": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The contact telephone number (ZRA's `telNo`).",
                        "example": "0211123456"
                    },
                    "email": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The customer's email address.",
                        "example": "accounts@katema.example"
                    },
                    "remark": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The free-text note stored against this customer.",
                        "example": null
                    },
                    "active": {
                        "type": "boolean",
                        "description": "ZRA's `useYn`, as a boolean. False is a closed account.",
                        "example": true
                    },
                    "verification": {
                        "type": "object",
                        "properties": {
                            "state": {
                                "type": "string",
                                "enum": [
                                    "unchecked",
                                    "known",
                                    "unknown",
                                    "refused"
                                ],
                                "description": "The lookup reads **this branch's saved customer book on Smart Invoice**, not ZRA's taxpayer register: a TPIN answers `unknown` until the branch has registered a customer under it, and `known` afterwards — including a TPIN ZRA issued and knows perfectly well. Do not present these states to a user as ZRA confirming or denying a taxpayer.\n\n`unchecked` — nobody has looked this TPIN up yet, or there is no TPIN to look up. `known` — Smart Invoice holds a customer under it, and `registeredName` is the name it holds. `unknown` — Smart Invoice answered and holds nothing under it; a verdict, not an error. `refused` — Smart Invoice answered with something else, for example that the TPIN is malformed; `resultCode` says which.\n",
                                "example": "unchecked"
                            },
                            "resultCode": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "Smart Invoice's own result code from the lookup, unparsed — `000` a customer is held under this TPIN, `001` none is. Null until a lookup has been made. See `/docs/result-codes`.\n",
                                "example": null
                            },
                            "registeredName": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The name ZRA holds against this TPIN, which is not necessarily `name` above and is deliberately never written over it. A customer filed as one business against a TPIN registered to another is what this field exists to make visible.\n",
                                "example": null
                            },
                            "checkedAt": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "format": "date-time",
                                "description": "When ZRA last answered about this TPIN, either way.",
                                "example": null
                            }
                        }
                    },
                    "registration": {
                        "type": "object",
                        "properties": {
                            "state": {
                                "type": "string",
                                "enum": [
                                    "local-only",
                                    "pending",
                                    "registered",
                                    "failed"
                                ],
                                "description": "`local-only` — no TPIN, so ZRA has nothing to register this against and it never will be sent. Do not poll one of these. `pending` — queued, and being retried with backoff for roughly eight hours. `registered` — ZRA accepted it. `failed` — out of attempts or refused on its data; `lastError` says which, and it needs a person.\n",
                                "example": "pending"
                            },
                            "attempts": {
                                "type": "integer",
                                "description": "How many transmission attempts have been made. Zero until the first one runs.",
                                "example": 0
                            },
                            "lastError": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "Why the last attempt did not land, carrying ZRA's result code where the verdict was ZRA's. Null while nothing has failed.\n",
                                "example": null
                            },
                            "registeredAt": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "format": "date-time",
                                "description": "When ZRA accepted the registration. Null means it has not — never merely that we tried.",
                                "example": null
                            }
                        }
                    },
                    "createdAt": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time",
                        "description": "When the customer was created in the gateway.",
                        "example": "2026-09-08T09:14:22+00:00"
                    }
                }
            },
            "CustomerEnvelope": {
                "type": "object",
                "properties": {
                    "data": {
                        "$ref": "#/components/schemas/Customer"
                    }
                }
            },
            "CreatedCustomer": {
                "type": "object",
                "description": "The customer as stored, plus — when the request asked for one — what the TPIN lookup answered.\n",
                "properties": {
                    "data": {
                        "$ref": "#/components/schemas/Customer"
                    },
                    "tpinCheck": {
                        "type": "object",
                        "description": "Present only when `verify: true` was sent. Distinct from `data.verification`, which is what was *stored*: only a verdict is written to the customer, so a lookup that was refused or never answered leaves `data.verification.state` reading `unchecked`, and this object is how you tell those apart.\n",
                        "properties": {
                            "status": {
                                "type": "string",
                                "enum": [
                                    "known",
                                    "unknown",
                                    "rejected",
                                    "unavailable"
                                ],
                                "description": "`known` — Smart Invoice holds a customer under this TPIN, and `data.verification.registeredName` is the name it holds. `unknown` — it holds none; an answer, not a failure. `rejected` — it refused the question, so the value is not a usable TPIN and will be refused identically in an hour; change it. `unavailable` — nobody answered, or the device cannot; we do not know, so ask again later.\n\n`unknown` and `unavailable` are deliberately not the same word: a merchant told \"not registered\" during an outage will retype a TPIN that was right all along.\n",
                                "example": "known"
                            },
                            "resultCode": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "Smart Invoice's own result code for the lookup. Null when no call was made or nothing answered.\n",
                                "example": "000"
                            },
                            "message": {
                                "type": "string",
                                "description": "The outcome as a sentence, fit to show a person.",
                                "example": "Smart Invoice holds TPIN 2002200000 as \"KATEMA STORES LIMITED\"."
                            }
                        }
                    }
                }
            },
            "Item": {
                "type": "object",
                "description": "A catalogue item, and what ZRA has been told about it.",
                "properties": {
                    "id": {
                        "type": "integer",
                        "example": 42
                    },
                    "branch": {
                        "type": "string",
                        "example": "000",
                        "description": "Which branch's item master this row belongs to. ZRA holds items per branch, so the same product carried by two branches is two rows with two codes.\n"
                    },
                    "itemCode": {
                        "type": "string",
                        "example": "ZM2NTBA0000012"
                    },
                    "name": {
                        "type": "string",
                        "example": "Sugar 1kg"
                    },
                    "standardName": {
                        "type": "string",
                        "nullable": true
                    },
                    "classificationCode": {
                        "type": "string",
                        "example": "50221204",
                        "description": "ZRA's itemClsCd — what the item is taxed against."
                    },
                    "itemTypeCode": {
                        "type": "string",
                        "example": "2",
                        "description": "1 raw material, 2 finished product, 3 service, 4 rebate."
                    },
                    "originCountryCode": {
                        "type": "string",
                        "example": "ZM"
                    },
                    "packagingUnitCode": {
                        "type": "string",
                        "example": "NT"
                    },
                    "quantityUnitCode": {
                        "type": "string",
                        "example": "BA"
                    },
                    "taxCode": {
                        "type": "string",
                        "example": "A"
                    },
                    "defaultPrice": {
                        "type": "string",
                        "example": "32.5000"
                    },
                    "recommendedPrice": {
                        "type": "string",
                        "nullable": true
                    },
                    "safetyQuantity": {
                        "type": "string",
                        "nullable": true
                    },
                    "barcode": {
                        "type": "string",
                        "nullable": true
                    },
                    "batchNumber": {
                        "type": "string",
                        "nullable": true
                    },
                    "additionalInfo": {
                        "type": "string",
                        "nullable": true
                    },
                    "manufacturerTpin": {
                        "type": "string",
                        "nullable": true
                    },
                    "manufacturerItemCode": {
                        "type": "string",
                        "nullable": true
                    },
                    "active": {
                        "type": "boolean"
                    },
                    "rental": {
                        "type": "boolean"
                    },
                    "serviceCharge": {
                        "type": "boolean"
                    },
                    "movesStock": {
                        "type": "boolean",
                        "description": "Whether selling this item moves stock. False for a service and for a rebate — neither is a countable thing — which is the exemption ZRA marks \"MANDATORY EXCEPT SERVICE INDUSTRY\", answered per item rather than per merchant so that a business selling both is right on both.\n"
                    },
                    "registration": {
                        "type": "object",
                        "properties": {
                            "state": {
                                "type": "string",
                                "enum": [
                                    "draft",
                                    "pending",
                                    "registered",
                                    "failed"
                                ],
                                "description": "`draft` means the gateway created this row and has NOT sent it — see the Items tag. `pending` is queued, `registered` is accepted by ZRA, `failed` needs a person.\n"
                            },
                            "attempts": {
                                "type": "integer"
                            },
                            "error": {
                                "type": "string",
                                "nullable": true
                            },
                            "registeredAt": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                            },
                            "draftSource": {
                                "type": "string",
                                "nullable": true,
                                "enum": [
                                    "sale",
                                    "backfill"
                                ],
                                "description": "Why this row exists, when nobody created it on purpose. `sale` means a till sold something the catalogue did not hold; `backfill` means it came from historic sales. Null for an item a person entered.\n"
                            }
                        }
                    },
                    "createdAt": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "updatedAt": {
                        "type": "string",
                        "format": "date-time"
                    }
                }
            },
            "ItemRequest": {
                "type": "object",
                "required": [
                    "name",
                    "classificationCode",
                    "defaultPrice"
                ],
                "properties": {
                    "name": {
                        "type": "string",
                        "maxLength": 200,
                        "example": "Sugar 1kg"
                    },
                    "classificationCode": {
                        "type": "string",
                        "maxLength": 20,
                        "example": "50221204",
                        "description": "Required, and never defaulted — it is what ZRA taxes the item against. One of ZRA's published 8-digit codes, checked against the catalogue since 1.6.0.\n"
                    },
                    "defaultPrice": {
                        "type": "number",
                        "minimum": 0,
                        "example": 32.5
                    },
                    "itemCode": {
                        "type": "string",
                        "maxLength": 20,
                        "description": "Yours if you send it and it is unique within the branch; generated in ZRA's section 6.16 form if you do not. A code already in use answers 409 rather than quietly becoming a different one.\n"
                    },
                    "draft": {
                        "type": "boolean",
                        "description": "Hold the item back instead of registering it. What a bulk import wants — nothing is sent until a person releases it.\n"
                    },
                    "itemTypeCode": {
                        "type": "string",
                        "example": "2"
                    },
                    "originCountryCode": {
                        "type": "string",
                        "example": "ZM"
                    },
                    "packagingUnitCode": {
                        "type": "string",
                        "example": "NT"
                    },
                    "quantityUnitCode": {
                        "type": "string",
                        "example": "BA"
                    },
                    "taxCode": {
                        "type": "string",
                        "example": "A"
                    },
                    "standardName": {
                        "type": "string",
                        "maxLength": 200
                    },
                    "barcode": {
                        "type": "string",
                        "maxLength": 20
                    },
                    "batchNumber": {
                        "type": "string",
                        "maxLength": 20
                    },
                    "additionalInfo": {
                        "type": "string",
                        "maxLength": 500
                    },
                    "recommendedPrice": {
                        "type": "number",
                        "minimum": 0
                    },
                    "safetyQuantity": {
                        "type": "number",
                        "minimum": 0
                    },
                    "manufacturerTpin": {
                        "type": "string",
                        "example": "1002101584"
                    },
                    "manufacturerItemCode": {
                        "type": "string",
                        "maxLength": 20
                    },
                    "active": {
                        "type": "boolean"
                    },
                    "rental": {
                        "type": "boolean"
                    },
                    "serviceCharge": {
                        "type": "boolean"
                    }
                }
            },
            "ItemCollection": {
                "type": "object",
                "description": "A paginated list of items, in Laravel's collection envelope.",
                "properties": {
                    "data": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Item"
                        }
                    },
                    "links": {
                        "$ref": "#/components/schemas/PaginationLinks"
                    },
                    "meta": {
                        "$ref": "#/components/schemas/PaginationMeta"
                    }
                }
            },
            "ImportItem": {
                "type": "object",
                "description": "One line of one customs declaration, as the gateway holds it. `decision` is what the merchant said; `filing` is whether ZRA has accepted it; `stockPostedAt` is whether the goods reached the shelf. Three facts that move independently.\n",
                "properties": {
                    "id": {
                        "type": "integer",
                        "example": 42
                    },
                    "branch": {
                        "type": "string",
                        "example": "000"
                    },
                    "declaration": {
                        "type": "object",
                        "properties": {
                            "taskCode": {
                                "type": "string",
                                "description": "ZRA's task code. With `date`, this is what identifies a declaration.",
                                "example": "2400001"
                            },
                            "date": {
                                "type": "string",
                                "description": "Declaration date, yyyyMMdd — exactly eight characters.",
                                "example": "20260901"
                            },
                            "number": {
                                "type": "string",
                                "nullable": true,
                                "example": "C40001"
                            },
                            "reference": {
                                "type": "string",
                                "nullable": true,
                                "example": "REF-1"
                            },
                            "lineNumber": {
                                "type": "integer",
                                "example": 1
                            }
                        }
                    },
                    "goods": {
                        "type": "object",
                        "properties": {
                            "description": {
                                "type": "string",
                                "nullable": true,
                                "example": "Imported widget"
                            },
                            "hsCode": {
                                "type": "string",
                                "description": "A customs tariff heading. **Not** an `itemClassCode` — that is what ZRA taxes against, and there is no derivation from one to the other.\n",
                                "example": "8471300000"
                            },
                            "originCountry": {
                                "type": "string",
                                "nullable": true,
                                "example": "CN"
                            },
                            "exportCountry": {
                                "type": "string",
                                "nullable": true,
                                "example": "CN"
                            },
                            "quantity": {
                                "type": "number",
                                "format": "float",
                                "example": 10
                            },
                            "quantityUnit": {
                                "type": "string",
                                "nullable": true,
                                "example": "U"
                            },
                            "packages": {
                                "type": "number",
                                "format": "float",
                                "example": 2
                            },
                            "packageUnit": {
                                "type": "string",
                                "nullable": true,
                                "example": "CTN"
                            },
                            "grossWeight": {
                                "type": "number",
                                "format": "float",
                                "nullable": true,
                                "example": 55
                            },
                            "netWeight": {
                                "type": "number",
                                "format": "float",
                                "nullable": true,
                                "example": 50
                            },
                            "supplier": {
                                "type": "string",
                                "nullable": true
                            },
                            "agent": {
                                "type": "string",
                                "nullable": true,
                                "description": "The clearing agent."
                            }
                        }
                    },
                    "value": {
                        "type": "object",
                        "description": "What customs assessed, in the invoice currency and at the rate they applied. `kwacha` is their product — not a conversion at today's rate, because the declaration is the evidence.\n",
                        "properties": {
                            "amount": {
                                "type": "number",
                                "format": "float",
                                "nullable": true,
                                "example": 400
                            },
                            "currency": {
                                "type": "string",
                                "nullable": true,
                                "example": "USD"
                            },
                            "exchangeRate": {
                                "type": "number",
                                "format": "float",
                                "nullable": true,
                                "example": 25
                            },
                            "kwacha": {
                                "type": "number",
                                "format": "float",
                                "nullable": true,
                                "example": 10000
                            }
                        }
                    },
                    "catalogue": {
                        "type": "object",
                        "description": "What the merchant calls these goods. Null until somebody matches the line, and an approval cannot be filed without both codes.\n",
                        "properties": {
                            "itemCode": {
                                "type": "string",
                                "nullable": true,
                                "example": "WIDGET1"
                            },
                            "itemClassCode": {
                                "type": "string",
                                "nullable": true,
                                "example": "50161509"
                            },
                            "matched": {
                                "type": "boolean"
                            }
                        }
                    },
                    "decision": {
                        "type": "object",
                        "properties": {
                            "statusCode": {
                                "type": "string",
                                "enum": [
                                    "1",
                                    "2",
                                    "3",
                                    "4"
                                ],
                                "description": "ZRA's class-26 code, unparsed."
                            },
                            "status": {
                                "type": "string",
                                "enum": [
                                    "unsent",
                                    "awaiting-decision",
                                    "approved",
                                    "rejected",
                                    "unknown"
                                ]
                            },
                            "remark": {
                                "type": "string",
                                "nullable": true
                            },
                            "decidedAt": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                            }
                        }
                    },
                    "filing": {
                        "type": "object",
                        "properties": {
                            "state": {
                                "type": "string",
                                "enum": [
                                    "not-answered",
                                    "sending",
                                    "filed",
                                    "failed"
                                ],
                                "description": "Whether ZRA has accepted the decision. `not-answered` means the merchant has not decided yet, so there is nothing to file.\n"
                            },
                            "attempts": {
                                "type": "integer"
                            },
                            "error": {
                                "type": "string",
                                "nullable": true
                            },
                            "filedAt": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                            }
                        }
                    },
                    "stockPostedAt": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true,
                        "description": "When the goods reached the shelf. Null on an approved line means it was filed with ZRA before it was matched to the catalogue — the movement follows as soon as it is.\n"
                    }
                }
            },
            "ImportItemEnvelope": {
                "type": "object",
                "properties": {
                    "data": {
                        "$ref": "#/components/schemas/ImportItem"
                    }
                }
            },
            "ImportItemCollection": {
                "type": "object",
                "description": "A paginated list of declaration lines, in Laravel's collection envelope.",
                "properties": {
                    "data": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/ImportItem"
                        }
                    },
                    "links": {
                        "$ref": "#/components/schemas/PaginationLinks"
                    },
                    "meta": {
                        "$ref": "#/components/schemas/PaginationMeta"
                    }
                }
            },
            "CustomerCollection": {
                "type": "object",
                "description": "A paginated list of customers, in Laravel's collection envelope.",
                "properties": {
                    "data": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Customer"
                        }
                    },
                    "links": {
                        "$ref": "#/components/schemas/PaginationLinks"
                    },
                    "meta": {
                        "$ref": "#/components/schemas/PaginationMeta"
                    }
                }
            },
            "ItemClassCollection": {
                "type": "object",
                "description": "Paginated. `meta.total` is the size of the whole filtered catalogue, not of this page.",
                "properties": {
                    "data": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "code": {
                                    "type": "string",
                                    "example": "50221204"
                                },
                                "name": {
                                    "type": "string",
                                    "example": "Bread"
                                },
                                "level": {
                                    "type": [
                                        "integer",
                                        "null"
                                    ],
                                    "example": 5
                                },
                                "taxCategory": {
                                    "type": [
                                        "string",
                                        "null"
                                    ],
                                    "example": "A"
                                }
                            }
                        }
                    },
                    "links": {
                        "$ref": "#/components/schemas/PaginationLinks"
                    },
                    "meta": {
                        "$ref": "#/components/schemas/PaginationMeta"
                    }
                }
            },
            "NewTenant": {
                "type": "object",
                "required": [
                    "name",
                    "tpin"
                ],
                "properties": {
                    "name": {
                        "type": "string",
                        "maxLength": 255,
                        "description": "The merchant's trading name, for your own listings. Not sent to ZRA.",
                        "example": "Katema Stores Ltd"
                    },
                    "tpin": {
                        "type": "string",
                        "pattern": "^[0-9]{10}$",
                        "description": "The merchant's ZRA TPIN — **exactly 10 digits**, no spaces or dashes, sent as a string so a leading zero survives.\n\nIt is **unique across the whole platform, not just your book**: a TPIN already provisioned by another partner is refused with 422, and the message does not say who holds it. A merchant can therefore only be onboarded once, by one partner. If a TPIN you expect to be free is refused, that merchant is already live with somebody — resolve it with us rather than filing under a different number.\n\nThis is also the identity every invoice for this tenant is filed under, and there is no endpoint that changes it. A typo here means deleting nothing and provisioning again.\n",
                        "example": "2002200000"
                    },
                    "contactEmail": {
                        "type": "string",
                        "format": "email",
                        "maxLength": 255,
                        "description": "Where billing and dunning mail for this merchant goes."
                    },
                    "contactPhone": {
                        "type": "string",
                        "maxLength": 30
                    },
                    "defaultBranch": {
                        "type": "string",
                        "maxLength": 3,
                        "description": "The bhfId a tenant-level credential falls back to. It is a label only — it does **not** register the branch. You still have to `POST …/devices` and initialise it.\n",
                        "example": "000"
                    }
                }
            },
            "Tenant": {
                "type": "object",
                "description": "A merchant on your book. Created by `POST /api/v1/tenants` and not yet able to trade — a tenant needs an initialised device and a merchant token before its first sale.\n",
                "properties": {
                    "id": {
                        "type": "integer",
                        "description": "The id every tenant-scoped endpoint takes in its path.",
                        "example": 91
                    },
                    "name": {
                        "type": "string",
                        "description": "The merchant's trading name, as you supplied it.",
                        "example": "Katema Stores Ltd"
                    },
                    "tpin": {
                        "type": "string",
                        "description": "The merchant's ZRA TPIN. Every invoice for this tenant files under it, and it cannot be changed.",
                        "example": "2002200000"
                    },
                    "defaultBranch": {
                        "type": "string",
                        "description": "The bhfId a tenant-level credential falls back to. A label, not a registered device.",
                        "example": "000"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "active",
                            "suspended"
                        ],
                        "description": "`suspended` stops this merchant fiscalising on every surface — 403 on the merchant API, 402 on `/zm/api`. Nothing you can set through this API; it follows the merchant's account standing.\n"
                    },
                    "contactEmail": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Where this merchant's billing and dunning mail goes."
                    },
                    "contactPhone": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Contact number for the merchant."
                    },
                    "createdAt": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time",
                        "description": "When the tenant was provisioned."
                    },
                    "devicesCount": {
                        "type": "integer",
                        "description": "Present only where the collection was counted — on the list endpoint. Absent from the response to `POST /api/v1/tenants`.\n"
                    },
                    "devices": {
                        "type": "array",
                        "description": "The tenant's branches. Populated by `GET /api/v1/tenants/{tenant}` only; absent on the list and create responses. This is the call to use when checking which branches have been initialised.\n",
                        "items": {
                            "$ref": "#/components/schemas/Device"
                        }
                    }
                }
            },
            "TenantEnvelope": {
                "type": "object",
                "properties": {
                    "data": {
                        "$ref": "#/components/schemas/Tenant"
                    }
                }
            },
            "TenantCollection": {
                "type": "object",
                "description": "Paginated. Read `meta.total` before assuming one page is every merchant — a provisioning sweep that stops at 20 silently misses the rest.",
                "properties": {
                    "data": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Tenant"
                        }
                    },
                    "links": {
                        "$ref": "#/components/schemas/PaginationLinks"
                    },
                    "meta": {
                        "$ref": "#/components/schemas/PaginationMeta"
                    }
                }
            },
            "NewDevice": {
                "type": "object",
                "required": [
                    "branch",
                    "deviceSerial"
                ],
                "properties": {
                    "branch": {
                        "type": "string",
                        "maxLength": 3,
                        "description": "ZRA's bhfId for this branch — headquarters is `000`. Unique within the tenant: re-posting one returns 422 `branch_exists` rather than updating it.\n",
                        "example": "000"
                    },
                    "deviceSerial": {
                        "type": "string",
                        "maxLength": 100,
                        "description": "The serial this branch initialises against at ZRA. Give every branch its own — ZRA hands out the initialisation payload once per serial, and a serial already installed elsewhere cannot be initialised here.\n"
                    },
                    "branchName": {
                        "type": "string",
                        "maxLength": 100,
                        "description": "Your label for the branch. Overwritten by ZRA's own bhfNm at initialisation."
                    },
                    "vsdcBaseUrl": {
                        "type": "string",
                        "format": "uri",
                        "maxLength": 255,
                        "description": "The VSDC this branch talks to. Defaults to the platform's configured VSDC, which is what almost every merchant wants.\n\n**Set at registration and never afterwards** — there is no device update endpoint, and re-posting the branch returns 422 `branch_exists` rather than editing it. Changing it later means a support request, so get it right on the first call — and if you are unsure, omit it and take the platform default.\n"
                    }
                }
            },
            "Device": {
                "type": "object",
                "properties": {
                    "branch": {
                        "type": "string",
                        "description": "ZRA's bhfId. This is the value a sale's `branch` field must carry.",
                        "example": "000"
                    },
                    "name": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The branch name — ZRA's own after initialisation, otherwise the `branchName` you registered."
                    },
                    "deviceSerial": {
                        "type": "string",
                        "description": "The serial this branch initialised against at ZRA. One per branch — ZRA hands out the initialisation payload once per serial."
                    },
                    "sdcId": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "ZRA's device id, captured at initialisation and printed on every receipt. `null` until then — and it is the `X-SdcId` a `/zm/api` caller pairs with its `pk_…` key.\n",
                        "example": "SDC0010000001"
                    },
                    "vsdcBaseUrl": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "uri",
                        "description": "The VSDC this branch files through. Fixed at registration."
                    },
                    "initialized": {
                        "type": "boolean",
                        "description": "**Check this before minting a credential.** A branch that is registered but not initialised has no SDC id and cannot fiscalise; the sale fails at file time, not at mint time.\n"
                    },
                    "initializedAt": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time",
                        "description": "When the ZRA handshake succeeded."
                    },
                    "lastInvoiceNo": {
                        "type": "integer",
                        "description": "The last invoice number consumed on this branch. Sequential per branch, assigned by us — never sent by the caller."
                    }
                }
            },
            "DeviceEnvelope": {
                "type": "object",
                "properties": {
                    "data": {
                        "$ref": "#/components/schemas/Device"
                    }
                }
            },
            "NewApiKey": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string"
                    },
                    "branch": {
                        "type": "string",
                        "description": "The bhfId to bind this key to. **Omit it** and the key is a tenant key: it files through whichever of this tenant's branches the till sends as `X-SdcId`, which is what a POS setup screen expects, and `sdcId` comes back null because it pairs with no single branch — the till reads the ids it may use from `GET /zm/api/Branches`. **Pass it** and the key can file through that branch and no other, which is tighter and right for a till that will never move. Tenant keys authenticated nowhere before 1.9.0.\n",
                        "example": "000"
                    },
                    "dialect": {
                        "type": "string",
                        "enum": [
                            "bare",
                            "wrapped"
                        ],
                        "description": "Which response shape `/zm/api` answers this key with. `bare` is this surface's own contract and the default for every key. `wrapped` puts every reply inside `{success, message, data}` — the envelope older Zambian gateways answer with — for a till that reads `response.data.signature` and cannot be changed. It affects response SHAPE only: the HTTP status code, the request body and every field name are identical either way. See the Zambia (compat) tag.\n"
                    },
                    "environment": {
                        "type": "string",
                        "enum": [
                            "sandbox",
                            "production"
                        ],
                        "description": "**\"Sandbox\" is a naming and privilege tier, not a separate environment.** A sandbox key cannot mint production keys, but it authenticates the same API against the same ZRA infrastructure — a sale filed with an sk_test_ key is a real fiscal invoice on the tenant's real TPIN. There is no throwaway sandbox host; test against a tenant you control.\n"
                    },
                    "scopes": {
                        "type": "array",
                        "minItems": 1,
                        "description": "**Omit this and you get the right answer** — a key with no `scopes` field is granted both `invoices:write` and `invoices:read`.\n\nTwo ways to get it wrong, both of which mint successfully and fail later:\n\n* `\"scopes\": []` mints a key with **no** scopes. It authenticates\n  and then 403s on every call.\n* `\"scopes\": [\"invoices:read\"]` also 403s on **every** call,\n  including the GETs. `/zm/api` checks `invoices:write` in its\n  authentication middleware, before routing, so a read-only key\n  cannot read either.\n\nIn short: on this surface a key either holds `invoices:write` or it is inert. Only send `scopes` if you know why.\n",
                        "items": {
                            "type": "string",
                            "enum": [
                                "invoices:write",
                                "invoices:read"
                            ]
                        }
                    }
                }
            },
            "ApiKey": {
                "type": "object",
                "description": "The non-secret view of a key. The plaintext is returned once at issue time and is not stored, so it never appears here — identify a key by `id` for the management endpoints, and by `prefix` + `lastFour` when matching it against a secret someone is holding.\n",
                "properties": {
                    "id": {
                        "type": "integer",
                        "description": "The id every management endpoint takes — not the key itself.",
                        "example": 41
                    },
                    "name": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Whatever you named it at issue.",
                        "example": "POS fleet — production"
                    },
                    "type": {
                        "type": "string",
                        "enum": [
                            "partner",
                            "tenant",
                            "branch"
                        ],
                        "description": "`partner` is an `sk_…` secret key for this API. `branch` is a `pk_…` fiscalisation key bound to one device on `/zm/api`. `tenant` is a `pk_…` key for the same surface that is not bound to a branch — it files through whichever branch the `X-SdcId` names. Tenant keys authenticated nowhere before 1.9.0.\n"
                    },
                    "environment": {
                        "type": "string",
                        "enum": [
                            "sandbox",
                            "production"
                        ],
                        "description": "The privilege tier, not a separate host. Both file real invoices."
                    },
                    "dialect": {
                        "type": "string",
                        "enum": [
                            "bare",
                            "wrapped"
                        ],
                        "description": "Which response shape `/zm/api` answers this key with. `bare` is this surface's own contract and the default for every key. `wrapped` puts every reply inside `{success, message, data}` — the envelope older Zambian gateways answer with — for a till that reads `response.data.signature` and cannot be changed. It affects response SHAPE only: the HTTP status code, the request body and every field name are identical either way. See the Zambia (compat) tag.\n"
                    },
                    "prefix": {
                        "type": "string",
                        "description": "The leading identifier: `sk_live_`/`pk_test_` plus four characters. Safe to log.",
                        "example": "pk_live_9f2c"
                    },
                    "lastFour": {
                        "type": "string",
                        "description": "The key's last four characters. Safe to log.",
                        "example": "8Q4z"
                    },
                    "scopes": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "description": "What this key may do. A branch key without `invoices:write` is inert on /zm/api."
                    },
                    "allowedIps": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "description": "IPs and CIDR ranges this key may be presented from. **Empty means unrestricted**, not blocked.\n"
                    },
                    "lastUsedAt": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time",
                        "description": "When this key last authenticated. The way to find keys nobody is using before a revocation sweep — `null` means it has never been used since issue.\n"
                    },
                    "expiresAt": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time",
                        "description": "When it stops working. `null` means never. A rotated key's `expiresAt` is its grace deadline — that is the date to deploy the replacement by.\n"
                    },
                    "revoked": {
                        "type": "boolean",
                        "description": "Revocation is immediate and permanent; a revoked key is never reinstated."
                    },
                    "rotatedToId": {
                        "type": [
                            "integer",
                            "null"
                        ],
                        "description": "The `id` of the key that replaced this one, if it has been rotated. Non-null means this key is in its grace window and will stop working at `expiresAt`. A key can only be rotated once — a second attempt answers 403 `already_rotated`.\n"
                    },
                    "createdAt": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time",
                        "description": "When the key was issued."
                    }
                }
            },
            "ApiKeyCollection": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/ApiKey"
                        }
                    }
                }
            },
            "MintedApiKey": {
                "type": "object",
                "description": "`key` is the plaintext. This API does not return it again — store it before you discard the response. An operator can read a fiscalisation key back from the merchant page in the console if it is lost.\n",
                "properties": {
                    "key": {
                        "type": "string",
                        "description": "Plaintext — returned here and not again on this API.",
                        "example": "pk_live_9f2c8Q4z…"
                    },
                    "sdcId": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The branch's ZRA device id, to be sent as `X-SdcId` alongside this key on `/zm/api`.\n\n**`null` here means this key pairs with no single branch**, and it happens two ways, only one of which is a problem. You omitted `branch`, so the key is a tenant key and the till fetches the SDC ids it may use from `GET /zm/api/Branches` — expected, and nothing to handle. Or you passed a `branch` that has not been initialised with ZRA, so it has no SDC id yet: the mint still answers 201 and the failure surfaces as a 401 on the till, hours later. A provisioning script that passes `branch` should assert on this field.\n",
                        "example": "SDC0010000001"
                    },
                    "apiKey": {
                        "$ref": "#/components/schemas/ApiKey"
                    },
                    "message": {
                        "type": "string"
                    }
                }
            },
            "WebhookEndpointEnvelope": {
                "type": "object",
                "description": "`GET /api/v1/webhooks/{id}` wraps the endpoint in `data`, like the tenant and sale reads. The registration response does not — it returns `endpoint` alongside the one-time `secret`.\n",
                "properties": {
                    "data": {
                        "$ref": "#/components/schemas/WebhookEndpoint"
                    }
                }
            },
            "WebhookEndpoint": {
                "type": "object",
                "description": "One registered receiver. Endpoints belong to the **partner**, not to a merchant: a single endpoint receives events for every tenant you provision, and the payload's `tpin` says which one an event is about.\n",
                "properties": {
                    "id": {
                        "type": "integer",
                        "description": "The id the management paths take: enable, delete, and the delivery log.",
                        "example": 12
                    },
                    "url": {
                        "type": "string",
                        "format": "uri",
                        "description": "Where deliveries are POSTed. Fixed at registration — to change it, register a new endpoint and delete this one."
                    },
                    "description": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Your own label for this receiver."
                    },
                    "events": {
                        "type": "array",
                        "description": "What this endpoint is subscribed to. Events outside this list are never delivered here, even while it is enabled.",
                        "items": {
                            "type": "string",
                            "enum": [
                                "invoice.fiscalised",
                                "invoice.failed"
                            ]
                        }
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "enabled",
                            "disabled"
                        ],
                        "description": "`disabled` is set by the platform after 10 consecutive failures with no success in between. Recover with `POST /api/v1/webhooks/{id}/enable` — the secret is kept, but events fired while disabled were not queued and cannot be resent.\n\n**A failure is not always six attempts over eight hours.** A receiver that times out or 500s does exhaust the retry ladder first, so ten of those take days. But a URL the SSRF guard refuses at send time — the hostname now resolves to a private address, or stopped resolving — is terminal on the **first** attempt, with no retries. Ten events then disable the endpoint in however long it takes to file ten invoices, which under load is seconds.\n\nThe practical consequence: a DNS change or an expired record can take a healthy receiver from working to disabled between two polls of this field. Alert on `lastFailureAt` moving, rather than on `status` flipping.\n"
                    },
                    "lastSuccessAt": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time",
                        "description": "The last delivery this endpoint acknowledged with a 2xx. A success resets the consecutive-failure count to zero."
                    },
                    "lastFailureAt": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time",
                        "description": "The last failed delivery. **This is the field to alert on** — it moves on the first failure, whereas `status` only flips at ten, by which point events have already been dropped.\n"
                    },
                    "createdAt": {
                        "type": "string",
                        "format": "date-time",
                        "description": "When the endpoint was registered."
                    }
                }
            },
            "WebhookEndpointCollection": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/WebhookEndpoint"
                        }
                    }
                }
            },
            "NewWebhook": {
                "type": "object",
                "required": [
                    "url",
                    "events"
                ],
                "properties": {
                    "url": {
                        "type": "string",
                        "format": "uri",
                        "maxLength": 2048,
                        "description": "Where deliveries are POSTed. **`https://` in production** — we sign every payload, but fiscal data does not travel in cleartext.\n\nIt must also survive the SSRF guard, which resolves the hostname and refuses anything pointing at a private, loopback, link-local or otherwise reserved address, a non-standard port, or a hostname with a trailing dot. The rejection message names the reason.\n\nThe guard runs again before **every** send, not just at registration, and a URL that has become unsafe since is a terminal failure with no retries — see `status` on `WebhookEndpoint` for what that costs you.\n",
                        "example": "https://your-app.example/webhooks/zra"
                    },
                    "description": {
                        "type": "string",
                        "maxLength": 255,
                        "description": "Your own label for this receiver. Shown in listings; never sent to you."
                    },
                    "events": {
                        "type": "array",
                        "minItems": 1,
                        "description": "At least one, no duplicates. Subscribe to both unless you have a reason not to — `invoice.failed` is how you learn a queued sale will never file.",
                        "items": {
                            "type": "string",
                            "enum": [
                                "invoice.fiscalised",
                                "invoice.failed"
                            ]
                        }
                    }
                }
            },
            "CreatedWebhook": {
                "type": "object",
                "properties": {
                    "secret": {
                        "type": "string",
                        "description": "Signing secret (whsec_…). Shown once, never again."
                    },
                    "endpoint": {
                        "$ref": "#/components/schemas/WebhookEndpoint"
                    },
                    "message": {
                        "type": "string"
                    }
                }
            },
            "WebhookDelivery": {
                "type": "object",
                "description": "One event's journey to one endpoint — not one attempt. A delivery that has been retried five times is still a single row, with `attempts` at five.\n",
                "properties": {
                    "id": {
                        "type": "integer",
                        "description": "The delivery id — what the resend path takes, and what the `X-Webhook-Delivery` header carried.",
                        "example": 8821
                    },
                    "event": {
                        "type": "string",
                        "description": "The event name, as sent in `X-Webhook-Event`.",
                        "example": "invoice.fiscalised"
                    },
                    "eventId": {
                        "type": "string",
                        "description": "ULID; also the X-Webhook-Id header. Two rows can share it — the same event delivered to two endpoints."
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "pending",
                            "delivered",
                            "failed"
                        ],
                        "description": "`pending` means it is still working through the retry ladder. `failed` means it exhausted every attempt, or hit a terminal refusal such as a URL the SSRF guard now blocks.\n"
                    },
                    "attempts": {
                        "type": "integer",
                        "description": "Attempts made so far, including the first. Capped at 6; a resend puts it back to zero and the whole ladder applies again."
                    },
                    "responseStatus": {
                        "type": [
                            "integer",
                            "null"
                        ],
                        "description": "Your receiver's HTTP status on the last attempt. Null when nothing answered — a timeout, a DNS failure, or a refusal before the request left."
                    },
                    "error": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Why the last attempt failed, in prose. Null on a delivered row."
                    },
                    "nextRetryAt": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time",
                        "description": "When the next attempt is due. Null once the delivery is delivered or failed."
                    },
                    "deliveredAt": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time",
                        "description": "When your receiver returned a 2xx."
                    },
                    "createdAt": {
                        "type": "string",
                        "format": "date-time",
                        "description": "When the event was queued for this endpoint."
                    }
                }
            },
            "WebhookDeliveryCollection": {
                "type": "object",
                "description": "Paginated, newest first.",
                "properties": {
                    "data": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/WebhookDelivery"
                        }
                    },
                    "links": {
                        "$ref": "#/components/schemas/PaginationLinks"
                    },
                    "meta": {
                        "$ref": "#/components/schemas/PaginationMeta"
                    }
                }
            },
            "WebhookEvent": {
                "type": "object",
                "description": "The envelope every webhook delivery carries.",
                "properties": {
                    "id": {
                        "type": "string",
                        "description": "Event ULID. Equal to the X-Webhook-Id header — dedupe on it.",
                        "example": "01J3ZK5H2M9QWERTY0ULID26"
                    },
                    "event": {
                        "type": "string",
                        "enum": [
                            "invoice.fiscalised",
                            "invoice.failed"
                        ],
                        "example": "invoice.fiscalised"
                    },
                    "created": {
                        "type": "string",
                        "format": "date-time",
                        "description": "When the event was raised, ISO 8601.",
                        "example": "2026-09-02T09:00:01+00:00"
                    },
                    "data": {
                        "type": "object",
                        "properties": {
                            "invoice": {
                                "$ref": "#/components/schemas/WebhookInvoice"
                            }
                        }
                    }
                },
                "required": [
                    "id",
                    "event",
                    "created",
                    "data"
                ]
            },
            "WebhookInvoice": {
                "type": "object",
                "description": "The stable public shape of an invoice inside a webhook payload. It is a deliberately separate contract from the merchant API's `Sale` — a change to either never silently alters what the other delivers.\n",
                "properties": {
                    "id": {
                        "type": "integer",
                        "example": 1042
                    },
                    "reference": {
                        "type": "string",
                        "description": "The caller's own reference — clientInvoiceNo, falling back to the gateway invoice number.",
                        "example": "INV-1042"
                    },
                    "clientInvoiceNo": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "example": "INV-1042"
                    },
                    "invoiceNo": {
                        "type": "integer",
                        "example": 180
                    },
                    "zraInvoiceNo": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The composed ZRA form, `INV<branch><padded invoiceNo>/<receiptNo>`. Null until the document has a receipt number.",
                        "example": "INV001000000180/180"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "uploaded",
                            "pending",
                            "failed"
                        ],
                        "example": "uploaded"
                    },
                    "tpin": {
                        "type": "string",
                        "example": "1002101584"
                    },
                    "branch": {
                        "type": "string",
                        "example": "001"
                    },
                    "customer": {
                        "type": "object",
                        "properties": {
                            "name": {
                                "type": "string",
                                "example": "Katema Stores Ltd"
                            },
                            "tpin": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "example": "2002200000"
                            }
                        }
                    },
                    "totals": {
                        "type": "object",
                        "properties": {
                            "net": {
                                "type": "number",
                                "example": 603.45
                            },
                            "vat": {
                                "type": "number",
                                "example": 96.55
                            },
                            "total": {
                                "type": "number",
                                "example": 700
                            },
                            "currency": {
                                "type": "string",
                                "example": "ZMW"
                            }
                        }
                    },
                    "fiscal": {
                        "type": "object",
                        "description": "Null field-by-field while `status` is `pending`, and on a `failed` document.",
                        "properties": {
                            "receiptNo": {
                                "type": [
                                    "integer",
                                    "null"
                                ],
                                "example": 180
                            },
                            "internalData": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "example": "HFSD-KJHG-2SDF-9812"
                            },
                            "receiptSignature": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "example": "KJH2-8DJF-2LKD-9982"
                            },
                            "sdcId": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "example": "SDC0010000001"
                            },
                            "qrCodeUrl": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "example": "https://api-sandbox.zra.org.zm/verify/..."
                            },
                            "rewardMessage": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "ZRA's invoice-lottery message, null unless this invoice won a prize. Print it verbatim.",
                                "example": null
                            },
                            "publishedAt": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "format": "date-time"
                            }
                        }
                    },
                    "resultCode": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "ZRA's verdict — `000` on invoice.fiscalised, the refusal code on invoice.failed. See /docs/result-codes.",
                        "example": "000"
                    },
                    "lastError": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Why the last upload attempt failed, on invoice.failed.",
                        "example": null
                    },
                    "fiscalisedAt": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time",
                        "description": "When this invoice record last changed — **not** when ZRA fiscalised it. On an `invoice.fiscalised` event raised at upload the two coincide, which is why the name reads as it does, but any later write to the row (a retry recording an error, a sync touching it) moves this timestamp. It is also non-null on a `failed` document, which never fiscalised at all.\n\nFor the fiscalisation moment, use `fiscal.publishedAt` — ZRA's own timestamp, and null exactly when the document has not been filed.\n"
                    }
                }
            },
            "NewPartnerKey": {
                "type": "object",
                "required": [
                    "scopes"
                ],
                "properties": {
                    "name": {
                        "type": "string"
                    },
                    "environment": {
                        "type": "string",
                        "enum": [
                            "sandbox",
                            "production"
                        ],
                        "description": "Which tier the new key belongs to; a sandbox key cannot mint this as `production`. **\"Sandbox\" is a naming and privilege tier, not a separate environment.** A sandbox key cannot mint production keys, but it authenticates the same API against the same ZRA infrastructure — a sale filed with an sk_test_ key is a real fiscal invoice on the tenant's real TPIN. There is no throwaway sandbox host; test against a tenant you control.\n"
                    },
                    "scopes": {
                        "type": "array",
                        "minItems": 1,
                        "description": "Must be a subset of the calling key's scopes.",
                        "items": {
                            "type": "string",
                            "enum": [
                                "tenants:read",
                                "tenants:write",
                                "devices:read",
                                "devices:write",
                                "keys:write",
                                "invoices:read",
                                "webhooks:read",
                                "webhooks:write"
                            ]
                        }
                    },
                    "expiresInDays": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 3650,
                        "description": "Days from now until this key stops working. Omit it for a key that never expires — but only a caller that never expires may do that: **if the calling key has an expiry, omitting `expiresInDays` is refused** with 403 `expiry_escalation`, not silently clamped. The same 403 answers any date past the caller's own.\n\nThe 3650-day ceiling is this deployment's configured maximum and is enforced as a validation rule (422), separately from the escalation check (403).\n"
                    },
                    "allowedIps": {
                        "type": "array",
                        "maxItems": 20,
                        "description": "Exact IPs or CIDR ranges (IPv4 or IPv6) this key may be presented from. **At most 20 entries**, each at most 64 characters, no duplicates — a fleet behind more than 20 egress addresses wants a CIDR range, not 20 hosts.\n\nOmit it for an unrestricted key. If the *calling* key is IP-restricted this becomes required, and every range you give must sit inside the caller's own — narrowing is allowed, broadening and dropping are refused with 403 `ip_escalation`.\n",
                        "items": {
                            "type": "string",
                            "maxLength": 64,
                            "example": "41.72.0.0/16"
                        }
                    }
                }
            },
            "ZmError": {
                "type": "object",
                "description": "The flat error body used by every `/zm/api` endpoint.",
                "properties": {
                    "status": {
                        "type": "integer",
                        "description": "Repeats the HTTP status code.",
                        "example": 422
                    },
                    "message": {
                        "type": "string",
                        "description": "Human-readable summary."
                    },
                    "error": {
                        "type": "string",
                        "description": "Machine-readable detail when there is one — a billing reason (`subscription_inactive`, `quota_exceeded`, `no_subscription`) on a 402, or the underlying failure text on a 422.\n"
                    },
                    "resultCode": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Present when ZRA itself refused the document: its result code, verbatim. Branch on this, never on the prose — see `/docs/result-codes` for the table and the required action per code.\n",
                        "example": "910"
                    },
                    "resultMessage": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "ZRA's message for that code, verbatim."
                    }
                },
                "required": [
                    "status",
                    "message"
                ]
            },
            "TillLogin": {
                "type": "object",
                "required": [
                    "email",
                    "password"
                ],
                "description": "The credentials a merchant created against one key under Developers → Till keys. Not a dashboard sign-in.\n",
                "properties": {
                    "email": {
                        "type": "string",
                        "format": "email",
                        "description": "The till login address — not the merchant's own.",
                        "example": "till@katema.co.zm"
                    },
                    "password": {
                        "type": "string",
                        "format": "password",
                        "description": "At least 12 characters. Set by the merchant when they created the login."
                    }
                }
            },
            "ActiveApiKeyEnvelope": {
                "type": "object",
                "description": "The legacy success envelope. Always wrapped, whatever the key's dialect.",
                "properties": {
                    "success": {
                        "type": "boolean",
                        "example": true
                    },
                    "message": {
                        "type": "string",
                        "example": "Data retrieval successful."
                    },
                    "data": {
                        "$ref": "#/components/schemas/ActiveApiKey"
                    }
                }
            },
            "ActiveApiKey": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "description": "This gateway's key id.",
                        "example": "41"
                    },
                    "apiKeyId": {
                        "type": "string",
                        "description": "The same id again. The legacy contract carries two identifiers for one key and this gateway has one, so both name it; a till stores `apiKeyValue` and treats these as opaque.\n",
                        "example": "41"
                    },
                    "apiKeyName": {
                        "type": "string",
                        "description": "Whatever the key was named when it was minted.",
                        "example": "AIDEPOS"
                    },
                    "apiKeyValue": {
                        "type": "string",
                        "description": "The key itself, to send as `X-Api-Key` on every later call. Store it — each hand-back is audited against the key like any other plaintext read.\n",
                        "example": "pk_live_9f2c…"
                    },
                    "createdBy": {
                        "type": "string",
                        "description": "The till login that fetched it.",
                        "example": "till@katema.co.zm"
                    },
                    "edits": {
                        "type": "integer",
                        "description": "Always 0 — present for contract compatibility. This gateway does not version a key in place; a changed key is a new key.\n",
                        "example": 0
                    },
                    "branchInfo": {
                        "type": "array",
                        "description": "The branches this key may file through, one row per branch. Take the `sdcId` of the branch the till is installed at and send it as `X-SdcId` from then on. `GET /zm/api/Branches` answers the same question with more detail, including whether a branch has finished initialising with ZRA.\n",
                        "items": {
                            "$ref": "#/components/schemas/TillBranch"
                        }
                    },
                    "isValid": {
                        "type": "boolean",
                        "example": true
                    },
                    "created": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "deactivatedOn": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time",
                        "description": "Always null — a revoked key is not returned at all."
                    }
                }
            },
            "TillBranch": {
                "type": "object",
                "properties": {
                    "sdcId": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "example": "SDC0010000001"
                    },
                    "branchName": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "example": "Kabwe"
                    }
                }
            },
            "TillLoginError": {
                "type": "object",
                "description": "Every failure of this endpoint, in one shape.",
                "properties": {
                    "success": {
                        "type": "boolean",
                        "example": false
                    },
                    "httpCode": {
                        "type": "integer",
                        "example": 400
                    },
                    "message": {
                        "type": "string",
                        "example": "Invalid email or password"
                    },
                    "errorCode": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "example": null
                    },
                    "errors": {
                        "type": [
                            "object",
                            "null"
                        ],
                        "additionalProperties": true,
                        "description": "Field errors when the body failed validation; null otherwise."
                    },
                    "data": {
                        "type": [
                            "object",
                            "null"
                        ],
                        "example": null
                    }
                }
            },
            "ZmBranch": {
                "type": "object",
                "description": "One branch a key may file through. The same shape as an entry in `devices[]` on `GET /api/v1/me`, so the same question asked on either surface answers alike.\n",
                "properties": {
                    "branch": {
                        "type": "string",
                        "description": "ZRA branch id (bhfId).",
                        "example": "001"
                    },
                    "name": {
                        "type": "string",
                        "nullable": true,
                        "description": "The branch name as registered with ZRA.",
                        "example": "Kabwe"
                    },
                    "tpin": {
                        "type": "string",
                        "description": "The taxpayer this branch belongs to — worth showing back to an installer as confirmation they typed the right key.",
                        "example": "1002101584"
                    },
                    "sdcId": {
                        "type": "string",
                        "nullable": true,
                        "description": "Send this as `X-SdcId` on every other `/zm/api` call to file through this branch. Null until the device has been initialised with ZRA.\n",
                        "example": "SDC0060000514"
                    },
                    "initialized": {
                        "type": "boolean",
                        "description": "Whether device initialisation with ZRA has completed. A branch that reads false has no signing keys and cannot fiscalise yet.\n",
                        "example": true
                    },
                    "lastInvoiceNo": {
                        "type": "integer",
                        "description": "The last ZRA invoice number consumed on this branch.",
                        "example": 127
                    }
                }
            },
            "ZmTaxCode": {
                "type": "object",
                "properties": {
                    "taxCode": {
                        "type": "string",
                        "description": "Use this value in a line's `TaxCodes`.",
                        "example": "A"
                    },
                    "categoryName": {
                        "type": "string",
                        "example": "Standard Rated"
                    },
                    "rate": {
                        "type": "number",
                        "description": "VAT rate as a percentage.",
                        "example": 16
                    },
                    "taxableAmount": {
                        "type": "number",
                        "description": "Always 0 — present for contract compatibility.",
                        "example": 0
                    },
                    "taxAmount": {
                        "type": "number",
                        "description": "Always 0 — present for contract compatibility.",
                        "example": 0
                    },
                    "conversionRate": {
                        "type": "number",
                        "description": "Always 1 — present for contract compatibility.",
                        "example": 1
                    }
                }
            },
            "ZmInvoiceItem": {
                "type": "object",
                "required": [
                    "ItemDesc",
                    "itemCode",
                    "itemClassificationCode",
                    "TaxCodes",
                    "Quantity",
                    "UnitPrice"
                ],
                "properties": {
                    "ItemSequenceNumber": {
                        "type": "integer",
                        "description": "Accepted and ignored, for contract compatibility. The gateway always sequences lines by their array order — a caller-supplied number could leave gaps or duplicates ZRA refuses.\n",
                        "example": 1
                    },
                    "ItemDesc": {
                        "type": "string",
                        "maxLength": 200,
                        "description": "Description printed on the receipt.",
                        "example": "Maize meal 25kg"
                    },
                    "itemCode": {
                        "type": "string",
                        "maxLength": 50,
                        "description": "Your own product code.",
                        "example": "MM-25"
                    },
                    "itemClassificationCode": {
                        "type": "string",
                        "maxLength": 20,
                        "description": "ZRA UNSPSC classification code for the item — 8 digits, not a customs tariff (HS) number. Checked against ZRA's published catalogue since 1.6.0.\n",
                        "example": "50221204"
                    },
                    "TaxCodes": {
                        "type": "array",
                        "minItems": 1,
                        "description": "Tax categories for the line, from `GET /zm/api/TaxCodes`. Only the first entry is applied; the array shape is kept for contract compatibility.\n",
                        "items": {
                            "type": "string",
                            "enum": [
                                "A",
                                "B",
                                "C1",
                                "C2",
                                "C3",
                                "C",
                                "D",
                                "E",
                                "RVAT"
                            ]
                        },
                        "example": [
                            "A"
                        ],
                        "externalDocs": {
                            "description": "A = Standard rated (16%), B = Minimum taxable value (16%), C1 = Exports (0%, requires destinationCountryCode and may carry no other category), C2 = Zero-rating LPO (0%, requires lpoNumber), C3 = Zero-rated by nature (0%), D = Exempt, E = Disbursement, RVAT = Reverse VAT (16%). Bare \"C\" is accepted for legacy callers and treated as C3. Codes are case-insensitive. Anything outside this list — including TOT, which an earlier version of this document advertised — is refused with 422 rather than silently treated as Exempt.\n",
                            "url": "https://gateway.mikenyambe.com/docs"
                        }
                    },
                    "Quantity": {
                        "type": "number",
                        "exclusiveMinimum": 0,
                        "example": 2
                    },
                    "UnitPrice": {
                        "type": "number",
                        "minimum": 0,
                        "description": "VAT-inclusive unless `isTaxInclusive` is false.",
                        "example": 350
                    },
                    "isTaxInclusive": {
                        "type": "boolean",
                        "default": true,
                        "description": "When false the gateway grosses the price up by the tax category's rate before fiscalising, so the receipt total stays exact.\n"
                    },
                    "PackagingUnitCode": {
                        "type": "string",
                        "maxLength": 5,
                        "description": "ZRA packaging unit code.",
                        "example": "BG"
                    },
                    "QuantityUnitCode": {
                        "type": "string",
                        "maxLength": 5,
                        "description": "ZRA quantity unit code.",
                        "example": "KG"
                    }
                }
            },
            "ZmInvoice": {
                "type": "object",
                "required": [
                    "InvoiceNumber",
                    "IssuerName",
                    "IssuerId",
                    "ReceiptTypeCode",
                    "PaymentTypeCode",
                    "currencyType",
                    "invoiceItems"
                ],
                "properties": {
                    "InvoiceNumber": {
                        "type": "string",
                        "maxLength": 100,
                        "description": "Your own invoice number. Doubles as the idempotency key — re-sending one already fiscalised by this tenant returns the original result.\n",
                        "example": "INV-1042"
                    },
                    "IssuerName": {
                        "type": "string",
                        "maxLength": 60,
                        "description": "Cashier or till operator name.",
                        "example": "Cashier 1"
                    },
                    "IssuerId": {
                        "type": "string",
                        "maxLength": 20,
                        "description": "Cashier or till operator id.",
                        "example": "104"
                    },
                    "ReceiptTypeCode": {
                        "type": "string",
                        "enum": [
                            "S",
                            "R",
                            "D"
                        ],
                        "description": "`S` normal sale, `R` credit note, `D` debit note. On this legacy surface only `S` is supported — `R` and `D` are accepted by validation but rejected with 422. Credit notes have their own endpoint on the v1 surface: POST /api/v1/sales/{sale}/credit-note, which takes the original invoice from the URL rather than trusting a caller to state it.\n",
                        "example": "S"
                    },
                    "PaymentTypeCode": {
                        "type": "string",
                        "enum": [
                            "01",
                            "02",
                            "03",
                            "04",
                            "05",
                            "06",
                            "07",
                            "08"
                        ],
                        "description": "ZRA's payment method table, declared verbatim: `01` Cash, `02` Credit, `03` Cash/Credit, `04` Bank cheque, `05` Debit & credit card, `06` Mobile money, `07` Other, `08` Bank transfer. Note `02` is Credit, not card — a card payment is `05`. An unknown code is refused with 422.\n",
                        "example": "01"
                    },
                    "currencyType": {
                        "type": "string",
                        "enum": [
                            "ZMW",
                            "USD",
                            "GBP",
                            "EUR",
                            "CNY",
                            "ZAR"
                        ],
                        "description": "The currency you invoiced in. ZRA publishes 179 currencies but Smart Invoice accepts these six; anything else is refused with **422** rather than filed at face value in kwacha.\n",
                        "example": "ZMW"
                    },
                    "conversionRate": {
                        "type": "number",
                        "description": "How many kwacha one unit of `currencyType` buys, e.g. `26.5` for USD. **Required for every currency except ZMW**, where it must be `1` or omitted. ZRA refuses a foreign currency at a rate of 1, and refuses ZMW at any other rate; both directions are checked here and answer **422** before an invoice number is consumed.\n",
                        "example": 1
                    },
                    "SaleDate": {
                        "type": "string",
                        "format": "date-time",
                        "description": "When the sale actually happened, for a till that rang it up while it could not reach the gateway. Omitted means now. The future is clamped to now; anything past ZRA's 180-day floor is refused with 422 in words. The declared `salesDt` on the fiscal document comes from this value.\n",
                        "example": "2026-08-30 18:42:00"
                    },
                    "sdcId": {
                        "type": "string",
                        "description": "Optional echo of the branch SDC id. Authentication uses the `X-SdcId` header — this field is ignored for routing.\n"
                    },
                    "CustomerName": {
                        "type": "string",
                        "maxLength": 100,
                        "description": "Defaults to \"Walk-in Customer\" when omitted.",
                        "example": "Katema Stores Ltd"
                    },
                    "CustomerTpin": {
                        "type": "string",
                        "maxLength": 14,
                        "description": "The customer's ZRA TPIN, required for a claimable VAT invoice. (Legacy gateways call this field `customerTin`.)\n",
                        "example": "1002101584"
                    },
                    "OriginalInvoiceNumber": {
                        "type": "string",
                        "description": "The invoice being corrected — for credit/debit notes, once supported."
                    },
                    "lpoNumber": {
                        "type": "string",
                        "maxLength": 50,
                        "description": "Local Purchase Order number. **Required** when the invoice's items use tax code `C2` (Zero-rating LPO), and rejected otherwise. ZRA does not allow C2 items to share an invoice with any other tax category — put LPO items on their own invoice.\n",
                        "example": "LPO-2026-00412"
                    },
                    "destinationCountryCode": {
                        "type": "string",
                        "minLength": 2,
                        "maxLength": 2,
                        "description": "Country of destination, ISO 3166-1 alpha-2. **Required** when the invoice's items use tax code `C1` (Exports), and rejected otherwise. ZRA does not allow C1 items to share an invoice with any other tax category — put exported items on their own invoice.\n\nEarlier versions of this document listed this field among those accepted and silently ignored. It is now honoured, and an export invoice without it is refused with 422.\n",
                        "example": "CD"
                    },
                    "invoiceItems": {
                        "type": "array",
                        "minItems": 1,
                        "items": {
                            "$ref": "#/components/schemas/ZmInvoiceItem"
                        }
                    }
                }
            },
            "ZmTaxItem": {
                "type": "object",
                "description": "One tax category's contribution to the invoice total.",
                "properties": {
                    "taxCode": {
                        "type": "string",
                        "example": "A"
                    },
                    "rate": {
                        "type": "number",
                        "example": 16
                    },
                    "taxableAmount": {
                        "type": "number",
                        "description": "Net of VAT.",
                        "example": 603.45
                    },
                    "taxAmount": {
                        "type": "number",
                        "example": 96.55
                    }
                }
            },
            "ZmFiscalDetails": {
                "type": "object",
                "description": "What a till prints on a fiscalised receipt. `signature`, `qrCode` and `internalData` are `null` while a sale is queued offline and are populated once it reaches ZRA.\n\n**Returned bare, at the top level.** Some legacy VSDC gateways wrap every reply in `{success, message, data: {…}}`; this surface does not, so the field a till reads as `response.data.signature` is `response.signature` here, and there is no `success` flag to branch on — use the HTTP status code.\n",
                "properties": {
                    "invoiceNumber": {
                        "type": "string",
                        "description": "The `InvoiceNumber` you sent.",
                        "example": "INV-1042"
                    },
                    "taxPayerInvoiceNumber": {
                        "type": "string",
                        "description": "ZRA receipt number. Empty string while the sale is queued offline.",
                        "example": "91"
                    },
                    "invoiceSequence": {
                        "type": "integer",
                        "description": "The branch's ZRA invoice sequence number.",
                        "example": 91
                    },
                    "vsdcdate": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "VSDC publish timestamp, yyyyMMddHHmmss. Null while the sale is queued offline.",
                        "example": "20260730103012"
                    },
                    "signature": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "ZRA receipt signature — print on the receipt.",
                        "example": "PGRWGD65"
                    },
                    "qrCode": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "uri",
                        "description": "Verification URL to render as a QR code."
                    },
                    "rewardMessage": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "ZRA's invoice-lottery message, null unless this invoice won a prize. Not part of the surface this endpoint mirrors — ZRA introduced the lottery afterwards — but published here so a till integrating on /zm/api can still tell a customer they won.",
                        "example": null
                    },
                    "internalData": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "ZRA internal data block — print on the receipt."
                    },
                    "sdcId": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Null while the sale is queued offline.",
                        "example": "SDC0060000514"
                    },
                    "receiptTotalCounter": {
                        "type": "integer",
                        "description": "0 while the sale is queued offline.",
                        "example": 91
                    },
                    "normalReceiptTypeCounter": {
                        "type": "integer",
                        "description": "0 while the sale is queued offline.",
                        "example": 91
                    },
                    "currencyType": {
                        "type": "string",
                        "example": "ZMW"
                    },
                    "totalAmount": {
                        "type": "number",
                        "description": "VAT-inclusive invoice total.",
                        "example": 700
                    },
                    "taxItems": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/ZmTaxItem"
                        }
                    }
                }
            }
        }
    }
}
