If you've ever been handed a ticket that says "integrate e-Invoicing" with zero context, you know the first hour is spent just figuring out what you're actually building. Not the compliance side — someone in finance already explained that. The integration side: what's the payload, what's the auth flow, and why does the sandbox behave differently from what the docs say.
I've worked on a few of these integrations now (ERP-side and standalone billing systems), and this post is the write-up I wish I'd had before the first one. No fluff, just what actually matters when you're the one writing the code.
What "generate an e-Invoice" actually means as an API call
Under GST rules, once a business crosses a turnover threshold, B2B invoices need to be registered with the government's Invoice Registration Portal (IRP) before they're legally valid. Registering one gets you back:
an IRN (Invoice Reference Number) — a unique hash tied to that invoice
a QR code that has to appear on the printed/PDF invoice
signed invoice data you need to store as proof of registration
So functionally, you're building a generateIRN(invoicePayload) call that sits between "invoice created in your system" and "invoice sent to customer." Simple in concept. The payload itself, though, has a lot of surface area — supplier/buyer GSTIN and address details, line-item HSN/SAC codes, tax breakdowns per line, and totals that all have to reconcile exactly, or the IRP rejects the whole thing.
*The stuff that isn't in the "getting started" docs
*
- Timeouts don't mean failure. This one bit me early. If a generateIRN call times out on your end, that does not mean it failed on the IRP side. If you blindly retry, you risk generating two IRNs for the same invoice — which is its own mess to clean up. The right move is: on ambiguous failure, call the "get IRN by document details" lookup first, and only resubmit if it genuinely wasn't registered.
js
async function safeGenerateIRN(invoice) {
try {
return await generateIRN(invoice);
} catch (err) {
if (isTimeoutOrAmbiguous(err)) {
const existing = await getIRNByDocDetails(invoice.docNumber, invoice.docDate);
if (existing) return existing;
}
return await generateIRN(invoice); // safe to retry now
}
}
Cancellation has a window and preconditions. You can't cancel an e-Invoice whenever you want — there's a limited time window, and if an E-Way Bill is already linked to it, that has to be handled first. Check eligibility client-side before calling the cancel endpoint, or you'll be surfacing a confusing IRP rejection to whoever's using your app.
Rate limits mean you need a queue, not just a retry loop. If your system can create invoices faster than the IRP will accept IRN submissions (very possible during month-end batch runs or a flash sale), you need actual backpressure handling — a queue that submits within rate limits — not a for loop with await and a prayer.
Real-time validation saves you from yourself. Validate HSN codes, tax rates, and place-of-supply logic before you submit, using the same rules the IRP enforces. Catching an error client-side takes milliseconds. Catching it after an IRP rejection means parsing their error response and mapping it back to a field — every provider's error codes are a little different, and not always self-explanatory.
Sandbox vs. production: what actually changes
Sandbox is fine for validating your payload shape and auth flow. It will not prepare you for:
realistic rate limiting under load
what happens when the IRP itself is slow (it happens, especially near filing deadlines)
multi-GSTIN session handling, if your business operates across states
If you're building for a business with more than one GSTIN, isolate sessions/credentials per GSTIN from day one. I've seen a setup where a token issue on GSTIN #1 silently backed up invoice queues for GSTIN #2 and #3 — because auth was treated as global instead of per-entity. Painful to debug in production, trivial to avoid in the initial design.
*A minimal mental model
*
If I had to summarize the whole integration in one diagram:
[Invoice created]
↓
[Client-side validation: HSN, tax rate, place of supply]
↓
[Submit to IRP via generateIRN — queued, rate-limit aware]
↓
success? → [Store IRN + QR + signed data] → [Sync back to invoice record]
ambiguous failure? → [Lookup by doc details] → [retry only if truly missing]
hard failure? → [Surface specific validation error to user]
Everything else — cancellation, bulk generation, E-Way Bill linkage — builds on top of this same core loop.
*Wrapping up
*
None of this is exotic engineering. It's mostly just knowing the failure modes ahead of time instead of discovering them in production during month-end billing. If you're picking a provider to build on top of, the thing worth actually evaluating isn't the happy-path demo — it's how well they document the retry logic, cancellation rules, and rate limits, because that's what you'll actually be writing code against.
I work with e-Invoice/GST/E-Way Bill APIs fairly often at PeriOne — happy to answer questions in the comments if you're mid-integration and stuck on something specific.
Top comments (0)