Denshin / Blog / Engineering
Notes from wiring GST e-invoicing into a billing system
Engineering notes for adding GST e-invoicing to an ERP or custom billing app: the 30 day reporting limit and 24 hour cancellation window, idempotent IRN calls, validation, locked invoices, credit notes and what to store.
Denshin Engineering · Engineering Team · 20 October 2026 · 5 min read
E-invoicing in India is a simple idea with a lot of edges. Your system sends invoice details as JSON to the Invoice Registration Portal (IRP). The IRP validates it and returns an Invoice Reference Number (IRN) and a digitally signed QR code, and only then is the invoice valid for a B2B supply. The details flow on to the GST portal and, where needed, to the e-way bill system.
These are the notes we keep for teams adding this to an ERP or a custom billing system. The tax questions belong to your CA. These are about the code.
Who it applies to, and two time limits
As of October 2026, e-invoicing applies to businesses whose aggregate annual turnover has crossed Rs 5 crore in any financial year since 2017-18. Two time limits shape the design:
- Since 1 April 2025, taxpayers with AATO of Rs 10 crore or more cannot report an invoice, credit note or debit note to the IRP more than 30 days after the document date. The portal simply rejects it. Below Rs 10 crore there is no such block at the moment.
- An IRN can be cancelled only within 24 hours of generation, and only in full. After that, the correction is a credit note.
Both are about time, and both turn a relaxed "we'll fix it later" into a hard failure. Most of the design is about respecting them.
Register the invoice when it is finalised
The cleanest flow we have found: the invoice is a draft and freely editable until the user finalises it. Finalising calls the IRP. On success the invoice is locked, and the IRN, acknowledgement number, acknowledgement date and signed QR are stored on the record and printed on the PDF. If the call fails, the invoice stays finalised but flagged as pending IRN, and a background job retries.
The 30-day rule means pending cannot be allowed to sit. Put an alert on anything pending for more than a couple of days, and make it louder as day 30 approaches. A queue that quietly retries for a month is how you end up with an invoice the portal will never accept.
Make the call idempotent
The IRP rejects a second request for the same document number, type and financial year with a duplicate IRN error. That is helpful, as long as your code treats it properly. If the first call timed out after the IRP had already generated the IRN, the retry gets "duplicate", and the right response is to fetch the existing IRN by document details and store it. Not to mark the invoice failed, and certainly not to reissue it under a new number.
try {
irn = await irp.generate(payload)
} catch (err) {
if (!isDuplicateIrn(err)) throw err
irn = await irp.getByDocument(payload.docType, payload.docNo, payload.docDate)
}
await invoices.attachIrn(invoice.id, irn)
The exact error code and the lookup endpoint depend on your GSP or the IRP you connect to, so read their docs rather than ours.
Validate before you send
Most rejections are data problems: an inactive or mistyped GSTIN, a wrong HSN code, a state code that does not match the GSTIN, totals that do not add up after rounding, a pincode in the wrong state. Each is cheaper to catch on the form than at the portal. We check GSTIN format and checksum on entry, look up its status through the provider where the API allows, compute totals in paise as integers and round once at the end. Money looks like a number and is not one, which is the same lesson as our timestamp bug in a different costume.
A registered invoice is read only
Once there is an IRN, the invoice in your database has to match what the IRP holds. The admin panel should not allow edits to amounts, items or parties on a registered invoice. Within 24 hours, offer cancel and reissue. After that, offer a credit note flow that creates its own document, gets its own IRN and links back to the original. We are firm believers in shipping a capable admin panel, and this is the one place where a too-permissive one does real damage. Make the locked state obvious in the UI so nobody wonders why the edit button is gone.
Providers, sandboxes and month end
Most teams integrate through a GST Suvidha Provider or an ASP rather than calling the IRP directly. They handle auth tokens, give you a sandbox and sometimes a friendlier API. Keep the provider behind a small interface of your own so switching later is a change in one module. In sandbox, test the awkward cases: credit notes, exports, reverse charge, SEZ supplies, and invoices with a few dozen lines.
The portals do slow down and occasionally go down, usually at month end when everyone is filing. Retries with backoff, a visible pending queue and a screen where finance can see what is stuck are worth more than any clever code.
E-way bills
For goods that need an e-way bill, the IRP can generate it along with the IRN if transport details are included, or later using the IRN. Store the e-way bill number and validity next to the IRN. It has its own cancellation window, separate from the invoice's.
What to store
- IRN, acknowledgement number and date, the signed QR string and the signed invoice JSON.
- The exact payload you sent and the raw response, for the day someone disputes a figure.
- Links between cancelled invoices, reissues and credit notes.
- One status per invoice that finance can filter on.
These rules have changed several times since e-invoicing started in 2020 and will change again. GSTN publishes advisories, and the IRP publishes its own notes for API changes. Put a named person next to "read the advisories", or you will learn about the next change from a rejected invoice.
Tags: GST, E-invoicing, Integrations, India, Billing
All posts · Work with Denshin