api · eTIMS API integration Kenya

eTIMS API integration for POS, ERP and platform products

Add KRA eTIMS invoice workflows to the product your merchants already use. Risiti provides one versioned API for connected businesses, asynchronous invoice submission, receipt status, webhooks and operational logs.

KRA provides OSCU and VSCU for businesses and software providers integrating an existing invoicing system with eTIMS. Risiti sits at the product-integration layer: your server calls the Risiti API, while each connected business keeps its own taxpayer identity, branch, invoice sequence and compliance history.

Use the sandbox to build and test without production access. Moving live is a separate controlled step that requires the applicable billing setup, a readiness review and the applicable KRA production approval and credentials. Risiti is not KRA and this page does not replace KRA's current technical specifications or certification process.

Direct API or Platform API: choose by who issues the invoice

Use Direct API when one verified business owns the sales. Use Platform API when your product serves separate businesses and must keep their taxpayer identities and records apart. Both use the same invoice fields and submission lifecycle, with different URL namespaces and key ownership.

Choose the API namespace before writing your first request
DecisionDirect APIPlatform API
Business ownershipOne verified workspace business; the key identifies it.Multiple connected merchants; send the correct merchant_id.
Sandbox base URLhttps://sandbox-api.getrisiti.com/v1/directhttps://sandbox-api.getrisiti.com/v1/platform
Production base URLhttps://api.getrisiti.com/v1/directhttps://api.getrisiti.com/v1/platform
Merchant setupUse the business verified during workspace onboarding.Create each merchant with POST /merchants and store data.merchant_id.

Who the Risiti eTIMS API is for

The API is designed for products that already create a sale, order, job card, booking or ledger entry and need to add compliant Kenyan tax invoicing without forcing users into a second daily system.

  • POS and retail systems issuing receipts for many independent businesses.
  • ERP and accounting products that need eTIMS submission and reconciliation.
  • SaaS products for garages, property managers, clinics, hospitality and field services.
  • Marketplaces, ecommerce systems and merchant platforms operating structured multi-business workflows.
  • Procurement and aggregator platforms preparing for buyer-initiated or reverse-invoicing workflows.

A practical integration flow

Create the workspace that matches your product and a sandbox API key. Platform integrations register each connected business as a merchant; Direct API uses the verified workspace business. Submit invoices with a stable Idempotency-Key from your own system. An accepted API request means Risiti has received the work, not that KRA has completed it.

Keep processing and action-required states visible in your product. Use signed webhooks for status changes and the invoice endpoint for reconciliation. Read data.submission for the authoritative result and retrieve live KRA receipt fields after successful completion.

  • Authentication: server-side Bearer API keys; never expose a key in browser or mobile code.
  • Write safety: stable idempotency keys prevent accidental duplicate merchant and invoice operations.
  • Operations: request IDs, API logs and webhook delivery history support troubleshooting and reconciliation.

Send a sandbox invoice, then read its result

Create a Direct API workspace and a sandbox key with invoices:write and invoices:read scopes. Set RISITI_SANDBOX_KEY in your server's environment, then run the curl request below. It represents a fictional non-VAT service for KES 1,000; choose the correct tax treatment and registered item codes for your own business before live use.

For Platform API, use /v1/platform/invoices instead and include the merchant_id returned when you created the test merchant. Keep this request's external_id and Idempotency-Key stable when replaying it; give each new sale its own identifiers.

Risiti's invoice sandbox simulates processing and can complete a successful test immediately. A sandbox success is a contract test, not a KRA-signed production receipt or evidence of KRA approval. Live delivery remains asynchronous and requires the applicable production readiness checks.

The downloadable TypeScript example creates this fictional kind of sale and retrieves its saved invoice. Its HTTP behavior was tested with mocked responses only: no observed sandbox submission or KRA acceptance is claimed. It accepts only the exact Direct API sandbox host and an rsk_test_ key, refuses redirects and never prints credentials. It does not implement item registration, webhooks or credit notes.

For the TypeScript example, set RISITI_SANDBOX_KEY securely in your local environment, then supply your own stable RISITI_SALE_ID and RISITI_IDEMPOTENCY_KEY. Keep those values and the fictional payload unchanged on replay. To reconcile an existing invoice without creating one, also set RISITI_INVOICE_ID to the returned ID. The example performs one read, reports pending or action_required honestly and stops; it does not poll indefinitely. Keep your sale-to-invoice mapping beyond the 30-day idempotency window.

Run the downloaded file from a standalone folder without a parent package.json, or from an ES-module project with type set to module. The commands explicitly select module mode for Node.js 22.6; an existing project explicitly configured as CommonJS is not supported by this standalone example.

1. Create a fictional invoice with a Direct API sandbox key
curl --include --request POST \
  'https://sandbox-api.getrisiti.com/v1/direct/invoices' \
  --header "Authorization: Bearer $RISITI_SANDBOX_KEY" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: demo-sale-001' \
  --data '{
    "external_id": "demo-sale-001",
    "payment_method": "06",
    "buyer": { "name": "Sandbox buyer" },
    "items": [{
      "external_id": "demo-service-001",
      "name": "Sandbox non-VAT service",
      "quantity": 1,
      "unit_price": 1000,
      "tax_type": "D"
    }]
  }'
2. Read the HTTP 202 response — successful sandbox excerpt
{
  "data": {
    "invoice_id": "<returned invoice id>",
    "external_id": "demo-sale-001",
    "submission": {
      "accepted_by_risiti": true,
      "state": "completed",
      "terminal": true,
      "succeeded": true,
      "will_retry_automatically": false
    }
  },
  "request_id": "<request id for support>"
}
3. Set RISITI_INVOICE_ID to data.invoice_id and retrieve the record
curl --request GET \
  "https://sandbox-api.getrisiti.com/v1/direct/invoices/$RISITI_INVOICE_ID" \
  --header "Authorization: Bearer $RISITI_SANDBOX_KEY"
4. Download and run the Direct API example with Node.js 22.6+
curl --fail --output risiti-sandbox-invoice.ts \
  'https://getrisiti.com/downloads/risiti-sandbox-invoice.ts'

# Set RISITI_SANDBOX_KEY securely first; never paste it into source control.
# Use your own stable identifiers for this fictional sale.
RISITI_SALE_ID='my-sandbox-sale-001' \
RISITI_IDEMPOTENCY_KEY='my-sandbox-request-001' \
node --experimental-strip-types --experimental-default-type=module risiti-sandbox-invoice.ts
5. Reconcile the returned invoice with GET only
# Retain the same sale ID and request key. Replace the invoice placeholder.
RISITI_SALE_ID='my-sandbox-sale-001' \
RISITI_IDEMPOTENCY_KEY='my-sandbox-request-001' \
RISITI_INVOICE_ID='<returned invoice id>' \
node --experimental-strip-types --experimental-default-type=module risiti-sandbox-invoice.ts

# Exit 0: sandbox processing completed, not a production KRA receipt.
# Exit 2: pending or reconciliation unavailable; retain the existing ID.
# Exit 1: operator action or an error; inspect the original request.

A 202 response is acceptance; submission tells you the outcome

Build completion checks from data.submission. A successful finish requires both terminal and succeeded to be true. The legacy status and etims_status fields remain for compatibility; submitted in those fields does not prove that KRA has accepted the invoice.

Store data.invoice_id, your external_id and the response request_id. After live completion, retrieve the invoice's available kra_invoice_number, kra_receipt_number, kra_receipt_signature and qr_code_data. The risiti_url field is an app invoice link; do not present it as an anonymous public receipt URL.

The canonical submission lifecycle for your integration
submission.stateWhat it meansYour product's next action
queuedSaved by Risiti; delivery has not started.Show processing and keep the existing invoice ID.
submitting_to_kraA submission attempt is in progress.Wait for a webhook or reconcile the existing invoice.
waiting_for_kraThe invoice is waiting for upstream recovery.Check will_retry_automatically and next_retry_at; do not create another invoice.
completedterminal: true and succeeded: true.Store the result; fetch live receipt fields where available.
action_requiredterminal: true and succeeded: false.Stop polling and follow submission.message. Escalate exhausted retries to support before any resubmission.

Sandbox first, production by approval

A new workspace can use sandbox before paying for live access. Sandbox keys create test merchants and exercise the API contract against the sandbox environment. They cannot be switched into production by changing a URL or request field.

Production keys remain server-gated. Live access requires the billing setup for your product and a review of the organization, KRA ownership, integration contact, sandbox results, webhook handling and operational readiness. Every connected merchant still needs the taxpayer and OSCU setup required for live submission.

OSCU, VSCU and the Risiti API are different layers

KRA describes OSCU as suitable for systems that are always online and VSCU as suitable for bulk invoicing or systems that are not always online. Both are official KRA system-to-system routes with development, sandbox testing, vetting and certification requirements.

Risiti's public API gives product teams a stable merchant and invoice contract. It does not rename itself as KRA's API, and using Risiti does not remove taxpayer verification or any approval KRA requires. Your architecture decision should be based on transaction timing, connectivity, volume, branch structure and reconciliation needs.

Built for failure handling, not only successful demos

Once Risiti accepts an invoice, it owns KRA delivery and retries temporary upstream failures automatically. Do not create another invoice because KRA is slow. Retry an uncertain HTTP request with its original idempotency key and unchanged JSON; a rejected request that needs corrected data requires a new key.

Production integrations should alert on old pending invoices, repeated failures and webhook dead letters. Support staff should be able to find a request by request ID, merchant, external invoice ID or API log without exposing credentials or raw infrastructure details to end users.

Six integration checks before production

Keep the request, response, invoice ID and expected outcome for each check in your release evidence. Rehearse transport and webhook failures in your own test harness. A simulated sandbox result cannot establish live KRA availability, production latency or tax acceptance.

Risiti retains idempotency records for 30 days. An exact replay returns the original resource result with Idempotency-Replayed: true and a request_id for the new HTTP request. Your own sale-to-invoice mapping must remain durable after that replay window.

Failure and recovery acceptance criteria
Test caseExpected signalPass condition
1. Valid saleHTTP 202 and data.invoice_id.The sale is linked to one invoice; success is shown only when submission.terminal and submission.succeeded are true.
2. Response lost after POSTThe caller cannot tell whether the request was accepted.Replay identical JSON with the same key. A replay returns the same invoice_id; no second sale is created.
3. Key reused with changed JSONHTTP 409 with error.code: idempotency_conflict.The caller stops and fixes the key/body mismatch instead of treating the conflict as success.
4. Invalid invoice dataHTTP 422 with safe field or business-rule details.The operator can correct the data, track the correction and submit the corrected request with a new key.
5. Rate limit or upstream delayHTTP 429 with Retry-After, or an accepted invoice in waiting_for_kra.Honor Retry-After for the HTTP request. For an accepted invoice, let Risiti own KRA retries; route action_required to an operator.
6. Duplicate or missing webhookRepeated X-Risiti-Delivery, or no event received for a saved invoice.Verify signatures over the raw body, process a delivery once and reconcile missing events with GET /invoices/{invoiceId}.

Buyer-initiated invoicing is available in sandbox preview

KRA distinguishes the public Buyer Initiated Invoicing route, accessed through eCitizen or USSD with seller approval, from reverse invoicing for structured supply chains. Reverse invoicing uses an approved buyer's existing billing system, OSCU or VSCU integration, KRA KYC approval and seller consent. Establish the applicable route before designing a procurement integration.

Risiti now exposes a sandbox buyer-invoice resource so procurement platforms can test creation, seller approval or rejection, consent expiry, signed webhooks and audit history alongside ordinary sales invoicing. Live KRA transmission remains disabled until the buyer's approved route and production capability are configured.

What to prepare before integration

Map the business that legally issues each invoice, the branch ID, item and tax-code ownership, original invoice identifiers, buyer PIN rules and how users will see pending or failed submissions. Decide which system owns retries and which system remains the financial source of truth.

  • A stable internal merchant ID and invoice ID for every API object.
  • A backend service that stores API keys securely and can rotate them.
  • An HTTPS webhook receiver with signature verification and idempotent event processing.
  • A reconciliation job for invoices that remain pending or miss a webhook.
  • An operator workflow for validation failures, credit notes and production incidents.

Official sources

Product guidance on this page is separated from KRA's official requirements. Use the current KRA documents below for regulatory and certification decisions.

Questions Kenyan businesses ask

Start with the contract

Build the eTIMS workflow in sandbox, then prove it before going live.

Choose Direct API for one business or Platform API for connected merchants, generate a sandbox key and test invoice states, idempotency and webhook recovery.