KRA's eTIMS (electronic Tax Invoice Management System) requires businesses in Kenya to generate electronic tax invoices for sales. When a customer pays through Paystack, you need to generate an eTIMS-compliant tax invoice for that transaction. Paystack does not generate eTIMS invoices for you. Your system must call the eTIMS API (directly or through a third-party integration) after each successful payment to generate and transmit the invoice to KRA.
This article is not tax or legal advice. eTIMS requirements are set by KRA and have been updated multiple times since the system was introduced. The information here is based on publicly available guidelines and may not reflect the most current rules.
Before implementing eTIMS integration:
eTIMS stands for electronic Tax Invoice Management System. It is KRA's system for tracking every business-to-customer and business-to-business sale in Kenya through electronic tax invoices.
The idea is straightforward: instead of businesses issuing paper receipts that may or may not be reported to KRA, every sale generates an electronic invoice that is transmitted to KRA in real time or near-real time. This gives KRA visibility into actual sales volumes, which helps them collect the correct amount of VAT and income tax.
For businesses, eTIMS replaces the old Electronic Tax Register (ETR) system. Instead of a physical device printing receipts, the invoice generation happens through software (a mobile app, a web portal, or an API).
For developers building products that process payments, eTIMS means one more system to integrate with. Every time a customer pays through your Paystack-powered product, you may need to generate an eTIMS invoice for that transaction and transmit it to KRA.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
When a sale occurs, eTIMS requires a tax invoice that includes:
The invoice must be transmitted to KRA through one of the approved channels: the eTIMS mobile app, the eTIMS web portal (Taxpayer Portal on iTax), or the eTIMS API. For automated systems that process payments through Paystack, the API is the relevant option.
The key point for developers: the Paystack payment confirmation and the eTIMS invoice are two different things. The Paystack webhook tells you money moved. The eTIMS invoice tells KRA that a taxable sale occurred. Your system connects these two events.
This is where many developers get confused. A Paystack payment confirmation is not a tax invoice. They serve different purposes.
Paystack payment receipt: Confirms that the customer paid a specific amount through a specific payment method. Contains the transaction reference, amount, currency, payment channel, and timestamp. This is a record of money moving. Paystack may send the customer an email receipt. This receipt is not an eTIMS-compliant tax invoice.
eTIMS tax invoice: A legally compliant document that records the sale for tax purposes. Contains seller and buyer details, item descriptions, tax breakdown, and the eTIMS control number from KRA. This is a record of a taxable event.
Both are generated for the same underlying event (a customer bought something), but they go to different places and serve different legal purposes.
Your system needs to:
Each Paystack transaction should map to one eTIMS invoice. The Paystack reference becomes the bridge between your payment records and your tax records.
The cleanest integration puts the eTIMS invoice generation in your Paystack webhook handler. When payment succeeds, generate the invoice. Here is the architectural flow:
// Paystack webhook handler with eTIMS integration
app.post('/webhooks/paystack', express.json(), async (req, res) => {
// Return 200 immediately
res.status(200).send('OK');
// Verify Paystack signature
const hash = crypto
.createHmac('sha512', process.env.PAYSTACK_SECRET_KEY)
.update(JSON.stringify(req.body))
.digest('hex');
if (hash !== req.headers['x-paystack-signature']) {
console.error('Invalid Paystack signature');
return;
}
const event = req.body;
if (event.event === 'charge.success') {
const { reference, amount, currency, metadata } = event.data;
// Step 1: Update order status
const order = await getOrderByReference(reference);
if (!order || order.status === 'paid') return;
await updateOrderStatus(order.id, 'paid');
// Step 2: Generate eTIMS invoice
try {
const etimsResult = await generateEtimsInvoice({
orderId: order.id,
items: order.items,
totalAmount: amount / 100, // Convert from cents to KES
customerName: order.customerName,
customerPin: order.customerKraPin || null, // Optional for B2C
paymentReference: reference,
paymentDate: new Date(),
});
// Step 3: Store the eTIMS control number
await storeEtimsRecord(order.id, {
controlNumber: etimsResult.controlNumber,
invoiceNumber: etimsResult.invoiceNumber,
paystackReference: reference,
generatedAt: new Date(),
});
} catch (etimsError) {
// eTIMS failed but payment succeeded
// Log the error and queue for retry
console.error('eTIMS generation failed:', etimsError);
await queueEtimsRetry(order.id, reference);
}
}
});
A critical design decision: what happens when the eTIMS API call fails but the payment succeeded? You have two options:
Most businesses choose Option A with a reliable retry mechanism. The eTIMS invoice can be generated minutes or hours after the sale and still be compliant, as long as it reflects the correct date and time of the original transaction. Check the current rules on acceptable delay.
KRA provides API access for eTIMS integration. The API allows your system to generate and transmit tax invoices programmatically without manual entry in the eTIMS portal.
The general API flow is:
// Simplified eTIMS invoice generation
// The actual API endpoint, authentication, and payload format
// must be verified against KRA's current eTIMS API documentation
async function generateEtimsInvoice(saleData) {
const invoicePayload = {
// Seller details (your business)
sellerPin: process.env.KRA_PIN,
sellerName: process.env.BUSINESS_NAME,
// Buyer details
buyerPin: saleData.customerPin || '', // Empty for B2C
buyerName: saleData.customerName || 'Walk-in Customer',
// Invoice metadata
invoiceDate: saleData.paymentDate.toISOString().split('T')[0],
invoiceTime: saleData.paymentDate.toISOString().split('T')[1].split('.')[0],
// Line items
items: saleData.items.map(item => ({
description: item.name,
quantity: item.quantity,
unitPrice: item.unitPrice,
totalPrice: item.quantity * item.unitPrice,
taxRate: item.vatRate || 16, // Standard VAT rate, verify current rate
taxAmount: (item.quantity * item.unitPrice * (item.vatRate || 16)) / 100,
})),
// Totals
totalExclTax: saleData.totalAmount / 1.16, // If prices include VAT
totalTax: saleData.totalAmount - (saleData.totalAmount / 1.16),
totalInclTax: saleData.totalAmount,
// Payment reference for your records
externalReference: saleData.paymentReference,
};
// Call the eTIMS API
// [TODO: verify current eTIMS API endpoint and authentication method]
const response = await fetch(process.env.ETIMS_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.ETIMS_API_TOKEN}`,
},
body: JSON.stringify(invoicePayload),
});
const result = await response.json();
if (!result.success) {
throw new Error(`eTIMS error: ${result.message}`);
}
return {
controlNumber: result.controlNumber,
invoiceNumber: result.invoiceNumber,
};
}
Important: The code above is illustrative. The actual eTIMS API endpoint, authentication mechanism, payload structure, and field names must be obtained from KRA's current eTIMS API documentation. KRA may use different formats, require specific certificates, or mandate a different authentication flow. Do not ship this code without verifying against the actual API documentation.
You do not have to build a direct eTIMS integration from scratch. Several Kenyan companies offer eTIMS middleware that simplifies the integration:
If you choose a third-party tool, verify:
For most developers, a third-party eTIMS middleware is the fastest path to compliance. Building a direct integration with the KRA API is feasible but requires more effort to get the authentication, certificate handling, and payload formatting right.
When a customer gets a refund through Paystack, you need to handle the eTIMS side as well. A refund does not cancel the original invoice. Instead, you generate a credit note.
The flow:
refund.processed webhook.// Handle refund and generate eTIMS credit note
async function handleRefund(originalReference, refundAmount) {
// Get the original eTIMS record
const etimsRecord = await getEtimsRecordByPaystackRef(originalReference);
if (!etimsRecord) {
console.error('No eTIMS record found for refunded transaction:', originalReference);
// Queue for manual review
await flagForManualReview(originalReference, 'Missing eTIMS record for refund');
return;
}
// Generate credit note through eTIMS
const creditNote = await generateEtimsCreditNote({
originalInvoiceNumber: etimsRecord.invoiceNumber,
originalControlNumber: etimsRecord.controlNumber,
refundAmount: refundAmount / 100, // Convert from cents
reason: 'Customer refund',
date: new Date(),
});
// Store the credit note reference
await storeCreditNoteRecord(originalReference, {
creditNoteNumber: creditNote.creditNoteNumber,
controlNumber: creditNote.controlNumber,
refundAmount: refundAmount / 100,
});
}
Credit notes are where eTIMS integration gets more complex. Not all refund scenarios are simple. Partial refunds, refunds months after the original sale, and refunds across different tax periods each have their own considerations. Consult your accountant for the correct handling of each scenario.
KRA has rolled out eTIMS in phases, with different deadlines for different categories of businesses. The rollout has been adjusted multiple times, so the current timeline may differ from what was originally announced.
General categories that have been subject to eTIMS requirements include:
[TODO: verify current eTIMS requirements]. Check the KRA website for your specific category and deadline. If you are unsure whether eTIMS applies to your business, ask your accountant.
For developers, the practical implication is this: even if eTIMS is not required for your business today, it likely will be in the near future. Building the integration now (or at least designing your system to accommodate it) is easier than retrofitting it later under deadline pressure.
When you design your payment flow, leave a hook in your webhook handler for eTIMS. Even if the hook does nothing today, having the architecture in place means adding eTIMS later is a matter of implementing one function, not restructuring your entire payment pipeline.
Here are patterns that make eTIMS integration less painful:
For related regulatory and compliance topics:
For building production payment systems with proper compliance, the McTaba 26-week bootcamp covers payment integration, API design, and the business context of building for the Kenyan market.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course