eTIMS (KRA fiscalization)
Optional Kenya Revenue Authority fiscalisation. Paysasa stores each fiscal document and submits it asynchronously to KRA.
https://api.paysasa.com/api/v1Authx-api-keyFormatJSONOptional Kenya KRA eTIMS module. Paysasa stores fiscal documents and submits them asynchronously to KRA (OSCU/VSCU). Your checkout or POS must never wait on KRA — always treat fiscalization as a background job.
Prerequisites#
- eTIMS must be enabled for your account before you can configure it. Contact support if the module is not available.
- Merchant opens
/dashboard/etims, enters KRA PIN + Path B credentials (or Path A when allowed), then Activate. - Create a business API key (
apk_live_…/apk_test_…) for that same business. - Subscribe webhooks to
etims.document.accepted(and optionallyetims.document.rejected), or pollGET /etims/invoices/:id.
Xduka / Airsoko POS / external ERP: If the merchant already uses this Paysasa business for payments and has activated eTIMS on it, your app can fiscalize with that business's API key after each sale. Call POST /etims/invoices with an idempotencyKey (e.g. your sale id) and line items including KRA itemClassificationCode. Paysasa POS Lite auto-enqueues from its own sales; external systems must call this API themselves — that is the supported SaaS path today.
Endpoints#
| Method | Path | Purpose |
|---|---|---|
| POST | /etims/invoices | Create fiscal invoice (QUEUED or PENDING_MANUAL) |
| GET | /etims/invoices/:id | Fetch status + KRA CU / QR when accepted |
| POST | /etims/invoices/:id/confirm-manual-filing | Mark PENDING_MANUAL as filed (CU #) |
| GET | /etims/invoices | List recent documents (?status=&limit=) |
| POST | /etims/credit-notes | Create credit note (needs originalDocumentId) |
| GET | /etims/reports/manual-export | CSV export for KRA portal re-entry |
| GET | /etims/items | List item ↔ KRA classification mappings |
| POST | /etims/items | Upsert item mapping |
| GET | /etims/tax-codes | Tax types (A–E) |
| GET/POST | /etims | Account setup / activate (dashboard + API) |
Manual mode#
Default filing_method is manual_portal. Invoices are stored as PENDING_MANUAL with no KRA submission queue. Use GET /etims/reports/manual-export for a CSV, file on the KRA portal, then POST /etims/invoices/:id/confirm-manual-filing with the CU invoice number. Switch to automated_api when credentials are ready (optional per-invoice override via filingMethod). Manual/buyer docs require an 8–10 digit itemClassificationCode; buyer PIN is required when total ≥ KES 10,000 (except buyer_initiated).
Create invoice (external POS / ERP)#
curl -X POST "https://api.paysasa.com/api/v1/etims/invoices" \
-H "x-api-key: apk_live_<prefix>.<secret>" \
-H "Content-Type: application/json" \
-d '{
"idempotencyKey": "xduka-sale-9981247",
"externalSysId": "XD-INV-2026-0821-009",
"buyer": {
"pin": "A001234567Z",
"name": "Acme Kenya Ltd",
"phone": "+254711000000"
},
"currency": "KES",
"lines": [
{
"itemSeq": 1,
"itemClassificationCode": "50121500",
"itemCode": "SKU-100",
"itemName": "Enterprise Software Subscription",
"quantity": 1,
"unitPrice": 100000.00,
"discountAmount": 0,
"taxType": "B_STANDARD"
}
]
}'
# → HTTP 202
# { "id": "…", "status": "QUEUED", "message": "Invoice queued for KRA transmission.", … }Poll status#
curl "https://api.paysasa.com/api/v1/etims/invoices/<document_id>?business_id=biz_xxxxxxxxxxxxxxxx" \
-H "x-api-key: apk_live_<prefix>.<secret>"
# When ACCEPTED:
# {
# "id": "…",
# "status": "ACCEPTED",
# "kraDetails": {
# "cuInvoiceNumber": "KRA…",
# "cuSerialNumber": "…",
# "qrCodeUrl": "https://etims.kra.go.ke/…",
# "receiptSignature": "…"
# },
# "financials": { "taxableAmount": 100000, "taxAmount": 16000, "totalAmount": 116000 }
# }Credit note#
curl -X POST "https://api.paysasa.com/api/v1/etims/credit-notes" \
-H "x-api-key: apk_live_<prefix>.<secret>" \
-H "Content-Type: application/json" \
-d '{
"idempotencyKey": "xduka-cn-9981247",
"originalDocumentId": "<etims_document_id>",
"buyer": { "name": "Acme Kenya Ltd", "pin": "A001234567Z" },
"lines": [
{
"itemClassificationCode": "50121500",
"itemName": "Refund — Software Subscription",
"quantity": 1,
"unitPrice": 100000.00,
"taxType": "B_STANDARD"
}
]
}'Item mappings & tax codes#
# List / upsert KRA item classification mappings
curl "https://api.paysasa.com/api/v1/etims/items?business_id=biz_xxxxxxxxxxxxxxxx" \
-H "x-api-key: apk_live_<prefix>.<secret>"
curl -X POST "https://api.paysasa.com/api/v1/etims/items" \
-H "x-api-key: apk_live_<prefix>.<secret>" \
-H "Content-Type: application/json" \
-d '{
"business_id": "biz_xxxxxxxxxxxxxxxx",
"externalItemCode": "SKU-100",
"itemClassificationCode": "50121500",
"itemName": "Enterprise Software",
"unitOfMeasure": "PCE",
"taxType": "B_STANDARD"
}'
curl "https://api.paysasa.com/api/v1/etims/tax-codes?business_id=biz_xxxxxxxxxxxxxxxx" \
-H "x-api-key: apk_live_<prefix>.<secret>"Recommended Xduka / Airsoko POS flow#
// 1) Complete sale in your POS (source of truth)
// 2) Fire-and-forget fiscalize — do not block the receipt UI
async function fiscalizeSale(sale) {
const res = await fetch("https://api.paysasa.com/api/v1/etims/invoices", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.PAYSASA_API_KEY, // apk_* for the Paysasa business
},
body: JSON.stringify({
idempotencyKey: `xduka:${sale.id}`, // prevents double fiscalization
externalSysId: sale.number,
buyer: {
name: sale.customer?.name || "Walk-in Customer",
pin: sale.customer?.kraPin || null,
phone: sale.customer?.phone || null,
},
lines: sale.items.map((item, i) => ({
itemSeq: i + 1,
itemClassificationCode: item.kraClassCode || "99000000",
itemCode: item.sku,
itemName: item.name,
quantity: item.qty,
unitPrice: item.unitPrice, // major units (KES)
taxType: item.taxType || "B_STANDARD",
})),
}),
});
// 202 → queued. Poll GET /etims/invoices/:id or wait for webhook.
return res.json();
}
// 3) On webhook etims.document.accepted → print QR / CU invoice number on reprintTax types: A_EXEMPT, B_STANDARD (16%), C_ZERO_RATED, D_NON_VAT, E_EIGHT_PERCENT. Failed submissions are retried automatically. In sandbox, mock CU numbers may be returned when KRA credentials are not configured.