Introduction

The KRA Agent App enables businesses to submit their sales invoices to the Kenya Revenue Authority easily without changing how their existing APIs work. It features robust reporting and retry mechanisms to ensure all submitted invoices are successfully delivered, even when downtimes occur at KRA. Visit Symatech Labs Ltd for more information on our solutions.

How the KRA Agent App Works

1. The Base Address

All of our API endpoints share a common base URL. You'll just need to append the specific path for the action you want to take:

https://kra-agent-sbx.symatechlabs.com/app/api/v1/...

v1 is the version. It will not change under you: if a future version makes breaking changes, it becomes v2, and v1 keeps working.

In the examples, {{base_url}} stands for:

https://kra-agent-sbx.symatechlabs.com/app/api

So {{base_url}}/v1/documents means https://kra-agent-sbx.symatechlabs.com/app/api/v1/documents.


2. Authentication

Requests carry credentials that prove who is calling. There are two main authentication surfaces, and they never mix: a till's credential can only send sales, and can never read reports, see money, or change settings.

2a. Till Key (Header Auth)

A till authenticates with a key made of two parts joined by a dot:

Authorization: Bearer <key_id>.<secret>

For example:

Authorization: Bearer kra_x8v9zjglzknr3sniwflk.sk_ILdUug8GyNunXOIecHkb3XdBoj6brNT03UmmMWGuLAR304El

You get this key when you create a credential for a till. The key belongs to one till only. The secret is shown once, when created, and never again. Used by endpoints under /v1/pos/documents.

2b. SenderId Credential (Body Auth)

For a POS system that cannot send custom HTTP authorization headers, authentication uses a provider-generated SenderId secret carried inside the JSON body at Invoice.SenderId:

{
  "Invoice": {
    "SenderId": "3c0e19f2f890ea7ca15e",
    "TraderSystemInvoiceNumber": "REC-57"
  }
}

The SenderId secret is registered for the till first (Section D5). Calls are sent to POST {{base_url}}/v1/pos/ingest.

2c. Bearer Token

Signing in with email and password returns a token, sent the same way:

Authorization: Bearer 3|hR9xkP2wQ7...

Used by every endpoint not under /v1/pos/…, except the public links.

2d. Public (No Credential)

Three endpoints are open on purpose, because the caller cannot sign in:

  • Sign in itself (POST /v1/auth/login).
  • The M-PESA callback, Safaricom cannot present our credentials, so a long secret is carried in the address instead.
  • The email unsubscribe link, it must work from an email client, so it carries a one-time token in the address.

Headers You Will Almost Always Send

Header When Meaning
Accept: application/json Always "Reply to me in JSON."
Content-Type: application/json When you send a body "My body is JSON."
Authorization: Bearer … All non-public endpoints Your credential (Section 2a or Section 2b).
X-Client-Request-Id Optional, on sending a sale or a payment Your own reference for the request, so a repeat is easy to recognise.

3. The Shape of Every Response

We like to keep things consistent! Every response from our API follows the same basic structure:

{
  "success": true,
  "message": "A human-readable sentence about what happened.",
  "data": { }
}
  • success, true or false. Check this first.
  • message, a sentence you can show to a person as-is. Refusals explain why, not just that they were refused.
  • data, the actual result: an object, a list, or null.
  • errors, present only on failures, details of what was wrong, field by field.

When a list can be long, a pagination block is added:

"pagination": { "total": 128, "per_page": 25, "current_page": 1, "last_page": 6, "from": 1, "to": 25 }

Ask for a page with ?page=2, and change the page size with ?per_page=50.


4. Status Codes You Will See

We use standard HTTP status codes to let you know how your request went:

Code Meaning Typical cause
200 OK Done A read, or an action that finished.
201 Created Made something new A new account, user, credential, plan, invoice, or a sale filed immediately in "sync" mode.
202 Accepted Taken, still working A sale was stored and queued to file, poll for the outcome.
402 Payment Required Blocked on billing A subscription is unpaid, so filing is paused (the sale is still stored).
403 Forbidden Not allowed The caller's credential is not permitted this action.
404 Not Found No such thing Unknown id/reference, or a bad public link.
409 Conflict Clash Something was already done, or two workers raced.
413 Payload Too Large Too big The body exceeds the 1 MB limit.
422 Unprocessable Understood, but invalid A validation error, or a payload the Agent could not read.
429 Too Many Requests Slow down A rate limit was hit (see Section 5).
503 Service Unavailable Temporarily off Ingestion has been taken down for maintenance.

Important: a sale is never refused because of billing. If the subscription is unpaid, the sale is still accepted and stored (202), only the filing waits. Nothing a shop records is ever lost.


5. Rate Limits

To ensure the system stays fast and reliable for everyone, we have some sensible rate limits in place. If you happen to send too many requests, we'll return a 429 status code along with a Retry-After header so you know exactly when to try again.

Where Limit
Sending sales (/v1/pos/…) 300 requests per minute per till (configurable). Tills behind one shop's shared internet do not throttle each other.
Sign in (/v1/auth/login) 5 attempts per minute per network address.
Unsubscribe link 6 requests per minute.

The largest body accepted anywhere is 1 MB, larger is refused with 413.


6. Endpoints

Authentication at a glance: section G uses a till key (Section 2a). The public links (sign in, M-PESA callback, unsubscribe) need no credential. Everything else uses a bearer token (Section 2b).


A. Sign In and Out

A1. Sign In

What it does: Use this endpoint to securely log in with your email and password. We'll give you a token that you can use to authenticate your future requests. Public. Limited to 5 tries a minute per network.

Request

POST {{base_url}}/v1/auth/login
Accept: application/json
Content-Type: application/json
{
  "email": "[email protected]",
  "password": "Password!2345",
  "device_name": "ops-laptop"
}

Responses

  • 200 OK, signed in. data.token is the token to send on later requests, data.user describes the caller including their roles and permissions.
{
  "success": true,
  "message": "Signed in.",
  "data": {
    "token": "3|hR9xkP2wQ7...",
    "user": {
      "id": 2,
      "name": "Sunrise Admin",
      "email": "[email protected]",
      "account_id": 1,
      "roles": ["account-admin"],
      "permissions": ["accounts.view", "users.view", "integrations.view", "documents.view"]
    }
  }
}
  • 403, deactivated account: "This account has been deactivated. Contact your administrator."
  • 422, wrong email or password: "These credentials do not match our records."

A2. Who Am I

What it does: Returns the signed-in caller's details, handy for a front-end deciding what to show.

Request

GET {{base_url}}/v1/auth/me
Accept: application/json
Authorization: Bearer <token>

Response: 200 OK with the caller's id, name, email, roles, and permissions.

A3. Sign Out

What it does: Invalidates the current token.

Request: POST {{base_url}}/v1/auth/logout with the Authorization header. Response: 200 OK, "Signed out."


B. Accounts and Users

An account is a merchant business. Users are the people who sign in.

B1. List Accounts

GET {{base_url}}/v1/accounts?per_page=25, returns accounts with a count of their tills.

B2. Create an Account

POST {{base_url}}/v1/accounts

{ "name": "Sunrise Cafe Ltd", "kra_pin": "P051234567X", "contact_email": "[email protected]", "contact_phone": "0722000111" }

201 Created, or 422 if the KRA PIN format is wrong.

B3. Get / Update an Account

  • GET {{base_url}}/v1/accounts/{account}
  • PATCH {{base_url}}/v1/accounts/{account}, send only the fields you want to change.

B4. List / Create Users

  • GET {{base_url}}/v1/users
  • POST {{base_url}}/v1/users
{ "name": "Jane Wanjiru", "email": "[email protected]", "password": "Str0ng!Passw0rd!", "account_id": 1, "roles": ["account-viewer"] }

201 Created. A 403 is returned if an account user tries to grant a role they are not allowed to assign.

B5. Update a User

PATCH {{base_url}}/v1/users/{user}.

B6. List Roles

GET {{base_url}}/v1/roles, lists each role and what it can do, useful when choosing what to assign a user.


C. POS Integrations (Tills)

A POS integration ("till") is one connected point-of-sale. Each is billed and configured on its own.

C1. List Tills

GET {{base_url}}/v1/integrations?per_page=25. Each row shows accepts_ingestion (is it taking sales?) and may_submit (is it allowed to file, i.e. paid up and active?).

C2. Create a Till

POST {{base_url}}/v1/integrations

{
  "account_id": 1,
  "name": "Sunrise Restaurant Till",
  "code": "sunrise-restaurant",
  "branch_code": "00",
  "provider": "thirdparty",
  "result_delivery_mode": "webhook",
  "webhook_url": "https://sunrisecafe.co.ke/hooks/kra",
  "billing_plan_code": "standard_monthly",
  "grace_days": 7,
  "trial_periods": 1
}
  • result_delivery_mode is how outcomes reach the shop: async (they poll), sync (wait for the answer in the same call), or webhook (the Agent calls their URL).
  • 201 Created. 422 if the code is already used in that account, or if a webhook_url points at a private/internal address (blocked for safety).

C3. Get / Update a Till

  • GET {{base_url}}/v1/integrations/{integration}
  • PATCH {{base_url}}/v1/integrations/{integration}, send only the fields to change.

C4. Pause / Resume a Till

  • POST {{base_url}}/v1/integrations/{integration}/pause, stop filing, but keep accepting and storing sales. Nothing is lost.
  • POST {{base_url}}/v1/integrations/{integration}/resume, start filing again, any sales held while paused are released. The reply says how many.

C5. Rotate the Webhook Signing Secret

What it does: Issues a new secret used to sign outbound callbacks, so the shop can verify a callback truly came from the Agent. POST {{base_url}}/v1/integrations/{integration}/webhook-secret

{ "immediate": false }
  • immediate: false (recommended), the old secret keeps working for a short overlap so you can update your endpoint with no downtime. During the overlap, callbacks carry two signatures, accept either.
  • immediate: true, use only if the old secret leaked, it stops at once. The new secret is shown once.

C6. View / Set the Monthly Allowance (Quota)

  • GET {{base_url}}/v1/integrations/{integration}/quota
  • PATCH {{base_url}}/v1/integrations/{integration}/quota
{ "monthly_document_quota": 25000 }

The quota is a warning line, not a barrier. Documents are counted and you are warned as usage climbs, but nothing is ever blocked for exceeding it.


D. API Credentials (Till Keys)

These are the keys a till uses to authenticate (see Till Key).

D1. List Credentials

GET {{base_url}}/v1/integrations/{integration}/credentials. Secrets are never shown here, only whether each key is usable, retiring, and safe_to_revoke.

D2. Create a Credential

POST {{base_url}}/v1/integrations/{integration}/credentials

{ "label": "Front counter till", "expires_at": null }

201 Created, the reply includes the secret and a ready-to-use authorization_header. Copy them now, the secret is never shown again.

D3. Rotate a Credential (No Downtime)

POST {{base_url}}/v1/integrations/{integration}/credentials/{credential}/rotate

{ "grace_hours": 24, "label": "Front counter (rotated)" }

Creates a new key while the old one keeps working for grace_hours, so tills can be updated one at a time. Set grace_hours: 0 (or immediate) if the old key leaked and must stop at once.

D4. Revoke a Credential

DELETE {{base_url}}/v1/integrations/{integration}/credentials/{credential}, the key stops working immediately. 422 if it was already revoked.

D5. Register a SenderId (Body-Auth Till)

What it does: Registers a provider-generated SenderId secret (the random value placed at Invoice.SenderId) for a till whose auth_scheme is set to sender_id. Only its hash is stored. POST {{base_url}}/v1/integrations/{integration}/sender-credentials

{ "sender_id": "3c0e19f2f890ea7ca15e", "label": "Front counter" }

201 Created, returns { "id": 12, "label": "Front counter", "usable": true }.

D6. Revoke a SenderId

DELETE {{base_url}}/v1/integrations/{integration}/sender-credentials/{senderCredential}, revokes the SenderId credential immediately.


E. Mappings (Teaching the Agent a Till's Format)

Your POS system never has to change how it outputs sales data. KRA Agent adapts to your till's existing JSON structure, not the other way around.

To teach the Agent how to read your till, you set up two complementary mappings together:

  • Payload Mapping: Describes the shape of your till's JSON, which field is the invoice number, where line items sit, and whether prices already include tax.
  • Code Mappings: Translate your till's short codes into the values KRA expects, most importantly converting your till's tax code into a real VAT rate, and optionally your till's item code into a registered KRA item code.

Setup Sequence

The onboarding flow follows this specific order:

Suggest > Add Code Mappings > Dry-Run > Publish > Activate

Code mappings come before the dry-run on purpose. The dry-run works out the tax on every line, and the Agent will never guess a rate. If a line carries a tax code you have not mapped, the dry-run stops with an UNMAPPED_TAX_CODE refusal. Setting up the code mappings first ensures the dry-run has everything it needs to show you real, accurate figures on your first test.

E1. Code Mappings: pos_code, target_code, tax_rate

A code mapping is a single row that says "when this till sends code X, it means Y." Each has a type, tax or item. Three fields do the work, and here is where each comes from and why it matters:

  • pos_code, the code exactly as your till writes it in the payload. It is whatever value your payload mapping's source_tax_code (or source_item_code) points at. If your invoices carry "TaxRate": 16, then pos_code is "16". If they carry "vat": "A", then pos_code is "A". It must match character-for-character ("16", not 16.0 or "16%"), the Agent looks it up as plain text.
  • tax_rate (tax mappings only), the actual VAT percentage to apply, as a number (16, 8, 0). This is the figure used to compute the tax on every line, so it must be exactly right: a wrong number here files the wrong amount to KRA. It is required on every tax mapping, the Agent refuses to guess it, which is the whole reason UNMAPPED_TAX_CODE exists.
  • target_code, the canonical name the sale is filed under. For tax it is a label like VAT_16, VAT_8, VAT_ZERO, VAT_EXEMPT, for items it is the registered KRA item code. It carries meaning the rate alone cannot: at 0%, the target_code is what distinguishes zero-rated (VAT_ZERO) from exempt (VAT_EXEMPT) from non-VAT, three different things on a KRA return that all share a 0 rate.

In one line: pos_code is what your till says, tax_rate is the number that gets filed, and target_code is what it means to KRA.

E2. Suggest a Mapping From a Sample

What it does: You paste a real sample payload, the Agent proposes a draft payload mapping and explains its reasoning (including the tricky question of whether prices already include tax). It stores nothing. POST {{base_url}}/v1/integrations/{integration}/mappings/suggest

{ "document_type": "sales_invoice", "payload": { "Invoice": { "TraderSystemInvoiceNumber": "REC-57", "TotalInvoiceAmount": 3047.48, "ItemDetails": [ ] } } }

200 OK returns proposed_rules, a confidence level, and notes. It cannot invent your tax codes, that is the next step. Carry data.proposed_rules forward into the dry-run (E6) as its rules.

E3. Add or Update Code Mappings (in Bulk)

POST {{base_url}}/v1/integrations/{integration}/code-mappings

{
  "mappings": [
    { "type": "tax",  "pos_code": "16", "target_code": "VAT_16",   "tax_rate": 16, "description": "Standard rated" },
    { "type": "tax",  "pos_code": "8",  "target_code": "VAT_8",     "tax_rate": 8,  "description": "8%" },
    { "type": "tax",  "pos_code": "0",  "target_code": "VAT_ZERO",  "tax_rate": 0,  "description": "Zero rated" },
    { "type": "item", "pos_code": "BF-01", "target_code": "1234567890", "description": "Beef Stew" }
  ]
}

200 OK reports how many were saved. Map every code your till can send, so a later invoice does not fail mid-filing. Every tax mapping must state its tax_rate, or you get 422. (An unmapped item code is tolerated, the till's own code is used as-is, but an unmapped tax code is always refused.) You rarely add item mappings by hand: registering a product (section F) creates them for you, this endpoint is the manual alternative.

E4. List Code Mappings

GET {{base_url}}/v1/integrations/{integration}/code-mappings?type=tax&per_page=50

E5. Delete a Code Mapping

DELETE {{base_url}}/v1/integrations/{integration}/code-mappings/{codeMapping}

E6. Dry-Run a Mapping

What it does: Runs a mapping against a sample and shows exactly what would be filed, without storing anything. This is how you confirm a mapping before turning it on. It needs the code mappings (E3) to already be in place. POST {{base_url}}/v1/integrations/{integration}/mappings/test

{
  "document_type": "sales_invoice",
  "rules": { "document": { "source_document_ref": "receipt.no" }, "lines": { "path": "items", "fields": { } } },
  "payload": { "receipt": { "no": "RST-8891" }, "items": [ ], "totals": { "gross": 1450.0 } }
}
  • rules is the mapping to test, paste the proposed_rules from E1 here. payload is a real sample invoice, not the rules. If you leave rules out, the Agent falls back to the integration's saved mapping, and errors if there is none.
  • 200 OK shows the normalised document and lines, with the tax computed from your code mappings.
  • 422 names the first thing that did not fit, MISSING_FIELD (a path it could not find), UNMAPPED_TAX_CODE (a tax code with no mapping, add it in E3 first, then dry-run again), or INVALID_DATE (a date the Agent could not read, see the note below).

Reading dates: tell the Agent the shape of your date. Inside a mapping's rules, a date field (like issued_at) can carry a format that says how to read the value your till sends:

  • iso8601 (the default) reads standard timestamps such as 2026-07-21T10:04:00+03:00 or 2026-07-21.
  • timestamp reads a Unix number of seconds.
  • Anything else is a date pattern, use it when your till writes dates its own way. For example a value like 071221133300 (day, month, 2-digit year, then hour, minute, second) is read with "format": "dmyHis":
    "issued_at": { "path": "PaymentInfo.AuthDateTime", "format": "dmyHis" }
    
    The letters are the usual date pieces, d day, m month, y two-digit year, H hour, i minute, s second (so d/m/Y reads 21/07/2026).

If the value does not match the format you gave, the dry-run, and a real sale, stops with INVALID_DATE, naming the field, the path, and the value it could not read. A date with no timezone of its own is read as wall-clock time in the till's own timezone (set on the integration), so a 10:04 sale stays 10:04 there and is never shifted to the server's zone. It is set once in the mapping and only affects this till, other tills keep their own formats.

E7. Publish a Mapping Version

POST {{base_url}}/v1/integrations/{integration}/mappings

{ "document_type": "sales_invoice", "activate": true, "notes": "Nested receipt format", "rules": { "document": { "source_document_ref": "receipt.no" }, "lines": { "path": "items", "fields": { } } } }

201 Created returns the new version. A financial document must declare rules.lines.path, or you get 422.

E8. Activate an Older Version (Roll Back)

POST {{base_url}}/v1/integrations/{integration}/mappings/{mapping}/activate, makes a chosen version the active one.

E9. List Mapping Versions

GET {{base_url}}/v1/integrations/{integration}/mappings. Mappings are versioned and never edited in place, so a sale always keeps the exact rules that produced it.


F. Products (The Item Catalogue)

You cannot file a sale for a product KRA has never seen. So before a till can sell, its products must be in the catalogue and, for aggregators that need it, registered with the aggregator. This is where a sale line gets the item code KRA files under.

The Agent keeps the catalogue in plain terms (name, price, tax, unit). When you register it with a till's aggregator it does two things for you: it obtains the aggregator's item code, and it auto-creates the item code mapping, so a sale line then resolves on its own, with no hand-mapping.

The order is: add products, register them for the till, then sell. Skip it and the sale is still safely stored, but refused with no eTIMS item code until the product is registered. Some aggregators (like Digitax) take items inline and need no registration, for those the register step is a harmless no-op.

The merchant's trading address is set on the aggregator key, not on each sale. For aggregators that sign a KRA receipt (such as Advatech), the merchant's address is printed on the receipt. It travels with the merchant's connected aggregator key, set once alongside their KRA PIN and branch, not with each sale. If it is left unset the sale still files, but the receipt shows a placeholder (N/A), so give the real address when the key is connected.

F1. List Products

GET {{base_url}}/v1/accounts/{account}/items. Each product shows its per-aggregator registrations, the code it was assigned, and whether it is registered.

F2. Add a Product

POST {{base_url}}/v1/accounts/{account}/items

{
  "source_item_code": "MILK-01",
  "name": "Strawberry Milkshake",
  "item_class_code": "5020230100",
  "tax_code": "VAT_16",
  "packaging_unit": "NT",
  "quantity_unit": "U",
  "origin_country": "KE",
  "default_price": 381.36
}
  • source_item_code, what your till sends for this product. It becomes the pos_code of the item mapping, so a sale line matches it.
  • item_class_code, the KRA item classification (itemClsCd), picked from your aggregator's code list.
  • tax_code, the product's tax (e.g. VAT_16).
  • packaging_unit / quantity_unit / origin_country / item_type_code, eTIMS attributes, with sensible defaults (NT / U / KE / 2).

201 Created. One product per source_item_code per account, a duplicate is a 409.

F3. Update a Product

PATCH {{base_url}}/v1/accounts/{account}/items/{item}, send only the fields you want to change.

F4. Register Products With a Till's Aggregator

What it does: Registers the account's products with the aggregator this till files through, and creates the item mappings for the ones that succeed. Optional item_ids limits it to specific products, omit to register all. POST {{base_url}}/v1/integrations/{integration}/items/register

{ "item_ids": [7] }
  • 200 OK with a per-product summary, registered, failed, skipped, and results (each carrying the assigned item_cd).
  • Idempotent, a product already registered for this till is skipped, so re-running is safe.
  • For an aggregator that takes items inline, the reply is "not_required": true and nothing is registered.
  • A product with no item_class_code, or one the aggregator rejects, comes back failed with the reason, the others still succeed.

Once a product is registered, a sale line that carries its source_item_code resolves to the registered code automatically, that is what section G relies on.

F5. Sync KRA Code Lists

What it does: Pulls the till provider's KRA reference code lists (quantity/packing units, payment types, item classifications, refund reasons, countries) into cache. POST {{base_url}}/v1/integrations/{integration}/reference/sync

  • 200 OK returns { "supported": true, "synced": 1580 }.
  • Returns supported: false if the provider does not supply code lists.

F6. Look Up KRA Codes

What it does: Search or browse the synced KRA reference codes for a till's provider. Filter by type (quantity_unit, packaging_unit, payment_type, item_class, country, refund_reason) and search. GET {{base_url}}/v1/integrations/{integration}/reference?type=quantity_unit&search= 200 OK returns paginated code objects with code_type, code, and label.


G. Sending Sales From a Till

This is where the magic happens! It's the core of our service. Depending on your POS capability, you can authenticate using a till key in the header (Section 2a) or a SenderId in the body (Section 2b).

Setup comes first. A till can only file once its format is mapped (section E) and its products are registered (section F). A sale sent before that is still safely stored, it just will not file until the setup is done.

G1. Send a Sale (Header Auth)

What it does: Send us a sale! We'll safely store it, convert it into the standard format KRA expects, and queue it up for filing.

Request

POST {{base_url}}/v1/pos/documents
Accept: application/json
Authorization: Bearer <key_id>.<secret>
Content-Type: application/json
X-Client-Request-Id: <your own optional reference>

The body is your till's own format, whatever shape it produces, as long as a mapping has been set up for it. Two common shapes:

Nested:

{
  "receipt": { "no": "RST-8891", "issued": "2026-07-21T10:04:00+03:00" },
  "customer": { "name": "Walk-in", "pin": null },
  "items": [
    { "sku": "BF-01", "desc": "Beef Stew", "qty": 2, "price": 650.0, "vat": "A" }
  ],
  "totals": { "gross": 1450.0 }
}

Flat:

{
  "invoice_number": "RT-4410",
  "date": "21/07/2026",
  "customer_name": "Walk-in",
  "lines": [ { "code": "BF-01", "name": "Beef Stew", "quantity": 2, "unit_price": 560.34, "tax_code": "VAT16" } ]
}

Responses

  • 202 Accepted (normal, "async"), stored and queued. Poll G6 for the outcome.
{
  "success": true,
  "message": "Document accepted for submission.",
  "data": { "document_id": 90001, "reference": "RST-8891", "status": "queued", "total_incl_tax": 1450.0, "total_tax": 200.0, "currency": "KES" }
}
  • 201 Created (only if the till is set to "sync" mode), filed there and then, the reply already carries the kra_control_code.
  • 200 OK with "duplicate": true, this reference was already received, the existing record is returned. The same sale is never filed twice.
  • 422 Unprocessable, the payload could not be read. Common causes include:
    • UNMAPPED_TAX_CODE, a tax code with no mapping for this till.
    • INVALID_CUSTOMER_PIN, buyer PIN format is invalid.
    • INVALID_DATE, a date field did not match the format set in the mapping (see E6).
    • TOTALS_DISAGREE, declared total does not match the sum of line items.
    • no eTIMS item code, a product that has not been registered (set it up in section F). The payload is still saved so it can be recovered later (see H4).
  • 402 Payment Required, the subscription is unpaid, the sale is stored and held, not filed.

G1b. Send a Sale via SenderId (Body Auth)

What it does: For a POS system that cannot send an auth header. Authentication is performed by a provider-generated secret carried in the body at Invoice.SenderId. Requires that the till's auth_scheme is sender_id and the SenderId has been registered (Section D5).

Request

POST {{base_url}}/v1/pos/ingest
Accept: application/json
Content-Type: application/json
{
  "Invoice": {
    "SenderId": "3c0e19f2f890ea7ca15e",
    "InvoiceTimestamp": "2026-07-21T10:04:00",
    "TraderSystemInvoiceNumber": "REC-57",
    "TotalInvoiceAmount": 442.38,
    "ItemDetails": [
      { "HSDesc": "Strawberry Milkshake", "HSCode": "MILK-09", "TaxRate": 16, "UnitPrice": 381.36, "Quantity": 1, "ItemAmount": 381.36 }
    ]
  }
}

Responses

  • 202 Accepted, stored and queued.
  • 401 Unauthorized, Invalid sender credential.

G2. Send a Sale, Stating the Type in the Address

What it does: Same as G1, but the document type is specified in the URL address, used for credit notes, debit notes, etc. A credit note must carry a reference to the sale it amends (original_document_ref) and a valid KRA reason code (reason_code).

Valid reason 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.

Request

POST {{base_url}}/v1/pos/documents/type/credit_note
{
  "receipt": {
    "no": "CN-0007",
    "original": "RST-8891",
    "reason": "06",
    "issued": "2026-07-22T09:00:00+03:00"
  },
  "items": [ { "sku": "BF-01", "desc": "Beef Stew (returned)", "qty": 1, "price": 650.0, "vat": "A" } ],
  "totals": { "gross": 650.0 }
}

Responses

  • 202 Accepted, as G1, with "document_type": "credit_note".
  • 422 MISSING_ORIGINAL_REFERENCE, a credit note must name the sale it corrects, none was found.

You can also keep using /v1/pos/documents and send an X-Document-Type header instead of using this address.

G3. Send End-of-Day Totals (Z-Report)

What it does: When you close shop for the day, your till sends us its daily totals. We compare this with the sales we actually received to make absolutely sure nothing slipped through the cracks.

Request

POST {{base_url}}/v1/pos/daily-summary
{
  "business_date": "2026-07-21",
  "document_count": 412,
  "gross_total": 604210.0,
  "tax_total": 83340.0,
  "first_reference": "RST-8891",
  "last_reference": "RST-9302"
}

Responses

  • 200 OK status: matched, everything the till recorded reached the Agent.
  • 202 Accepted status: variance, some receipts never arrived, the message says how many, and they must be re-sent.

G4. Read Back a Day's Comparison

What it does: Fetches the stored comparison for a past day.

Request: GET {{base_url}}/v1/pos/daily-summary/2026-07-21 Responses: 200 OK with the comparison, or 404 if no summary was sent for that day.

G5. Ask Which Receipts to Re-Send

What it does: Lists the exact references the till recorded but the Agent never received, so the till can re-send only those.

Request: GET {{base_url}}/v1/pos/daily-summary/2026-07-21/missing Response: 200 OK

{
  "success": true,
  "data": { "never_received_count": 4, "missing_references": ["RST-8903", "RST-8904"], "resend_required": true }
}

G6. Check a Sale by Your Own Reference

What it does: Curious about the status of a specific sale? You can look it up using your till's own reference number to see exactly where it is in the process, queued, filed (with the KRA control code), waiting to retry, or rejected.

Request: GET {{base_url}}/v1/pos/documents/RST-8891 Responses

  • 200 OK status: accepted, includes kra_control_code, kra_invoice_number.
  • 200 OK status: retry_scheduled, the tax system was briefly down, includes next_attempt_at.
  • 404, no sale with that reference from this till.

G6b. Check a Sale Status via SenderId (Body Auth)

What it does: If your POS system uses a SenderId secret in the JSON body (rather than an authorization header), you can check a sale's filing status using this endpoint.

Request

POST {{base_url}}/v1/pos/ingest/REC-57
Accept: application/json
Content-Type: application/json
{
  "SenderId": "3c0e19f2f890ea7ca15e"
}

Responses

  • 200 OK, returns the sale status (accepted, queued, retry_scheduled, etc.), KRA control code, and invoice details.
  • 401 Unauthorized, Invalid sender credential.
  • 404 Not Found, no sale found with that reference for this SenderId.

H. Documents (The Filed Sales)

A document is one sale (or credit note, etc.) inside the Agent, with its full history. These are the management endpoints, a till uses Section G.

A document's life: received, normalized, queued, submitting, then accepted. Along the way it can become rejected, retry_scheduled (will try again), dead_letter (retries exhausted, a human's inbox), on_hold (paused, e.g. unpaid), normalization_failed (could not be read), or cancelled.

H1. List Documents

GET {{base_url}}/v1/documents?status=on_hold,retry_scheduled&from=2026-07-01&per_page=25. Filter by pos_integration_id, status (comma-separated), document_type, reference, from/to dates, or needs_attention=true.

H2. Get One Document (With Lines and Attempts)

GET {{base_url}}/v1/documents/{document}. Returns the full sale, its line items, every filing attempt, and whether the totals agree with the lines.

H3. Retry a Sale

What it does: Puts a sale that failed to file back in the queue to try again (for rejected, dead_letter, retry_scheduled). POST {{base_url}}/v1/documents/{document}/retry

{ "reset_attempts": true }

200 OK, queued. An already-accepted sale can never be retried (422), it has a KRA control code, and re-filing would duplicate the tax record.

H4. Reprocess a Sale That Could Not Be Read

What it does: Re-runs the reading/normalising step for a normalization_failed sale, using the retained original payload and the current mappings. Use it after fixing a mapping or adding a missing code. POST {{base_url}}/v1/documents/{document}/reprocess

  • 200 OK, read successfully and queued to file.
  • 422, still cannot be read (the message names why), or the sale already got past reading (only unread sales can be reprocessed, this is what keeps a filed sale from ever being re-filed).

Retry vs reprocess: retry re-tries the filing, reprocess re-tries the reading. They fix different problems.

H5. Reprocess All Failures for a Till (Clear the Backlog)

What it does: After fixing a mapping once, re-reads every normalization_failed sale on the till in a single call. POST {{base_url}}/v1/integrations/{integration}/documents/reprocess-failed

{ "limit": 500 }

200 OK, "12 of 12 recovered, 0 still failing."

H6. Submit Now (Force an Immediate Attempt)

POST {{base_url}}/v1/documents/{document}/submit, files the sale synchronously right now (support use). 409 if another worker already claimed it, nothing is submitted twice.

H7. Hold / Release a Sale

  • POST {{base_url}}/v1/documents/{document}/hold, pause a sale by hand while something is investigated.
  • POST {{base_url}}/v1/documents/{document}/release, send it back to the queue. 402 if the subscription is unpaid (release is not a way around billing).

H8. Cancel a Sale

POST {{base_url}}/v1/documents/{document}/cancel

{ "reason": "Duplicate created by a till misconfiguration" }

200 OK, it will never be filed. An already-filed sale cannot be cancelled (422), issue a credit note instead.