{
  "info": {
    "name": "KRA Agent SBX API v1",
    "_postman_id": "8f3c1d20-7b41-4f6e-9d2a-3c5e7a91b204",
    "description": "# KRA Agent API\n\nThe KRA Agent API is built by [Symatech Labs Ltd](https://symatechlabs.com/) to help businesses submit sales invoices to the Kenya Revenue Authority (KRA) effortlessly. It accepts payloads from various point-of-sale (POS) systems, formats them automatically, and guarantees reliable filing with automated retries whenever tax servers experience downtime.\n\n## Key Features\n\n1. **Flexible Payload Mapping**: Different POS systems send data in different JSON structures. Each POS till uses configurable payload and code mappings to translate item and tax codes without requiring custom code changes or deployments.\n\n2. **Isolated POS Integrations**: Each till is managed and authenticated independently under an account, ensuring that individual till credentials remain separate and secure.\n\n3. **Reliable Queueing and Retries**: Temporary network or server failures back off and retry automatically. Every submission attempt is logged, and duplicate submission checks ensure sales are never double-filed.\n\n## Authentication Surfaces\n\n- **POS Tills (pos.auth)**: POS devices send sales using till API keys (`Authorization: Bearer <key_id>.<secret>`) or body-authenticated SenderId signatures.\n- **Account Users (auth:sanctum)**: Users manage settings, tills, catalogue items, and mappings using Sanctum tokens (`Authorization: Bearer <token>`).\n\n## Response Envelope\n\nEvery endpoint returns a consistent JSON envelope:\n\n```json\n{ \"success\": true, \"message\": \"Human readable summary\", \"data\": { } }\n```\n\n## Getting Started\n\n1. Run **Login** using your account email and password to receive a Sanctum token.\n2. Create or list a POS integration under your account to get an `integration_id`.\n3. Generate a Till Credential (`key_id` and `secret`) for your POS till.\n4. Set up tax and item code mappings for your POS format.\n5. Register your catalogue items and start sending sales invoices.\n\nFor full developer documentation and integration support, visit [Symatech Labs Ltd](https://symatechlabs.com/).",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "1. Authentication (humans)",
      "description": "Sanctum tokens for people. A POS till never uses these — it presents its own API key instead, so a leaked till key cannot read reports.",
      "item": [
        {
          "name": "Login",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/auth/login",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "auth",
                "login"
              ]
            },
            "description": "Exchange credentials for a Sanctum token.\n\n**Business rules**\n- Rate limited to 5 attempts per minute per IP.\n- An unknown email and a wrong password return the **same** message and the same\n  timing — otherwise the endpoint is an account-enumeration oracle.\n- A deactivated user gets 403, not 422: the credentials were right, the account is not.\n- Both successful and failed attempts are written to the audit trail.\n\nThe response includes the caller's full permission list so a client can hide\nactions it cannot perform.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"admin@sunrisecafe.co.ke\",\n  \"password\": \"Password!2345\",\n  \"device_name\": \"ops-laptop\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Signed in.\",\n  \"data\": {\n    \"token\": \"3|hR9xkP2wQ7...\",\n    \"user\": {\n      \"id\": 1,\n      \"name\": \"Sunrise Admin\",\n      \"email\": \"admin@sunrisecafe.co.ke\",\n      \"account_id\": 1,\n      \"account\": null,\n      \"is_platform_user\": false,\n      \"roles\": [\n        \"account-admin\"\n      ],\n      \"permissions\": [\n        \"accounts.view\",\n        \"documents.view\",\n        \"billing.payments.record\"\n      ]\n    }\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Bad credentials",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Validation failed.\",\n  \"errors\": {\n    \"email\": [\n      \"These credentials do not match our records.\"\n    ]\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "403 Deactivated",
              "status": "Forbidden",
              "code": 403,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"This account has been deactivated. Contact your administrator.\"\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "429 Rate limited",
              "status": "Too Many Requests",
              "code": 429,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Too many requests. Retry after the interval in the Retry-After header.\"\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('login succeeds', () => pm.response.to.have.status(200));",
                  "const b = pm.response.json();",
                  "if (b.success) { pm.collectionVariables.set('token', b.data.token); }",
                  "pm.test('returns a token', () => pm.expect(b.data.token).to.be.a('string'));"
                ]
              }
            }
          ]
        },
        {
          "name": "Who am I",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/auth/me",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "auth",
                "me"
              ]
            },
            "description": "Returns the acting user with their roles and effective permissions."
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Success\",\n  \"data\": {\n    \"id\": 1,\n    \"name\": \"Sunrise Admin\",\n    \"email\": \"admin@sunrisecafe.co.ke\",\n    \"account_id\": null,\n    \"is_platform_user\": true,\n    \"roles\": [\n      \"platform-admin\"\n    ],\n    \"permissions\": [\n      \"accounts.view\"\n    ]\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Logout",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/auth/logout",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "auth",
                "logout"
              ]
            },
            "description": "Deletes the token used for this request. Other tokens for the same user are unaffected, so revoking one device does not sign out the rest."
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Signed out.\",\n  \"data\": null\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "2. POS ingestion (machines)",
      "description": "\nThe machine-facing surface, authenticated by a **per-integration API key** —\nnever a user session.\n\n```\nAuthorization: Bearer <key_id>.<secret>\n```\nor `X-Api-Key` + `X-Api-Secret`.\n\n**The request body is the POS's own native JSON.** We do not ask merchants to\nreshape their payloads; translating them is what this system is for. The shape\nis interpreted by the integration's active payload mapping.\n\n**Idempotency.** `(pos_integration_id, document_type, source_document_ref)` is\nunique. Re-sending a reference we already hold returns the existing record with\n`duplicate: true` — it never files a second time.\n\n**Billing never blocks ingestion.** An unpaid integration still receives 202 and\nits documents are stored in full; they simply park in `on_hold` until the\nsubscription is settled.\n",
      "item": [
        {
          "name": "Submit a sale (SenderId body auth — no header)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/pos/ingest",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "pos",
                "ingest"
              ]
            },
            "description": "For a POS that **cannot send an auth header**. There is **no `Authorization`** here — the sale is authenticated by a provider-generated secret carried in the body at `Invoice.SenderId` (a random per-sender token). We resolve it by an indexed hash to exactly one till.\n\n**Requires:** the till's `auth_scheme` is `sender_id` (set on the integration), and the `SenderId` has been registered (see *Register a SenderId*). Otherwise every call returns a generic `401`.\n\nEverything else is identical to the header-authenticated path — same mapping, same `202`/duplicate idempotency (on `TraderSystemInvoiceNumber`), same billing hold. Sales only. The `SenderId` is stored in the payload as received.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"Invoice\": {\n    \"SenderId\": \"{{pos_sender_id}}\",\n    \"InvoiceTimestamp\": \"2026-07-21T10:04:00\",\n    \"InvoiceCategory\": \"Tax invoice\",\n    \"TraderSystemInvoiceNumber\": \"REC-57\",\n    \"PINOfBuyer\": \"\",\n    \"InvoiceType\": \"Original\",\n    \"TotalInvoiceAmount\": 442.38,\n    \"TotalTaxableAmount\": 381.36,\n    \"TotalTaxAmount\": 61.02,\n    \"ItemDetails\": [\n      { \"HSDesc\": \"Strawberry Milkshake\", \"HSCode\": \"MILK-09\", \"TaxRate\": 16, \"UnitPrice\": 381.36, \"Quantity\": 1, \"ItemAmount\": 381.36, \"TaxAmount\": 61.02 }\n    ]\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "202 Accepted",
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Document accepted for submission.\",\n  \"data\": { \"document_id\": 90002, \"reference\": \"REC-57\", \"status\": \"queued\" }\n}",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "401 Invalid sender credential",
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Invalid sender credential.\"\n}",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ]
        },
        {
          "name": "Check a sale status (SenderId body auth)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/pos/ingest/REC-57",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "pos",
                "ingest",
                "REC-57"
              ]
            },
            "description": "The status poll for a **sender_id** till. It cannot authenticate the bearer `GET /v1/pos/documents/{ref}` (no header, and a GET has no body to carry the secret), so it polls here: the **reference is in the path**, and the **SenderId secret rides in the body** (`Invoice.SenderId`) — never in the URL or query, so it stays out of access logs.\n\nReturns the same document status as the bearer status endpoint (`queued` / `accepted` with the KRA control code / `retry_scheduled` / `rejected`). It only ever returns **this till's own** documents — another till's reference is a `404`.\n\n**Prefer `webhook` or `sync` delivery** for outcomes; use this poll as the fallback when a webhook is missed.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"Invoice\": {\n    \"SenderId\": \"{{pos_sender_id}}\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 Status",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Success\",\n  \"data\": { \"document_id\": 90002, \"reference\": \"REC-57\", \"status\": \"accepted\", \"kra_control_code\": \"KRACU0300003914\", \"kra_invoice_number\": \"25\" }\n}",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "404 Not this till's document",
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"No document with reference [REC-57] has been received from this POS integration.\"\n}",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ]
        },
        {
          "name": "Submit a sales invoice (nested format)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{pos_key_id}}.{{pos_secret}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Client-Request-Id",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/pos/documents",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "pos",
                "documents"
              ]
            },
            "description": "Files one document. The default type is `sales_invoice`; override with the\n`X-Document-Type` header or the typed URL.\n\n**Responses by delivery mode**\n| Mode | Response |\n|---|---|\n| `async` | 202 immediately; poll the status endpoint |\n| `webhook` | 202 immediately; we POST the signed result to your URL |\n| `sync` | 201 with the control code, or 202 if it could not complete in time |\n\n**Optional headers**\n- `X-Client-Request-Id` — echoed onto the document for your own reconciliation.\n- `X-Document-Type` — one of `sales_invoice`, `credit_note`, `debit_note`,\n  `stock_movement`, `item_registration`, `customer_registration`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"receipt\": {\n    \"no\": \"RST-8891\",\n    \"issued\": \"2026-07-21T10:04:00+03:00\"\n  },\n  \"customer\": {\n    \"name\": \"Walk-in\",\n    \"pin\": null\n  },\n  \"items\": [\n    {\n      \"sku\": \"BF-01\",\n      \"desc\": \"Beef Stew\",\n      \"qty\": 2,\n      \"price\": 650.0,\n      \"vat\": \"A\"\n    },\n    {\n      \"sku\": \"CH-02\",\n      \"desc\": \"Chapati\",\n      \"qty\": 3,\n      \"price\": 50.0,\n      \"vat\": \"A\"\n    }\n  ],\n  \"totals\": {\n    \"gross\": 1450.0\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "202 Accepted (async)",
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Document accepted for submission.\",\n  \"data\": {\n    \"document_id\": 90001,\n    \"reference\": \"RST-8891\",\n    \"document_type\": \"sales_invoice\",\n    \"status\": \"queued\",\n    \"total_incl_tax\": 1450.0,\n    \"total_tax\": 200.0,\n    \"currency\": \"KES\",\n    \"received_at\": \"2026-07-21T10:04:02+00:00\"\n  }\n}",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"receipt\": {\n    \"no\": \"RST-8891\",\n    \"issued\": \"2026-07-21T10:04:00+03:00\"\n  },\n  \"customer\": {\n    \"name\": \"Walk-in\",\n    \"pin\": null\n  },\n  \"items\": [\n    {\n      \"sku\": \"BF-01\",\n      \"desc\": \"Beef Stew\",\n      \"qty\": 2,\n      \"price\": 650.0,\n      \"vat\": \"A\"\n    },\n    {\n      \"sku\": \"CH-02\",\n      \"desc\": \"Chapati\",\n      \"qty\": 3,\n      \"price\": 50.0,\n      \"vat\": \"A\"\n    }\n  ],\n  \"totals\": {\n    \"gross\": 1450.0\n  }\n}"
                },
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "201 Created (sync mode)",
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Document accepted by the revenue authority.\",\n  \"data\": {\n    \"document_id\": 90001,\n    \"reference\": \"RST-8891\",\n    \"document_type\": \"sales_invoice\",\n    \"status\": \"accepted\",\n    \"kra_control_code\": \"KRACU00918273645\",\n    \"kra_invoice_number\": \"INV0000090001\",\n    \"verification_url\": \"https://etims.kra.go.ke/verify/KRACU00918273645\",\n    \"provider_reference\": \"AGX-77120\",\n    \"total_incl_tax\": 1450.0,\n    \"total_tax\": 200.0,\n    \"currency\": \"KES\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "200 Duplicate replay",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"This document reference has already been received; returning the existing record.\",\n  \"data\": {\n    \"document_id\": 90001,\n    \"reference\": \"RST-8891\",\n    \"status\": \"accepted\",\n    \"duplicate\": true,\n    \"kra_control_code\": \"KRACU00918273645\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "202 Held (subscription unpaid)",
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Document accepted and stored. Submission is on hold: Subscription is not paid up. It will be submitted automatically once this is resolved.\",\n  \"data\": {\n    \"document_id\": 90042,\n    \"reference\": \"RST-8891\",\n    \"status\": \"on_hold\",\n    \"hold_reason\": \"billing_suspended\",\n    \"total_incl_tax\": 1450.0\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Unmapped tax code",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Tax code [UNKNOWN] has no mapping for this POS integration. Add a code mapping of type [tax] so the correct rate is applied \\u2014 guessing a tax rate would file the wrong amount.\",\n  \"errors\": {\n    \"error_code\": \"UNMAPPED_TAX_CODE\",\n    \"field\": \"tax_code\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Invalid buyer PIN",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Buyer PIN [P05XXXX] is not a valid KRA PIN. It must be a letter, nine digits and a letter (for example P051234567X). Leave it empty for a walk-in customer \\u2014 a wrong PIN would file the sale against the wrong taxpayer.\",\n  \"errors\": {\n    \"error_code\": \"INVALID_CUSTOMER_PIN\",\n    \"field\": \"customer_pin\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Totals disagree",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"The declared total of 1500.00 does not agree with the sum of the lines, 1450.00 (difference 50.00, tolerance 0.01). Filing a total that disagrees with its lines would be rejected downstream, so it is refused here where it can still be corrected.\",\n  \"errors\": {\n    \"error_code\": \"TOTALS_DISAGREE\",\n    \"field\": \"total_incl_tax\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "401 Bad credentials",
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Invalid API credentials.\"\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "403 Integration disabled",
              "status": "Forbidden",
              "code": 403,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"This POS integration has been disabled. Contact support.\"\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "413 Payload too large",
              "status": "Content Too Large",
              "code": 413,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Payload is 2097152 bytes; the maximum accepted is 1048576 bytes.\"\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "503 Planned maintenance",
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "Retry-After",
                  "value": "600"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"The service is temporarily unavailable for planned maintenance. Nothing you have already sent is affected. Hold this document and send it again after 600 seconds \\u2014 it will be accepted normally.\",\n  \"errors\": {\n    \"retry_after_seconds\": 600,\n    \"reason\": \"Database migration\",\n    \"since\": \"2026-07-21T02:00:00+00:00\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('accepted', () => pm.expect([200,201,202]).to.include(pm.response.code));",
                  "const b = pm.response.json();",
                  "if (b.data && b.data.document_id) { pm.collectionVariables.set('document_id', b.data.document_id); }",
                  "pm.test('reference echoed back', () => pm.expect(b.data.reference).to.be.a('string'));"
                ]
              }
            }
          ]
        },
        {
          "name": "Submit a sales invoice (flat format)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{pos_key_id}}.{{pos_secret}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/pos/documents",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "pos",
                "documents"
              ]
            },
            "description": "The same endpoint, a completely different JSON dialect. Nothing changes server-side except which mapping row is active for the integration — this is the point of holding mappings as data.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"invoice_number\": \"RT-4410\",\n  \"date\": \"21/07/2026\",\n  \"customer_name\": \"Walk-in\",\n  \"lines\": [\n    {\n      \"code\": \"BF-01\",\n      \"name\": \"Beef Stew\",\n      \"quantity\": 2,\n      \"unit_price\": 560.34,\n      \"tax_code\": \"VAT16\"\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "202 Accepted",
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Document accepted for submission.\",\n  \"data\": {\n    \"document_id\": 90002,\n    \"reference\": \"RT-4410\",\n    \"status\": \"queued\",\n    \"total_incl_tax\": 1300.0,\n    \"currency\": \"KES\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Submit a credit note",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{pos_key_id}}.{{pos_secret}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/pos/documents/type/credit_note",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "pos",
                "documents",
                "type",
                "credit_note"
              ]
            },
            "description": "A credit note **must** carry two things, both refused if absent:\n\n- a reference to the document it amends (mapped to `original_document_ref`) — without it the note cannot be reconciled against the sale it reverses;\n- a KRA **reason code** (mapped to `reason_code`) — eTIMS rejects a credit note that does not say WHY it reverses the sale. Valid codes: `01` Missing quantity, `02` Missing item, `03` Damaged, `04` Wasted, `05` Raw material shortage, `06` Refund, `07` Wrong quantity, `08` Wrong item, `09` Wrong price, `10` Cancelled sale, `11` Other.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"receipt\": {\n    \"no\": \"CN-0007\",\n    \"original\": \"RST-8891\",\n    \"reason\": \"06\",\n    \"issued\": \"2026-07-22T09:00:00+03:00\"\n  },\n  \"items\": [\n    {\n      \"sku\": \"BF-01\",\n      \"desc\": \"Beef Stew (returned)\",\n      \"qty\": 1,\n      \"price\": 650.0,\n      \"vat\": \"A\"\n    }\n  ],\n  \"totals\": {\n    \"gross\": 650.0\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "202 Accepted",
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Document accepted for submission.\",\n  \"data\": {\n    \"document_id\": 90003,\n    \"reference\": \"CN-0007\",\n    \"document_type\": \"credit_note\",\n    \"status\": \"queued\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Missing original reference",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"A credit_note must reference the document it amends, but no value was mapped to [original_document_ref].\",\n  \"errors\": {\n    \"error_code\": \"MISSING_ORIGINAL_REFERENCE\",\n    \"field\": \"original_document_ref\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Missing / invalid reason code",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Reason code [99] is not a KRA credit-note reason. Valid codes are: 01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11.\",\n  \"errors\": {\n    \"error_code\": \"INVALID_REASON_CODE\",\n    \"field\": \"reason_code\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Submit end-of-day totals (Z-report)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{pos_key_id}}.{{pos_secret}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/pos/daily-summary",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "pos",
                "daily-summary"
              ]
            },
            "description": "Your till's own figures for the day. **This is the only check that can reveal a\nsale that never reached us at all** — every other report reconciles what we\nreceived, and a document that never arrived leaves no trace.\n\nSend it once at close of business. Re-sending for the same date corrects the\nearlier figures rather than creating a second, contradictory record.\n\n**Any gap is sorted into buckets, and only ONE is yours to fix:**\n\n| Bucket | Meaning | Your action |\n|---|---|---|\n| `never_received` | It is on your till and nowhere else | **Re-send it** |\n| `in_progress` | We hold it and are still filing it | Nothing |\n| `held` | We hold it; the subscription is unpaid | Settle the account |\n| `failed` | We hold it; it was refused | We have contacted you |\n\n`200` = the figures agree. `202` = they do not; read `breakdown` and\n`resend_required`. We will never ask you to re-send a document we already hold.\n\n**The business date is yours, not ours.** Days are bounded by the integration's\nown `timezone` (default `Africa/Nairobi`), so a receipt issued at 01:30 EAT\nbelongs to that day even though the server clock still reads the previous date\nin UTC. \"Not in the future\" is judged the same way — a till closing at 23:30 EAT\ncan report a date the UTC server thinks is tomorrow.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"business_date\": \"2026-07-21\",\n  \"document_count\": 412,\n  \"gross_total\": 604210.0,\n  \"tax_total\": 83340.0,\n  \"first_reference\": \"RST-8891\",\n  \"last_reference\": \"RST-9302\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 Everything reached us",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Totals match. Every receipt your till recorded has reached us.\",\n  \"data\": {\n    \"id\": 51,\n    \"pos_integration_id\": 10,\n    \"business_date\": \"2026-07-21\",\n    \"status\": \"matched\",\n    \"revision\": 1,\n    \"declared\": {\n      \"count\": 412,\n      \"gross\": 604210.0,\n      \"tax\": 83340.0,\n      \"first_reference\": \"RST-8891\",\n      \"last_reference\": \"RST-9302\"\n    },\n    \"received\": {\n      \"count\": 412,\n      \"gross\": 604210.0,\n      \"tax\": 83340.0\n    },\n    \"breakdown\": {\n      \"accepted\": 410,\n      \"in_progress\": 2,\n      \"held\": 0,\n      \"failed\": 0,\n      \"never_received\": 0\n    },\n    \"variance\": {\n      \"count\": 0,\n      \"gross\": 0.0,\n      \"tax\": 0.0\n    },\n    \"missing_references\": [],\n    \"resend_required\": false\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "202 Four receipts never arrived",
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Your till recorded 412 receipts but we only ever received 408. 4 never reached us and must be re-sent.\",\n  \"data\": {\n    \"id\": 51,\n    \"business_date\": \"2026-07-21\",\n    \"status\": \"variance\",\n    \"declared\": {\n      \"count\": 412,\n      \"gross\": 604210.0,\n      \"tax\": 83340.0\n    },\n    \"received\": {\n      \"count\": 408,\n      \"gross\": 598890.0,\n      \"tax\": 82606.0\n    },\n    \"breakdown\": {\n      \"accepted\": 408,\n      \"in_progress\": 0,\n      \"held\": 0,\n      \"failed\": 0,\n      \"never_received\": 4\n    },\n    \"variance\": {\n      \"count\": 4,\n      \"gross\": 5320.0,\n      \"tax\": 734.0\n    },\n    \"missing_references\": [\n      \"RST-8903\",\n      \"RST-8904\",\n      \"RST-8917\",\n      \"RST-8952\"\n    ],\n    \"resend_required\": true\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "202 We hold them all — nothing to re-send",
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"We hold all 412 receipts. Some are still being processed or held \\u2014 nothing needs re-sending.\",\n  \"data\": {\n    \"business_date\": \"2026-07-21\",\n    \"status\": \"variance\",\n    \"breakdown\": {\n      \"accepted\": 0,\n      \"in_progress\": 0,\n      \"held\": 412,\n      \"failed\": 0,\n      \"never_received\": 0\n    },\n    \"resend_required\": false\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Tax exceeds gross",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"The declared tax of 700,000.00 is greater than the declared gross of 604,210.00. One of the two figures is wrong.\"\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Future date",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Validation failed.\",\n  \"errors\": {\n    \"business_date\": [\n      \"The business date field must be a date before or equal to today.\"\n    ]\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Business date is in the till's future",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"That business date is in the future for this till. Its current business date is 2026-07-22 (Africa/Nairobi).\",\n  \"errors\": {\n    \"business_date\": [\n      \"Cannot be later than the till's current business date.\"\n    ]\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('accepted', () => pm.expect([200,202]).to.include(pm.response.code));",
                  "const b = pm.response.json();",
                  "pm.test('only genuinely missing documents are flagged for resend', function () {",
                  "    if (b.data && b.data.resend_required) {",
                  "        pm.expect(b.data.breakdown.never_received).to.be.above(0);",
                  "    }",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Read back a day's comparison",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{pos_key_id}}.{{pos_secret}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/pos/daily-summary/2026-07-21",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "pos",
                "daily-summary",
                "2026-07-21"
              ]
            },
            "description": "The stored comparison for a business date, including the bucket breakdown."
          },
          "response": [
            {
              "name": "404 Never reported",
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"No end-of-day summary has been received for 2026-07-21.\"\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "What must I re-send?",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{pos_key_id}}.{{pos_secret}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/pos/daily-summary/2026-07-21/missing",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "pos",
                "daily-summary",
                "2026-07-21",
                "missing"
              ]
            },
            "description": "The machine-readable answer, so a well-written POS can repair the day without a\nhuman reading an email — and will never be told to re-send something already\nsitting in our database.\n\n`missing_references` is populated only when the till numbers receipts\nsequentially (set `sequential_references` on the integration). Otherwise the\ncount is reported but individual references cannot be named — guessing them\nwould send someone hunting for invoices that never existed."
          },
          "response": [
            {
              "name": "200 Four to re-send",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Success\",\n  \"data\": {\n    \"business_date\": \"2026-07-21\",\n    \"never_received_count\": 4,\n    \"missing_references\": [\n      \"RST-8903\",\n      \"RST-8904\",\n      \"RST-8917\",\n      \"RST-8952\"\n    ],\n    \"resend_required\": true,\n    \"note\": \"These receipts never reached us. Re-send them from your POS.\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "200 Nothing to re-send",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Success\",\n  \"data\": {\n    \"business_date\": \"2026-07-21\",\n    \"never_received_count\": 0,\n    \"missing_references\": [],\n    \"resend_required\": false,\n    \"note\": \"Nothing to re-send. Anything still outstanding is with us and is being handled.\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Poll a document by your own reference",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{pos_key_id}}.{{pos_secret}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/pos/documents/RST-8891",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "pos",
                "documents",
                "RST-8891"
              ]
            },
            "description": "The other half of the async contract. Poll with the reference **your** system\nissued — you never have to store our identifiers.\n\nPoll until `status` is one of `accepted`, `rejected`, `cancelled`, or\n`dead_letter`. `on_hold`, `queued` and `retry_scheduled` all mean \"still ours,\nkeep waiting\".\n\nOn a **credit or debit note**, the reply also carries `original_document_ref` (the sale it amends) and `reason_code`; a plain sale omits both."
          },
          "response": [
            {
              "name": "200 Accepted",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Success\",\n  \"data\": {\n    \"document_id\": 90001,\n    \"reference\": \"RST-8891\",\n    \"status\": \"accepted\",\n    \"kra_control_code\": \"KRACU00918273645\",\n    \"kra_invoice_number\": \"INV0000090001\",\n    \"verification_url\": \"https://etims.kra.go.ke/verify/KRACU00918273645\",\n    \"total_incl_tax\": 1450.0\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "200 Accepted (credit note — shows what it amends)",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Success\",\n  \"data\": {\n    \"document_id\": 90008,\n    \"reference\": \"CN-0007\",\n    \"document_type\": \"credit_note\",\n    \"original_document_ref\": \"RST-8891\",\n    \"reason_code\": \"06\",\n    \"status\": \"accepted\",\n    \"kra_control_code\": \"KRACU00918299001\",\n    \"kra_invoice_number\": \"INV0000090008\",\n    \"total_incl_tax\": 650.0\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "200 Retry scheduled",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Success\",\n  \"data\": {\n    \"document_id\": 90001,\n    \"reference\": \"RST-8891\",\n    \"status\": \"retry_scheduled\",\n    \"error_code\": \"UPSTREAM_UNAVAILABLE\",\n    \"error_message\": \"The revenue authority endpoint did not respond.\",\n    \"next_attempt_at\": \"2026-07-21T10:09:02+00:00\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "404 Never received",
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"No document with reference [RST-9999] has been received from this POS integration.\"\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "3. Accounts & users",
      "description": "Merchant accounts and the people who log in. A tenant user is pinned to one account; platform staff have `account_id = null` and see everything.",
      "item": [
        {
          "name": "List accounts",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/accounts?per_page=25",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "accounts"
              ],
              "query": [
                {
                  "key": "per_page",
                  "value": "25"
                }
              ]
            },
            "description": "Requires `accounts.view`. A tenant user only ever sees their own account, enforced in the query rather than by filtering afterwards."
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Accounts retrieved.\",\n  \"data\": [\n    {\n      \"id\": 1,\n      \"name\": \"Sunrise Cafe Ltd\",\n      \"kra_pin\": \"P051234567X\",\n      \"status\": \"active\",\n      \"pos_integrations_count\": 2\n    }\n  ],\n  \"pagination\": {\n    \"total\": 1,\n    \"per_page\": 25,\n    \"current_page\": 1,\n    \"last_page\": 1,\n    \"from\": 1,\n    \"to\": 1\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "List users",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/users",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "users"
              ]
            },
            "description": "Requires `users.view`."
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Users retrieved.\",\n  \"data\": [\n    {\n      \"id\": 2,\n      \"name\": \"Sunrise Admin\",\n      \"email\": \"admin@sunrisecafe.co.ke\",\n      \"account_id\": 1,\n      \"status\": \"active\",\n      \"roles\": [\n        \"account-admin\"\n      ]\n    }\n  ],\n  \"pagination\": {\n    \"total\": 1,\n    \"per_page\": 25,\n    \"current_page\": 1,\n    \"last_page\": 1,\n    \"from\": 1,\n    \"to\": 1\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Create user",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/users",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "users"
              ]
            },
            "description": "Requires `users.manage`.\n\n**Guards**\n- A tenant admin may only create users inside their own account.\n- A tenant admin may **not** assign a platform role — that is privilege escalation\n  and returns 403.\n- Passwords must be at least 12 characters with letters, numbers and symbols.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Jane Wanjiru\",\n  \"email\": \"jane@sunrisecafe.co.ke\",\n  \"password\": \"Str0ng!Passw0rd!\",\n  \"account_id\": 1,\n  \"roles\": [\n    \"account-viewer\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Created",
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"User created.\",\n  \"data\": {\n    \"id\": 5,\n    \"name\": \"Jane Wanjiru\",\n    \"email\": \"jane@sunrisecafe.co.ke\",\n    \"account_id\": 1,\n    \"roles\": [\n      \"account-viewer\"\n    ]\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "403 Escalation attempt",
              "status": "Forbidden",
              "code": 403,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Platform roles cannot be assigned by an account user.\",\n  \"errors\": {\n    \"roles\": [\n      \"Contains one or more platform-only roles.\"\n    ]\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "List roles and their permissions",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/roles",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "roles"
              ]
            },
            "description": "Requires `users.view`. The complete authorization matrix, as seeded."
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Success\",\n  \"data\": [\n    {\n      \"id\": 1,\n      \"name\": \"platform-admin\",\n      \"is_platform\": true,\n      \"permissions\": [\n        \"accounts.view\",\n        \"accounts.manage\",\n        \"billing.payments.record\"\n      ]\n    }\n  ]\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "4. POS integrations",
      "description": "\nThe centre of gravity of the system. Every credential, mapping, subscription and\ndocument hangs off a POS integration rather than the account — which is what\nmakes *one account, many POS systems, billed separately, different formats* fall\nout naturally.\n",
      "item": [
        {
          "name": "List integrations",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations?per_page=25",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations"
              ],
              "query": [
                {
                  "key": "per_page",
                  "value": "25"
                }
              ]
            },
            "description": "Requires `integrations.view`. Each row carries `may_submit`, which answers 'is this till currently filing?' in one field."
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"POS integrations retrieved.\",\n  \"data\": [\n    {\n      \"id\": 10,\n      \"account_id\": 1,\n      \"name\": \"Sunrise Restaurant Till\",\n      \"code\": \"sunrise-restaurant\",\n      \"branch_code\": \"00\",\n      \"provider\": \"thirdparty\",\n      \"result_delivery_mode\": \"webhook\",\n      \"webhook_url\": \"https://sunrisecafe.co.ke/hooks/kra\",\n      \"status\": \"active\",\n      \"accepts_ingestion\": true,\n      \"may_submit\": true,\n      \"subscription\": {\n        \"id\": 3,\n        \"status\": \"active\",\n        \"price\": 3000.0,\n        \"currency\": \"KES\",\n        \"interval\": \"month\",\n        \"interval_count\": 1,\n        \"paid_through_at\": \"2026-07-31\",\n        \"grace_days\": 7,\n        \"days_until_expiry\": 10\n      }\n    }\n  ],\n  \"pagination\": {\n    \"total\": 1,\n    \"per_page\": 25,\n    \"current_page\": 1,\n    \"last_page\": 1,\n    \"from\": 1,\n    \"to\": 1\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "if (b.data && b.data.length) { pm.collectionVariables.set('integration_id', b.data[0].id); }",
                  "pm.test('ok', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        },
        {
          "name": "Create integration",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations"
              ]
            },
            "description": "Requires `integrations.manage`.\n\nCreates the integration **and its subscription** in one transaction: an\nintegration with no subscription would ingest but never submit, which is a\nconfiguration trap rather than a useful state.\n\n**Terms are snapshot** from the plan at this moment. Raising the plan price later\ndoes not reprice this customer.\n\n- `code` — unique within the account, `[A-Za-z0-9_-]`.\n- `provider` — the fiscalisation aggregator this till files through: `digitax`, `advatech` (and the internal `mock`/`thirdparty`). One provider per till. Direct KRA is planned.\n- `auth_scheme` — how this till's POS authenticates *inbound*: `bearer` (default; a till key in the header) or `sender_id` (a secret in the JSON body at `Invoice.SenderId`, for a POS that cannot send headers). Distinct from `provider`, which is *outbound*. A `sender_id` till files at `/v1/pos/ingest` and needs a registered SenderId.\n- `result_delivery_mode` — `async` (poll), `webhook` (we call you), `sync` (we hold the request).\n- `webhook_url` is required when the mode is `webhook`; a signing secret is minted automatically.\n- `trial_periods` — optional free periods granted at onboarding.\n\n**A real aggregator files under the merchant's OWN credentials** (tied to their\nKRA PIN). So a till cannot be pointed at `digitax` or `advatech` until that\naggregator key is connected for the account — otherwise this returns 422.\n\n**Callback URLs are checked, not just parsed.** We resolve the host and refuse\nanything on a private, loopback, link-local or otherwise reserved network —\nincluding `169.254.169.254`, `127.0.0.1`, `10.x`, `192.168.x` and their IPv6\nequivalents. Only `http` and `https`, and no credentials in the URL.\n\nThe same check runs again immediately before every callback, so a hostname that\nresolves publicly today and privately tomorrow is still refused. Redirects are\nnot followed.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"account_id\": 1,\n  \"name\": \"Sunrise Restaurant Till\",\n  \"code\": \"sunrise-restaurant\",\n  \"branch_code\": \"00\",\n  \"provider\": \"digitax\",\n  \"auth_scheme\": \"bearer\",\n  \"result_delivery_mode\": \"webhook\",\n  \"webhook_url\": \"https://sunrisecafe.co.ke/hooks/kra\",\n  \"billing_plan_code\": \"standard_monthly\",\n  \"grace_days\": 7,\n  \"trial_periods\": 1\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Created",
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"POS integration created.\",\n  \"data\": {\n    \"id\": 10,\n    \"account_id\": 1,\n    \"name\": \"Sunrise Restaurant Till\",\n    \"code\": \"sunrise-restaurant\",\n    \"provider\": \"digitax\",\n    \"result_delivery_mode\": \"webhook\",\n    \"status\": \"active\",\n    \"may_submit\": true,\n    \"subscription\": {\n      \"status\": \"active\",\n      \"price\": 3000.0,\n      \"paid_through_at\": \"2026-08-20\"\n    }\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Aggregator not connected",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Connect a digitax key for this account before pointing an integration at it. A filing is made under the merchant’s own aggregator credentials.\",\n  \"errors\": {\n    \"provider\": [\n      \"No active digitax credentials are on file for this account.\"\n    ]\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Duplicate code",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"This account already has a POS integration with the code [sunrise-restaurant]. Codes must be unique within an account.\",\n  \"errors\": {\n    \"code\": [\n      \"Already in use for this account.\"\n    ]\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Callback URL points inside a private network",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Validation failed.\",\n  \"errors\": {\n    \"webhook_url\": [\n      \"That host resolves to an address on a private or reserved network, which we will not send callbacks to.\"\n    ]\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Get integration",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}"
              ]
            },
            "description": "Requires `integrations.view`. The webhook signing secret is never returned."
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Success\",\n  \"data\": {\n    \"id\": 10,\n    \"code\": \"sunrise-restaurant\",\n    \"status\": \"active\",\n    \"may_submit\": true,\n    \"provider\": \"digitax\",\n    \"provider_status\": {\n      \"selected\": \"digitax\",\n      \"label\": \"Digitax\",\n      \"requires_credentials\": true,\n      \"credentials_connected\": true,\n      \"ready_to_file\": true\n    }\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Update integration",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}"
              ]
            },
            "description": "Requires `integrations.manage`. Switching `provider` here migrates the till between aggregators (and, later, to direct KRA) — one integration at a time.\n\n**Switching the aggregator is bound-for-life per document.** A sale half-filed on the old aggregator must never be re-driven on the new one (a duplicate filing). So the switch is refused (422) while ANY document is still in flight — queued, submitting, retrying or held — and it only ever applies to NEW documents; history stays with the aggregator that filed it. Switching TO a real aggregator also requires its credentials on file (as with create).\n\n**Callback URLs are checked, not just parsed.** We resolve the host and refuse\nanything on a private, loopback, link-local or otherwise reserved network —\nincluding `169.254.169.254`, `127.0.0.1`, `10.x`, `192.168.x` and their IPv6\nequivalents. Only `http` and `https`, and no credentials in the URL.\n\nThe same check runs again immediately before every callback, so a hostname that\nresolves publicly today and privately tomorrow is still refused. Redirects are\nnot followed.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Sunrise Restaurant Till (Counter 1)\",\n  \"result_delivery_mode\": \"async\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"POS integration updated.\",\n  \"data\": {\n    \"id\": 10,\n    \"result_delivery_mode\": \"async\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Callback URL points inside a private network",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Validation failed.\",\n  \"errors\": {\n    \"webhook_url\": [\n      \"That host resolves to an address on a private or reserved network, which we will not send callbacks to.\"\n    ]\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Cannot switch aggregator while in flight",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"This integration has 3 document(s) still in flight on [advatech]. Let them finish (or cancel them) before switching aggregator, so nothing is filed twice.\",\n  \"errors\": {\n    \"provider\": [\n      \"Cannot switch while documents are queued, retrying, submitting or held.\"\n    ]\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Pause integration",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/pause",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "pause"
              ]
            },
            "description": "Requires `integrations.manage`. **Not** a data-loss operation: payloads are still accepted and stored, they simply hold. Use this while investigating a mapping problem."
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Integration paused. Payloads are still accepted and stored; nothing will be submitted until it is resumed.\",\n  \"data\": {\n    \"id\": 10,\n    \"status\": \"paused\",\n    \"may_submit\": false\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Resume integration",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/resume",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "resume"
              ]
            },
            "description": "Requires `integrations.manage`. Releases the held backlog in issue order, throttled to the configured batch size."
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Integration resumed. 412 held document(s) released for submission.\",\n  \"data\": {\n    \"id\": 10,\n    \"status\": \"active\",\n    \"may_submit\": true,\n    \"released_documents\": 412\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "View monthly allowance and usage",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/quota",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "quota"
              ]
            },
            "description": "Requires `billing.view`.\n\n**A speedometer, not a limiter.** `enforcement_enabled` is false: documents are\nnever blocked, however far past the allowance a till goes. The number exists so\nwe can warn in good time and start a conversation about a bigger plan."
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Success\",\n  \"data\": {\n    \"pos_integration_id\": 10,\n    \"month\": \"2026-07\",\n    \"limit\": 5000,\n    \"limit_source\": \"plan\",\n    \"used\": 4210,\n    \"remaining\": 790,\n    \"percent\": 84,\n    \"over\": false,\n    \"enforcement_enabled\": false,\n    \"behaviour\": \"Warnings only. Documents are never blocked.\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Rotate webhook signing secret",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/webhook-secret",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "webhook-secret"
              ]
            },
            "description": "Requires `integrations.manage`.\n\nRotating a signing secret **without an overlap is a guaranteed outage**: we would\nstart signing with a secret you do not have yet, and every callback would fail\nverification until you redeployed. So for 24 hours we sign with **both**, and the\nheader carries two `v1` values:\n\n```\nX-KRA-Signature: t=1753000000,v1=<new secret>,v1=<previous secret>\n```\n\n**Accept the callback if either matches.** Deploy the new secret at your own\npace, then drop the old one.\n\nPass `immediate: true` to skip the overlap — for a genuinely leaked secret, where\nbreaking callbacks is better than leaving the leak live.\n\nThe new secret is shown once and never again.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"immediate\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 Rotated with overlap",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Webhook signing secret rotated. The previous secret stays valid until Wed, 22 Jul 2026 14:30, so you can deploy without downtime. Store the new secret now \\u2014 it will not be shown again.\",\n  \"data\": {\n    \"webhook_secret\": \"9f2b7c4e1a8d3f60b5c9e2a7d4f18b03c6e5a29d7b4f1c80\",\n    \"previous_secret_valid_until\": \"2026-07-22T14:30:00+00:00\",\n    \"signature_note\": \"Until the date above, callbacks carry TWO v1 signatures \\u2014 the new secret and the old. Accept the callback if either matches, then remove the old secret.\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "200 Immediate (leaked secret)",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Webhook signing secret rotated immediately. Store it now \\u2014 it will not be shown again.\",\n  \"data\": {\n    \"webhook_secret\": \"1a2b3c...\",\n    \"previous_secret_valid_until\": null,\n    \"signature_note\": \"Callbacks are signed with the new secret only. Any endpoint still using the old one will reject them.\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 No callback configured",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"This integration has no callback URL, so it has no signing secret to rotate.\"\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Provider selection",
      "description": "Choosing which KRA aggregator a POS integration files through — and connecting the merchant's key for it.\n\n**The flow:** *discover* the options (`GET /providers`) → *connect* the merchant's aggregator key (`POST /accounts/{id}/provider-credentials`) → *select* it on the integration (set `provider` on create/update) → confirm the integration's `provider_status.ready_to_file`.\n\nA real aggregator files under the **merchant's own** credentials (tied to their KRA PIN), so the key is per account, per provider, per environment. The `api_key` is **write-only**: stored encrypted, hidden on the model, never returned.",
      "item": [
        {
          "name": "List selectable providers",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/providers",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "providers"
              ]
            },
            "description": "Requires `integrations.view`. The catalogue a UI uses to present the aggregator choice. `slug` is the value you put in the integration's `provider` field; `label` is display text and may change. Internal providers (mock/thirdparty) are never listed."
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Selectable fiscalisation providers.\",\n  \"data\": [\n    {\n      \"slug\": \"digitax\",\n      \"label\": \"Digitax\",\n      \"requires_credentials\": true,\n      \"document_types\": [\"sales_invoice\", \"credit_note\"],\n      \"environments\": [\"sandbox\", \"production\"]\n    },\n    {\n      \"slug\": \"advatech\",\n      \"label\": \"Advatech\",\n      \"requires_credentials\": true,\n      \"document_types\": [\"sales_invoice\", \"credit_note\"],\n      \"environments\": [\"sandbox\", \"production\"]\n    },\n    {\n      \"slug\": \"vscu\",\n      \"label\": \"KRA VSCU (Direct)\",\n      \"requires_credentials\": true,\n      \"document_types\": [\"sales_invoice\", \"credit_note\"],\n      \"environments\": [\"sandbox\", \"production\"]\n    }\n  ]\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ]
        },
        {
          "name": "Connect a provider key",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/accounts/1/provider-credentials",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "accounts",
                "1",
                "provider-credentials"
              ]
            },
            "description": "Requires `integrations.manage`. Connects the merchant's aggregator key so an integration can be pointed at that provider.\n\n- `provider` must be a selectable aggregator (`digitax`, `advatech`).\n- `api_key` is **write-only** — it is stored encrypted and never returned. Digitax authenticates with it as the `X-API-Key` header on every filing.\n- `base_url` **must be Digitax's full server URL including the path prefix — `https://api.digitax.tech/ke/v2`.** Omitting `/ke/v2` files against the bare host and every sale comes back `404 Not Found`. There is no separate sandbox host: sandbox vs production is decided by the key, not the URL.\n- `default_item_class_code` — the KRA item classification code (an **8-digit** code, e.g. `50000000` for food/beverage) used for any sale line that does not carry its own. Digitax **rejects a sale with no class code**, so set this unless every line supplies one.\n- `base_url` is resolved and refused if it points at a private/reserved network — the same SSRF guard as callbacks.\n- Only **one active key** per provider+environment; rotate or revoke to replace it.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"provider\": \"digitax\",\n  \"environment\": \"sandbox\",\n  \"label\": \"Sunrise Digitax\",\n  \"api_key\": \"dgx_live_secret_key_value\",\n  \"business_id\": \"biz-77\",\n  \"default_item_class_code\": \"50000000\",\n  \"base_url\": \"https://api.digitax.tech/ke/v2\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Connected (secret never echoed)",
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Provider credentials connected.\",\n  \"data\": {\n    \"id\": 5,\n    \"account_id\": 1,\n    \"provider\": \"digitax\",\n    \"environment\": \"sandbox\",\n    \"label\": \"Sunrise Digitax\",\n    \"status\": \"active\",\n    \"has_api_key\": true,\n    \"identifiers\": {\n      \"business_id\": \"biz-77\",\n      \"default_item_class_code\": \"50000000\",\n      \"base_url\": \"https://api.digitax.tech/ke/v2\"\n    }\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "409 Already connected",
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"This account already has an active digitax (sandbox) key. Update it to rotate the secret, or revoke it first.\"\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Base URL points inside a private network",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"That base URL resolves to a private or reserved network and will not be used for filing.\",\n  \"errors\": {\n    \"base_url\": [\n      \"Points at a blocked destination.\"\n    ]\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ]
        },
        {
          "name": "Connect a provider key (Advatech)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/accounts/1/provider-credentials",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "accounts",
                "1",
                "provider-credentials"
              ]
            },
            "description": "The Advatech variant of *Connect a provider key*. Advatech files raw eTIMS under the merchant's own KRA PIN, so its key carries more identifiers than Digitax:\n\n- `branch_id` — the aggregator's branch UUID (not the KRA `bhfId`).\n- `tin` — the merchant's KRA PIN.\n- `default_item_class_code` — the KRA item classification used when a product does not state its own.\n- `address` — the merchant's trading address. It is **printed on the KRA receipt** (`adrs`); if it is omitted the sale still files, but the receipt shows a placeholder (`N/A`), so set the real address once.\n\nAs with Digitax, `api_key` is **write-only** — stored encrypted, never returned — and only **one active key** is allowed per provider + environment.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"provider\": \"advatech\",\n  \"environment\": \"production\",\n  \"label\": \"Sunrise Advatech\",\n  \"api_key\": \"etims_live_secret_key_value\",\n  \"base_url\": \"https://etims-sbx.advatechintegrations.com\",\n  \"branch_id\": \"0041b214-5418-4402-888e-273927947a11\",\n  \"tin\": \"P051738747X\",\n  \"default_item_class_code\": \"5020230100\",\n  \"address\": \"Nairobi, Kenya\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Connect a provider key (VSCU Direct)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/accounts/1/provider-credentials",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "accounts",
                "1",
                "provider-credentials"
              ]
            },
            "description": "KRA **VSCU** is the DIRECT-to-KRA provider (Virtual Sales Control Unit) — a LOCAL component running on the merchant's own box, so it is unlike the aggregators:\n\n- **No `api_key`/bearer.** Auth is `tin` + `bhf_id` (the device `cmcKey` is read by the VSCU service from its own disk). `cmckey` is **optional** — the sandbox needs none; supply it only if your production device requires it.\n- **`base_url` is a trusted local endpoint** (e.g. `http://localhost:8088`). Because VSCU is local by design, its base URL is deliberately NOT run through the private/loopback SSRF guard that protects aggregator URLs.\n- **`bhf_id`** is the KRA branch id (e.g. `06`), and `default_item_class_code` is used when a product does not state its own.\n- The Agent **assigns the invoice number** itself (VSCU does not), and a resubmit reuses it — a duplicate is answered with `924` and treated as already-filed, never double-filed.\n\nAfter connecting, set the integration's `provider` to `vscu`; products register (`/items/register`) and sales file (`/v1/pos/documents`) exactly as for any other provider. Direct VSCU only works when the Agent runs on the same box as the merchant's VSCU service.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"provider\": \"vscu\",\n  \"environment\": \"production\",\n  \"label\": \"Sunrise VSCU\",\n  \"base_url\": \"http://localhost:8088\",\n  \"tin\": \"P051738747X\",\n  \"bhf_id\": \"06\",\n  \"default_item_class_code\": \"5059690800\",\n  \"trade_name\": \"Sunrise Cafe Ltd\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "List connected keys",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/accounts/1/provider-credentials",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "accounts",
                "1",
                "provider-credentials"
              ]
            },
            "description": "Requires `integrations.view`. Lists the account's connected aggregator keys. Secrets are never included — only `has_api_key` and the non-secret identifiers."
          },
          "response": []
        },
        {
          "name": "Rotate / update a key",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/accounts/1/provider-credentials/5",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "accounts",
                "1",
                "provider-credentials",
                "5"
              ]
            },
            "description": "Requires `integrations.manage`. Send a new `api_key` to rotate the secret (in place), and/or `label`/`status`. Any identifier fields sent are merged over the stored blob. The new secret is not echoed.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"api_key\": \"dgx_rotated_new_secret\",\n  \"label\": \"Sunrise Digitax (rotated)\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Revoke a key",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/accounts/1/provider-credentials/5",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "accounts",
                "1",
                "provider-credentials",
                "5"
              ]
            },
            "description": "Requires `integrations.manage`. Soft-revokes the key (kept for audit). Integrations still on this provider hold their documents until a key is reconnected — nothing is lost. Revoking frees the one-active-per-provider slot so a fresh key can be connected."
          },
          "response": [
            {
              "name": "200 Revoked",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Provider credentials revoked. Integrations on this provider will hold documents until a key is reconnected.\",\n  \"data\": {\n    \"id\": 5,\n    \"provider\": \"digitax\",\n    \"status\": \"revoked\",\n    \"has_api_key\": true\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ]
        }
      ]
    },
    {
      "name": "5. API credentials",
      "description": "Machine credentials for tills. Several may be live at once so keys can be rotated without downtime.\n\nTwo kinds, per the till's `auth_scheme`: **bearer keys** (key_id + secret, below) for the header endpoint, and **SenderId** credentials for the body-secret endpoint (`sender_id` scheme).",
      "item": [
        {
          "name": "Register a SenderId (body-auth till)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/sender-credentials",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "sender-credentials"
              ]
            },
            "description": "Requires `credentials.manage`. Registers the **provider-generated** `SenderId` (the random secret the POS puts at `Invoice.SenderId`) against this till. We store only its hash — it is **write-only** and never echoed back. One SenderId maps to one credential; a duplicate is a `409`. Revoke to retire it. Only meaningful for a till whose `auth_scheme` is `sender_id`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"sender_id\": \"3c0e19f2f890ea7ca15e\",\n  \"label\": \"Front counter\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Registered",
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"SenderId registered.\",\n  \"data\": { \"id\": 12, \"label\": \"Front counter\", \"usable\": true, \"revoked_at\": null }\n}",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Wrong scheme (till is not sender_id)",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"This till uses the [bearer] authentication scheme, so a SenderId would never authenticate. Create a bearer key at POST /v1/integrations/10/credentials instead, or switch the till's auth_scheme to sender_id first.\",\n  \"errors\": { \"auth_scheme\": [ \"A SenderId can only be registered for a sender_id-scheme till.\" ] }\n}",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "protocolProfileBehavior": {},
          "event": []
        },
        {
          "name": "Revoke a SenderId",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/sender-credentials/{{sender_credential_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "sender-credentials",
                "{{sender_credential_id}}"
              ]
            },
            "description": "Requires `credentials.manage`. Revokes a registered SenderId; it stops authenticating immediately. Because the secret is stored (not redacted), revocation is the primary way to retire a leaked one — register a fresh SenderId and revoke the old."
          },
          "response": []
        },
        {
          "name": "List credentials",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/credentials",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "credentials"
              ]
            },
            "description": "Requires `credentials.view`. Metadata only — the secret and its hash are never returned."
          },
          "response": [
            {
              "name": "200 Mid-rotation",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Success\",\n  \"data\": [\n    {\n      \"id\": 9,\n      \"key_id\": \"kra_n3wk3yh3re000000000000\",\n      \"label\": \"Front counter (rotated)\",\n      \"last_used_at\": \"2026-07-21T14:35:00+00:00\",\n      \"expires_at\": null,\n      \"revoked_at\": null,\n      \"usable\": true,\n      \"retiring\": false,\n      \"hours_until_expiry\": null,\n      \"superseded_by_id\": null,\n      \"safe_to_revoke\": false\n    },\n    {\n      \"id\": 5,\n      \"key_id\": \"kra_x8v9zjglzknr3sniwflk\",\n      \"label\": \"Front counter\",\n      \"last_used_at\": \"2026-07-21T14:22:00+00:00\",\n      \"expires_at\": \"2026-07-22T14:30:00+00:00\",\n      \"revoked_at\": null,\n      \"usable\": true,\n      \"retiring\": true,\n      \"hours_until_expiry\": 23.9,\n      \"superseded_by_id\": 9,\n      \"safe_to_revoke\": true\n    }\n  ]\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Create credential",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/credentials",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "credentials"
              ]
            },
            "description": "Requires `credentials.manage`.\n\nThe secret is returned **once** and is stored only as a hash — exactly like a\npassword. If it is lost, mint a new one and revoke the old.\n\n`authorization_header` is provided ready to paste into the till's configuration.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"label\": \"Front counter till\",\n  \"expires_at\": null\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Created",
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"API credential created. Copy the secret now \\u2014 it will not be shown again.\",\n  \"data\": {\n    \"id\": 5,\n    \"key_id\": \"kra_x8v9zjglzknr3sniwflk\",\n    \"secret\": \"sk_ILdUug8GyNunXOIecHkb3XdBoj6brNT03UmmMWGuLAR304El\",\n    \"authorization_header\": \"Bearer kra_x8v9zjglzknr3sniwflk.sk_ILdUug8GyNunXOIecHkb3XdBoj6brNT03UmmMWGuLAR304El\",\n    \"label\": \"Front counter till\",\n    \"expires_at\": null\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Wrong scheme (till is sender_id)",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"This till uses the [sender_id] authentication scheme, so a bearer key would never authenticate. Register a SenderId at POST /v1/integrations/10/sender-credentials instead.\",\n  \"errors\": { \"auth_scheme\": [ \"A bearer key can only be issued for a bearer-scheme till.\" ] }\n}",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "if (b.success) {",
                  "  pm.collectionVariables.set('pos_key_id', b.data.key_id);",
                  "  pm.collectionVariables.set('pos_secret', b.data.secret);",
                  "}",
                  "pm.test('created', () => pm.response.to.have.status(201));"
                ]
              }
            }
          ]
        },
        {
          "name": "Rotate credential (zero downtime)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/credentials/5/rotate",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "credentials",
                "5",
                "rotate"
              ]
            },
            "description": "Requires `credentials.manage`.\n\nMints a replacement **and** schedules the old key's death in one operation. A\nmerchant with three physical tills has to walk to each one: revoking first stops\nall three trading until the last is updated, and minting first without revoking\nleaves a key live forever. This does both halves, so nobody has to remember\nanything.\n\n- `grace_hours` — how long the old key keeps working. Defaults to 24.\n- `grace_hours: 0` — kill it immediately. For a genuine leak, where an outage is\n  the lesser evil.\n\nThe listing then reports `retiring`, `hours_until_expiry` and `safe_to_revoke`\n(true once nothing has used the old key since the rotation), so revoking early is\nan informed choice rather than a guess. If a till is *still* using the old key as\nthe window closes, we email the merchant before it stops working.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"grace_hours\": 24,\n  \"label\": \"Front counter (rotated)\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Rotated",
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"New API credential created. The old key keeps working until Wed, 22 Jul 2026 14:30, so you can update your tills one at a time with no downtime. Copy the secret now \\u2014 it will not be shown again.\",\n  \"data\": {\n    \"id\": 9,\n    \"key_id\": \"kra_n3wk3yh3re000000000000\",\n    \"secret\": \"sk_ZqR7...\",\n    \"authorization_header\": \"Bearer kra_n3wk3yh3re000000000000.sk_ZqR7...\",\n    \"label\": \"Front counter (rotated)\",\n    \"replaces\": {\n      \"id\": 5,\n      \"key_id\": \"kra_x8v9zjglzknr3sniwflk\",\n      \"stops_working_at\": \"2026-07-22T14:30:00+00:00\",\n      \"last_used_at\": \"2026-07-21T14:22:00+00:00\"\n    }\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "201 Immediate (leaked key)",
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"New API credential created and the old key stopped working immediately. Any till still using it will be rejected until you update it. Copy the secret now \\u2014 it will not be shown again.\",\n  \"data\": {\n    \"id\": 10,\n    \"key_id\": \"kra_emergency00000000000000\",\n    \"replaces\": {\n      \"id\": 5,\n      \"stops_working_at\": \"2026-07-21T14:30:00+00:00\"\n    }\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Already revoked",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"This credential is already revoked or expired, so there is nothing to roll over from. Create a new credential instead.\"\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('created', () => pm.response.to.have.status(201));",
                  "const b = pm.response.json();",
                  "if (b.success) {",
                  "  pm.collectionVariables.set('pos_key_id', b.data.key_id);",
                  "  pm.collectionVariables.set('pos_secret', b.data.secret);",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Revoke credential",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/credentials/5",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "credentials",
                "5"
              ]
            },
            "description": "Requires `credentials.manage`. Takes effect immediately on the next request."
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"API credential revoked. It will stop working immediately.\",\n  \"data\": {\n    \"id\": 5,\n    \"revoked_at\": \"2026-07-21T11:00:00+00:00\",\n    \"usable\": false\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Already revoked",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"This credential was already revoked on 2026-07-20.\"\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "6. Payload & code mappings",
      "description": "\nHow a POS's private JSON becomes a canonical document — held as **data**, so\nonboarding the twelfth POS format is a row insert, not a deploy.\n\n**Rule shape**\n\n```jsonc\n{\n  \"document\": {\n    \"source_document_ref\": \"receipt.no\",                          // dot path\n    \"issued_at\": {\"path\": \"receipt.issued\", \"format\": \"iso8601\"}, // or \"d/m/Y\"\n    \"total_incl_tax\": {\"path\": \"totals.gross\", \"type\": \"decimal\"}\n  },\n  \"lines\": {\n    \"path\": \"items\",\n    \"fields\": {\n      \"description\": \"desc\",\n      \"quantity\":   {\"path\": \"qty\", \"type\": \"decimal\", \"default\": 1},\n      \"unit_price\": {\"path\": \"price\", \"type\": \"decimal\"},\n      \"source_tax_code\": \"vat\"\n    }\n  },\n  \"options\": {\"amounts_include_tax\": true, \"total_tolerance\": 0.01}\n}\n```\n\nA field spec is either a dot-path string or an object with\n`{path, default, required, type, format, transform}`.\n`type`: `string|decimal|integer|boolean|datetime`.\n`transform`: `upper|lower|trim|digits`.\n",
      "item": [
        {
          "name": "List mapping versions",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/mappings",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "mappings"
              ]
            },
            "description": "Requires `mappings.view`. Versions are never edited — a document already filed keeps pointing at the rules that produced it."
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Success\",\n  \"data\": [\n    {\n      \"id\": 31,\n      \"document_type\": \"sales_invoice\",\n      \"version\": 2,\n      \"is_active\": true,\n      \"notes\": \"Added customer PIN\",\n      \"rules\": {\n        \"document\": {\n          \"source_document_ref\": \"receipt.no\"\n        }\n      }\n    }\n  ]\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Suggest a mapping from a sample",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/mappings/suggest",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "mappings",
                "suggest"
              ]
            },
            "description": "Requires `mappings.view`.\n\nPaste a sample of the POS's JSON and get a **draft mapping back for review** —\nit stores nothing and activates nothing. This turns onboarding a new format from\n\"write a mapping by hand\" into \"confirm a draft.\"\n\nThe response sorts every field into three trays:\n\n| Tray | Meaning |\n|---|---|\n| `confident` | high-confidence structural guesses — glance and move on |\n| `confirm` | a best guess plus alternatives, for the risky calls (which field is the invoice number?) |\n| `supply` | things the machine cannot know — the KRA meaning of a tax code, the timezone |\n\n**The one dangerous decision — tax-inclusive vs exclusive — is derived, not\nguessed.** A tax invoice carries its own answer key (its declared totals), so we\nreproduce them under each assumption and keep whichever reconciles. `pricing`\nshows the working and whether it is confident.\n\nThen: `POST proposed_rules` to **/mappings/test** to preview the computed\ninvoice, and to **/mappings** to activate. A human always confirms.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"document_type\": \"sales_invoice\",\n  \"payload\": {\n    \"Invoice\": {\n      \"SenderId\": \"3c0e19f2f890ea7ca15e\",\n      \"InvoiceTimestamp\": \"2024-03-16T20:47:49\",\n      \"TraderSystemInvoiceNumber\": \"REC-57\",\n      \"RelevantInvoiceNumber\": \"\",\n      \"PINOfBuyer\": \"\",\n      \"TotalInvoiceAmount\": 3047.48,\n      \"TotalTaxableAmount\": 2627.13,\n      \"TotalTaxAmount\": 420.35,\n      \"ItemDetails\": [\n        {\n          \"HSDesc\": \"Strawberry Milkshake\",\n          \"TaxRate\": 16,\n          \"ItemAmount\": 381.3559,\n          \"TaxAmount\": 61.0169,\n          \"UnitPrice\": 381.3559,\n          \"HSCode\": \"\",\n          \"Quantity\": 1\n        },\n        {\n          \"HSDesc\": \"Beef Burger\",\n          \"TaxRate\": 16,\n          \"ItemAmount\": 1694.9152,\n          \"TaxAmount\": 271.1864,\n          \"UnitPrice\": 847.4576,\n          \"HSCode\": \"\",\n          \"Quantity\": 2\n        },\n        {\n          \"HSDesc\": \"BBQ Wings\",\n          \"TaxRate\": 16,\n          \"ItemAmount\": 550.8475,\n          \"TaxAmount\": 88.1356,\n          \"UnitPrice\": 550.8475,\n          \"HSCode\": \"\",\n          \"Quantity\": 1\n        }\n      ]\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 Draft proposed",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Draft mapping proposed. Nothing has been stored \\u2014 review it, dry-run it, then activate.\",\n  \"data\": {\n    \"document_type\": \"sales_invoice\",\n    \"confidence\": \"medium\",\n    \"proposed_rules\": {\n      \"document\": {\n        \"source_document_ref\": \"Invoice.TraderSystemInvoiceNumber\",\n        \"issued_at\": {\n          \"path\": \"Invoice.InvoiceTimestamp\",\n          \"format\": \"iso8601\"\n        },\n        \"customer_pin\": \"Invoice.PINOfBuyer\"\n      },\n      \"lines\": {\n        \"path\": \"Invoice.ItemDetails\",\n        \"fields\": {\n          \"description\": \"HSDesc\",\n          \"quantity\": {\n            \"path\": \"Quantity\",\n            \"type\": \"decimal\"\n          },\n          \"unit_price\": {\n            \"path\": \"UnitPrice\",\n            \"type\": \"decimal\"\n          },\n          \"source_item_code\": \"HSCode\",\n          \"source_tax_code\": \"TaxRate\"\n        }\n      },\n      \"options\": {\n        \"amounts_include_tax\": false\n      }\n    },\n    \"review\": {\n      \"confident\": [\n        {\n          \"field\": \"lines.quantity\",\n          \"path\": \"Quantity\",\n          \"reason\": \"Matched a known field name.\"\n        }\n      ],\n      \"confirm\": [\n        {\n          \"field\": \"source_document_ref\",\n          \"proposed_path\": \"Invoice.TraderSystemInvoiceNumber\",\n          \"alternatives\": [\n            \"Invoice.SenderId\"\n          ],\n          \"reason\": \"Best guess for the invoice number. Confirm it is not a device or sender id.\"\n        }\n      ],\n      \"supply\": [\n        {\n          \"what\": \"tax_code_mappings\",\n          \"detail\": \"Tax values seen: [16]. Map each to a KRA tax code and rate \\u2014 the meaning cannot be inferred. Note: two different codes can both be 0% (exempt vs zero-rated).\"\n        },\n        {\n          \"what\": \"item_codes\",\n          \"detail\": \"The item-code field is present but empty on every line. If KRA requires item classification, map descriptions to codes or confirm the aggregator supplies them.\"\n        },\n        {\n          \"what\": \"timezone\",\n          \"detail\": \"The timestamp carries no timezone. Confirm the till's timezone (e.g. Africa/Nairobi) on the integration, or a late-night sale will land on the wrong business day.\"\n        }\n      ]\n    },\n    \"pricing\": {\n      \"amounts_include_tax\": false,\n      \"method\": \"per_line\",\n      \"derivation\": \"Compared quantity \\u00d7 unit price against each line's declared amount: 3 line(s) match tax-exclusive pricing, 0 match the other. Chose exclusive.\",\n      \"confident\": true\n    },\n    \"codes_to_map\": {\n      \"tax\": [\n        \"16\"\n      ],\n      \"item\": []\n    },\n    \"next_steps\": \"Review the trays, then POST proposed_rules to /integrations/{id}/mappings/test to preview the computed invoice, and /integrations/{id}/mappings to activate. Nothing has been stored.\"\n  }\n}",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Empty payload",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Validation failed.\",\n  \"errors\": {\n    \"payload\": [\n      \"The payload field is required.\"\n    ]\n  }\n}",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "413 Sample too large",
              "status": "Content Too Large",
              "code": 413,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Sample payload is too large.\"\n}",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('draft returned', ()=>pm.response.to.have.status(200));",
                  "const b=pm.response.json();",
                  "pm.test('nothing was activated', ()=>pm.expect(b.data.proposed_rules).to.be.an('object'));"
                ]
              }
            }
          ]
        },
        {
          "name": "Dry-run a mapping",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/mappings/test",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "mappings",
                "test"
              ]
            },
            "description": "Requires `mappings.view`. **Stores nothing.**\n\nThis is the tool that makes onboarding a configuration task: paste the merchant's\nJSON, see exactly what would be filed, iterate. Pass `rules` to try an unsaved\nmapping, `mapping_id` to try a specific version, or neither to use the active one.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"document_type\": \"sales_invoice\",\n  \"payload\": {\n    \"receipt\": {\n      \"no\": \"RST-8891\",\n      \"issued\": \"2026-07-21T10:04:00+03:00\"\n    },\n    \"customer\": {\n      \"name\": \"Walk-in\",\n      \"pin\": null\n    },\n    \"items\": [\n      {\n        \"sku\": \"BF-01\",\n        \"desc\": \"Beef Stew\",\n        \"qty\": 2,\n        \"price\": 650.0,\n        \"vat\": \"A\"\n      },\n      {\n        \"sku\": \"CH-02\",\n        \"desc\": \"Chapati\",\n        \"qty\": 3,\n        \"price\": 50.0,\n        \"vat\": \"A\"\n      }\n    ],\n    \"totals\": {\n      \"gross\": 1450.0\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 Transformed",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Payload transformed. Nothing was stored.\",\n  \"data\": {\n    \"document\": {\n      \"source_document_ref\": \"RST-8891\",\n      \"currency\": \"KES\",\n      \"total_incl_tax\": 1450.0,\n      \"total_tax\": 200.0,\n      \"total_excl_tax\": 1250.0\n    },\n    \"lines\": [\n      {\n        \"line_no\": 1,\n        \"item_code\": \"1234567890\",\n        \"description\": \"Beef Stew\",\n        \"quantity\": 2.0,\n        \"unit_price\": 650.0,\n        \"tax_code\": \"VAT_16\",\n        \"tax_rate\": 16.0,\n        \"taxable_amount\": 1120.69,\n        \"tax_amount\": 179.31,\n        \"line_total\": 1300.0\n      }\n    ],\n    \"mapping_version\": 2\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Mapping error",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Required field [source_document_ref] was not found at path [receipt.no] in the payload.\",\n  \"errors\": {\n    \"error_code\": \"MISSING_FIELD\",\n    \"field\": \"source_document_ref\",\n    \"path\": \"receipt.no\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Publish a mapping version",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/mappings",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "mappings"
              ]
            },
            "description": "Requires `mappings.manage`. Creates the next version number and, unless `activate` is false, makes it the one in force. Exactly one version per document type is ever active.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"document_type\": \"sales_invoice\",\n  \"activate\": true,\n  \"notes\": \"Nested receipt format, tax-inclusive prices\",\n  \"rules\": {\n    \"document\": {\n      \"source_document_ref\": \"receipt.no\",\n      \"issued_at\": {\n        \"path\": \"receipt.issued\",\n        \"format\": \"iso8601\"\n      },\n      \"customer_name\": \"customer.name\",\n      \"total_incl_tax\": {\n        \"path\": \"totals.gross\",\n        \"type\": \"decimal\"\n      }\n    },\n    \"lines\": {\n      \"path\": \"items\",\n      \"fields\": {\n        \"source_item_code\": \"sku\",\n        \"description\": \"desc\",\n        \"quantity\": {\n          \"path\": \"qty\",\n          \"type\": \"decimal\",\n          \"default\": 1\n        },\n        \"unit_price\": {\n          \"path\": \"price\",\n          \"type\": \"decimal\"\n        },\n        \"discount\": {\n          \"path\": \"disc\",\n          \"type\": \"decimal\",\n          \"default\": 0\n        },\n        \"source_tax_code\": \"vat\"\n      }\n    },\n    \"options\": {\n      \"amounts_include_tax\": true,\n      \"total_tolerance\": 0.01\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Created",
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Mapping version 3 created.\",\n  \"data\": {\n    \"id\": 33,\n    \"document_type\": \"sales_invoice\",\n    \"version\": 3,\n    \"is_active\": true\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Financial type needs lines",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"A mapping for [sales_invoice] must declare rules.lines.path \\u2014 a financial document is built from its lines.\",\n  \"errors\": {\n    \"rules.lines.path\": [\n      \"Required for financial document types.\"\n    ]\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Activate an older version (roll back)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/mappings/31/activate",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "mappings",
                "31",
                "activate"
              ]
            },
            "description": "Requires `mappings.manage`. Rolling back is a first-class operation because a bad mapping is discovered in production, not in review."
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Mapping version 1 is now active.\",\n  \"data\": {\n    \"id\": 31,\n    \"version\": 1,\n    \"is_active\": true\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "List code mappings",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/code-mappings?type=tax&per_page=50",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "code-mappings"
              ],
              "query": [
                {
                  "key": "type",
                  "value": "tax"
                },
                {
                  "key": "per_page",
                  "value": "50"
                }
              ]
            },
            "description": "Requires `mappings.view`. Types: `tax`, `item`, `unit`, `payment_method`."
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Code mappings retrieved.\",\n  \"data\": [\n    {\n      \"id\": 1,\n      \"type\": \"tax\",\n      \"pos_code\": \"A\",\n      \"target_code\": \"VAT_16\",\n      \"tax_rate\": \"16.00\",\n      \"description\": \"Standard rated\"\n    }\n  ],\n  \"pagination\": {\n    \"total\": 1,\n    \"per_page\": 50,\n    \"current_page\": 1,\n    \"last_page\": 1,\n    \"from\": 1,\n    \"to\": 1\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Upsert code mappings (bulk)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/code-mappings",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "code-mappings"
              ]
            },
            "description": "Requires `mappings.manage`. Upserts up to 1,000 at a time — onboarding sends\nhundreds.\n\n**A `tax` mapping must state its `tax_rate` explicitly.** An unmapped or\nun-rated tax code is refused at ingestion rather than defaulted: guessing a rate\nfiles the wrong amount of tax, and nobody notices until an audit.\n\nAn unmapped **item** code, by contrast, falls back to the source code — an\nunclassified item is a data-quality problem, not a wrong tax figure.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"mappings\": [\n    {\n      \"type\": \"tax\",\n      \"pos_code\": \"A\",\n      \"target_code\": \"VAT_16\",\n      \"tax_rate\": 16,\n      \"description\": \"Standard rated\"\n    },\n    {\n      \"type\": \"tax\",\n      \"pos_code\": \"E\",\n      \"target_code\": \"VAT_EXEMPT\",\n      \"tax_rate\": 0,\n      \"description\": \"Exempt\"\n    },\n    {\n      \"type\": \"tax\",\n      \"pos_code\": \"Z\",\n      \"target_code\": \"VAT_ZERO\",\n      \"tax_rate\": 0,\n      \"description\": \"Zero rated\"\n    },\n    {\n      \"type\": \"item\",\n      \"pos_code\": \"BF-01\",\n      \"target_code\": \"1234567890\",\n      \"description\": \"Beef Stew\"\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 Saved",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"4 code mapping(s) saved.\",\n  \"data\": {\n    \"upserted\": 4\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Tax mapping without a rate",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Mapping #0 maps tax code [A] but gives no tax_rate. A tax mapping must state its rate explicitly.\",\n  \"errors\": {\n    \"mappings.0.tax_rate\": [\n      \"Required for mappings of type tax.\"\n    ]\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Item catalogue",
      "description": "The merchant's products, described once in plain terms, then **registered** with whichever aggregator a till files through.\n\nWhy it matters: eTIMS refuses a sale line for an item it has never seen (`ETIMS_MAPPING_ERROR: no eTIMS item code`). Registering an item obtains the code KRA files under, and — the payoff — the Agent **auto-creates the item code mapping** for you, so sales then resolve cleanly.\n\nProvider-agnostic: for an aggregator that takes items inline (Digitax) the register call is a no-op (`not_required: true`). For one that needs registration (Advatech) the Agent builds the eTIMS item code, calls the aggregator, and records it. Registration is **idempotent** — a product already registered for a till is skipped.\n\nFlow: add products (account-level) → register them for a till → they file.",
      "item": [
        {
          "name": "List catalogue items",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/accounts/{{account_id}}/items",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "accounts",
                "{{account_id}}",
                "items"
              ]
            },
            "description": "Requires `mappings.view`. Each item shows its per-aggregator `registrations` (the code it was assigned, and status)."
          },
          "response": []
        },
        {
          "name": "Add a catalogue item",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/accounts/{{account_id}}/items",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "accounts",
                "{{account_id}}",
                "items"
              ]
            },
            "description": "Requires `mappings.manage`.\n\n- `source_item_code` — what your **till** sends for this product (becomes the item mapping's `pos_code`).\n- `item_class_code` — the KRA item classification (`itemClsCd`), chosen from the aggregator's code list.\n- `tax_code` — canonical tax (e.g. `VAT_16`).\n- `packaging_unit` / `quantity_unit` / `origin_country` / `item_type_code` — eTIMS attributes (sensible defaults `NT` / `U` / `KE` / `2`).\n\nOne item per `source_item_code` per account — a duplicate returns `409`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"source_item_code\": \"MILK-01\",\n  \"name\": \"Strawberry Milkshake\",\n  \"item_class_code\": \"5020230100\",\n  \"tax_code\": \"VAT_16\",\n  \"item_type_code\": \"2\",\n  \"packaging_unit\": \"NT\",\n  \"quantity_unit\": \"U\",\n  \"origin_country\": \"KE\",\n  \"default_price\": 381.36\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Created",
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Catalogue item created.\",\n  \"data\": {\n    \"id\": 7,\n    \"source_item_code\": \"MILK-01\",\n    \"name\": \"Strawberry Milkshake\",\n    \"item_class_code\": \"5020230100\",\n    \"tax_code\": \"VAT_16\",\n    \"default_price\": 381.36,\n    \"status\": \"active\",\n    \"registrations\": []\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ]
        },
        {
          "name": "Update a catalogue item",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/accounts/{{account_id}}/items/7",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "accounts",
                "{{account_id}}",
                "items",
                "7"
              ]
            },
            "description": "Requires `mappings.manage`. Send only the fields you want to change.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"default_price\": 400.00\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Register catalogue with a till's aggregator",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/items/register",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "items",
                "register"
              ]
            },
            "description": "Requires `mappings.manage`. Registers the account's active catalogue with this till's aggregator and **auto-creates the item code mappings** for the ones that succeed. Optional `item_ids` limits it to specific items; omit to register all.\n\n- Advatech: builds each eTIMS item code and calls `Save Items`.\n- Digitax (inline): `not_required: true`, nothing to do.\n- **Idempotent:** items already registered for this till are skipped.",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 Registered (Advatech)",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"1 registered, 0 failed, 0 already done.\",\n  \"data\": {\n    \"processed\": 1,\n    \"registered\": 1,\n    \"failed\": 0,\n    \"skipped\": 0,\n    \"not_required\": false,\n    \"results\": [\n      { \"source_item_code\": \"MILK-01\", \"item_cd\": \"KE2NTU0000001\", \"sequence\": 1, \"status\": \"registered\", \"error\": null }\n    ]\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "200 Not required (inline aggregator)",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"This aggregator takes items inline; no registration was needed.\",\n  \"data\": { \"processed\": 0, \"registered\": 0, \"failed\": 0, \"skipped\": 3, \"not_required\": true, \"results\": [] }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ]
        },
        {
          "name": "Sync KRA code lists",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/reference/sync",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "reference",
                "sync"
              ]
            },
            "description": "Requires `mappings.manage`. Pulls this till's provider KRA code lists — quantity/packing units, payment types, item classifications, refund reasons, countries — into the cache. VSCU serves them (`selectCodes` + `selectItemsClass`); a provider that doesn't returns `supported: false`. Run it once during onboarding (and again if KRA updates its codes). The reply reports how many codes were synced."
          },
          "response": [
            {
              "name": "200 Synced",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"1580 reference codes synced from vscu.\",\n  \"data\": { \"supported\": true, \"synced\": 1580 }\n}",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ]
        },
        {
          "name": "Look up KRA codes",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/reference?type=quantity_unit&search=",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "reference"
              ],
              "query": [
                {
                  "key": "type",
                  "value": "quantity_unit",
                  "description": "quantity_unit | packaging_unit | payment_type | item_class | country | refund_reason | tax_type | item_type"
                },
                {
                  "key": "search",
                  "value": "",
                  "description": "Matches code or label (e.g. a product name to find its item_class)."
                }
              ]
            },
            "description": "Requires `mappings.view`. Browse / search the synced KRA codes for this till's provider, so onboarding can pick valid codes for a product. Filter by `type` and `search` (matches code or label), paginated.\n\n**Units** (`quantity_unit`, `packaging_unit`) are complete lists and are **validated** when you create a catalogue item — a bad one is refused. **`item_class`** is lookup-only: the KRA list is large/incomplete, so it is never used to reject a product, only to help you find the right classification."
          },
          "response": [
            {
              "name": "200 Codes",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Reference codes.\",\n  \"data\": [\n    { \"code_type\": \"quantity_unit\", \"code\": \"U\", \"label\": \"Each\", \"extra\": { \"class\": \"10\" } },\n    { \"code_type\": \"quantity_unit\", \"code\": \"KG\", \"label\": \"Kilogram\", \"extra\": { \"class\": \"10\" } }\n  ],\n  \"pagination\": { \"total\": 43, \"per_page\": 25, \"current_page\": 1, \"last_page\": 2, \"from\": 1, \"to\": 25 }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ]
        }
      ]
    },
    {
      "name": "7. Documents",
      "description": "\n**Lifecycle**\n\n```\nreceived → normalized ─┬→ queued → submitting ─┬→ accepted\n     │                 │                       ├→ rejected\n     │                 │                       └→ retry_scheduled → queued\n     │                 └→ on_hold ──(payment)──→ queued\n     └→ normalization_failed\n                                  retries exhausted → dead_letter\n```\n\n`on_hold` is **not** a failure: it consumes no retry budget and can never reach\ndead-letter. Transitions happen through named endpoints; there is no generic\n\"update status\".\n",
      "item": [
        {
          "name": "List documents",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/documents?status=on_hold,retry_scheduled&from=2026-07-01&per_page=25",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "documents"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "on_hold,retry_scheduled"
                },
                {
                  "key": "from",
                  "value": "2026-07-01"
                },
                {
                  "key": "per_page",
                  "value": "25"
                }
              ]
            },
            "description": "Requires `documents.view`. Filters: `pos_integration_id`, `status` (comma-separated), `document_type`, `reference`, `from`, `to`, `needs_attention=1`. Always scoped to the caller's account."
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Documents retrieved.\",\n  \"data\": [\n    {\n      \"id\": 90001,\n      \"pos_integration_id\": 10,\n      \"document_type\": \"sales_invoice\",\n      \"reference\": \"RST-8891\",\n      \"status\": \"accepted\",\n      \"issued_at\": \"2026-07-21T10:04:00+00:00\",\n      \"currency\": \"KES\",\n      \"total_excl_tax\": 1250.0,\n      \"total_tax\": 200.0,\n      \"total_incl_tax\": 1450.0,\n      \"kra_control_code\": \"KRACU00918273645\",\n      \"attempt_count\": 1,\n      \"last_error_code\": null\n    }\n  ],\n  \"pagination\": {\n    \"total\": 1,\n    \"per_page\": 25,\n    \"current_page\": 1,\n    \"last_page\": 1,\n    \"from\": 1,\n    \"to\": 1\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Get document with lines and attempts",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/documents/{{document_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "documents",
                "{{document_id}}"
              ]
            },
            "description": "Requires `documents.view`. Includes every submission attempt — the evidence trail when a merchant disputes whether an invoice was filed."
          },
          "response": [
            {
              "name": "200 Success",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Success\",\n  \"data\": {\n    \"id\": 90001,\n    \"reference\": \"RST-8891\",\n    \"status\": \"accepted\",\n    \"kra_control_code\": \"KRACU00918273645\",\n    \"total_incl_tax\": 1450.0,\n    \"totals_agree_with_lines\": true,\n    \"mapping_version\": 2,\n    \"lines\": [\n      {\n        \"line_no\": 1,\n        \"item_code\": \"1234567890\",\n        \"source_item_code\": \"BF-01\",\n        \"description\": \"Beef Stew\",\n        \"quantity\": 2.0,\n        \"unit_price\": 650.0,\n        \"tax_code\": \"VAT_16\",\n        \"source_tax_code\": \"A\",\n        \"tax_rate\": 16.0,\n        \"taxable_amount\": 1120.69,\n        \"tax_amount\": 179.31,\n        \"line_total\": 1300.0\n      }\n    ],\n    \"attempts\": [\n      {\n        \"attempt_no\": 1,\n        \"provider\": \"thirdparty\",\n        \"outcome\": \"transient_failure\",\n        \"http_status\": null,\n        \"error_code\": \"TIMEOUT\",\n        \"duration_ms\": 30000,\n        \"attempted_at\": \"2026-07-21T10:04:31+00:00\"\n      },\n      {\n        \"attempt_no\": 2,\n        \"provider\": \"thirdparty\",\n        \"outcome\": \"accepted\",\n        \"http_status\": 200,\n        \"error_code\": null,\n        \"duration_ms\": 812,\n        \"attempted_at\": \"2026-07-21T10:09:33+00:00\"\n      }\n    ]\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Retry a document",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/documents/{{document_id}}/retry",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "documents",
                "{{document_id}}",
                "retry"
              ]
            },
            "description": "Requires `documents.retry`.\n\nApplies to `rejected`, `dead_letter` and `retry_scheduled` documents.\n`reset_attempts` clears the consumed retry budget — the append-only attempt log\nis untouched, so history is preserved.\n\n**An `accepted` document can never be retried.** It already carries a control\ncode; re-filing it would create a duplicate tax record. That refusal is\nunconditional and no permission overrides it.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reset_attempts\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 Queued",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Document queued for another submission attempt.\",\n  \"data\": {\n    \"id\": 90001,\n    \"status\": \"queued\",\n    \"attempt_count\": 0\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Already accepted",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"This document has already been accepted (control code KRACU00918273645). Re-submitting it would create a duplicate filing.\"\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Reprocess a normalisation failure",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/documents/{{document_id}}/reprocess",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "documents",
                "{{document_id}}",
                "reprocess"
              ]
            },
            "description": "Requires `documents.retry`.\n\n**Reprocess is not retry.** Retry re-SUBMITS a document that already normalised. Reprocess re-runs the NORMALISATION stage — the thing that failed — from the document's retained raw payload, through the CURRENT active mapping and code mappings. Use it after fixing a mapping or adding a missing code (e.g. an unmapped tax code) to recover a `normalization_failed` document without asking the till to send anything again.\n\nOnly a `normalization_failed` document can be reprocessed. A document that reached the queue, a hold, or a filing is refused with 422 — re-transforming a filed document would risk a duplicate filing, and that line is exactly what keeps idempotency intact. A `normalization_failed` document was never filed, so re-attempting it can never duplicate a filing.\n\nOn success the document is re-normalised in place (same row, same reference — never a second document) and queued for submission; if the subscription is unpaid it is held instead."
          },
          "response": [
            {
              "name": "200 Re-normalised and queued",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Document re-normalised and queued for submission.\",\n  \"data\": {\n    \"id\": 90007,\n    \"reference\": \"REC-57\",\n    \"status\": \"queued\",\n    \"total_tax\": 13.79,\n    \"last_error_code\": null\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Still cannot normalise",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"The document still cannot be normalised: No code mapping exists for tax code [Z].\",\n  \"errors\": {\n    \"error_code\": \"UNMAPPED_TAX_CODE\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Not reprocessable (already past normalisation)",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Only a document that failed normalisation can be reprocessed; this one is [accepted]. Re-transforming a document that reached the queue or a filing would risk a duplicate filing.\"\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Reprocess all failures for an integration",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/integrations/{{integration_id}}/documents/reprocess-failed",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "integrations",
                "{{integration_id}}",
                "documents",
                "reprocess-failed"
              ]
            },
            "description": "Requires `documents.retry`.\n\nRe-drives EVERY `normalization_failed` document on the integration through the current active mapping and code mappings — fix the mapping once, then clear the whole backlog in a single call. Recovered documents are queued for submission (or held if the subscription is unpaid); documents that still cannot normalise are left as `normalization_failed` with their error refreshed.\n\nOptional `limit` bounds how many are processed in one call.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"limit\": 500\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 Backlog cleared",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"12 of 12 recovered; 0 still failing.\",\n  \"data\": {\n    \"processed\": 12,\n    \"recovered\": 12,\n    \"still_failed\": 0\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b.data).to.have.property('recovered');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Submit now (synchronously)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/documents/{{document_id}}/submit",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "documents",
                "{{document_id}}",
                "submit"
              ]
            },
            "description": "Requires `documents.retry`. Used by support to verify a fix without waiting for the worker. Returns 409 if another worker already claimed the document — the claim is what makes double submission impossible."
          },
          "response": [
            {
              "name": "200 Attempted",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Submission attempted.\",\n  \"data\": {\n    \"id\": 90001,\n    \"status\": \"accepted\",\n    \"attempt\": {\n      \"attempt_no\": 3,\n      \"outcome\": \"accepted\",\n      \"error_code\": null\n    }\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "409 Claimed elsewhere",
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"The document was claimed by another worker; nothing was submitted twice.\"\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Hold a document",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/documents/{{document_id}}/hold",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "documents",
                "{{document_id}}",
                "hold"
              ]
            },
            "description": "Requires `documents.hold`. A manual hold is a decision about the **data** and survives a payment — releasing the billing backlog does not lift it."
          },
          "response": [
            {
              "name": "200 Held",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Document held. It will not be submitted until released.\",\n  \"data\": {\n    \"id\": 90001,\n    \"status\": \"on_hold\",\n    \"hold_reason\": \"manual\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Release a held document",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/documents/{{document_id}}/release",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "documents",
                "{{document_id}}",
                "release"
              ]
            },
            "description": "Requires `documents.release`. **Cannot bypass the billing gate** — if the subscription is unpaid this returns 402, otherwise 'release' would be a back door around non-payment."
          },
          "response": [
            {
              "name": "200 Released",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Document released for submission.\",\n  \"data\": {\n    \"id\": 90001,\n    \"status\": \"queued\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "402 Subscription unpaid",
              "status": "Payment Required",
              "code": 402,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"This POS integration is not currently permitted to submit (subscription not paid up or integration paused). Settle the subscription or resume the integration; held documents are then released automatically.\"\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Cancel a document",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/documents/{{document_id}}/cancel",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "documents",
                "{{document_id}}",
                "cancel"
              ]
            },
            "description": "Requires `documents.cancel`. Only for documents never filed. A filed document must be corrected with a credit note instead — the record with the revenue authority cannot be withdrawn from here.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reason\": \"Duplicate created by a till misconfiguration\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 Cancelled",
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Document cancelled. It will never be submitted.\",\n  \"data\": {\n    \"id\": 90001,\n    \"status\": \"cancelled\"\n  }\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            },
            {
              "name": "422 Already filed",
              "status": "Unprocessable Content",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"This document has already been filed with the revenue authority and cannot be cancelled here. Issue a credit note instead.\"\n}",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "body": null,
                "url": {
                  "raw": ""
                }
              }
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('envelope shape', function () {",
                  "    const b = pm.response.json();",
                  "    pm.expect(b).to.have.property('success');",
                  "    pm.expect(b).to.have.property('message');",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    }
  ],
  "variable": [
    {
      "key": "base_url",
      "value": "https://kra-agent-sbx.symatechlabs.com/app/api",
      "type": "string"
    },
    {
      "key": "account_id",
      "value": "1"
    },
    {
      "key": "token",
      "value": "",
      "type": "string"
    },
    {
      "key": "pos_key_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "pos_secret",
      "value": "",
      "type": "string"
    },
    {
      "key": "pos_sender_id",
      "value": "3c0e19f2f890ea7ca15e",
      "type": "string"
    },
    {
      "key": "sender_credential_id",
      "value": "1",
      "type": "string"
    },
    {
      "key": "integration_id",
      "value": "1",
      "type": "string"
    },
    {
      "key": "document_id",
      "value": "1",
      "type": "string"
    },
    {
      "key": "mpesa_callback_secret",
      "value": "change-me",
      "type": "string"
    },
    {
      "key": "unsubscribe_token",
      "value": "",
      "type": "string"
    }
  ],
  "event": [
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Collection-level contract: every response uses the one envelope.",
          "if (pm.response.headers.get('Content-Type') &&",
          "    pm.response.headers.get('Content-Type').includes('application/json')) {",
          "    const body = pm.response.json();",
          "    const isMpesaAck = Object.prototype.hasOwnProperty.call(body, 'ResultCode');",
          "    if (!isMpesaAck) {",
          "        pm.test('response uses the standard envelope', function () {",
          "            pm.expect(body).to.have.property('success');",
          "            pm.expect(body).to.have.property('message');",
          "        });",
          "        pm.test('failures never claim success', function () {",
          "            if (pm.response.code >= 400) { pm.expect(body.success).to.eql(false); }",
          "        });",
          "    }",
          "}"
        ]
      }
    }
  ]
}