Financial APIs punish sloppy integration harder than any other kind. A retried search query costs nothing; a retried invoice creation can issue a statutory document twice. This guide covers the three disciplines that make an idempotent invoice API integration safe in production — idempotency keys, a correct retry policy, and webhook-driven status — written for the developer wiring an accounting platform into an existing application.
Why a duplicate invoice is worse than a failed one
Understand the asymmetry first, because it should shape every decision below. A failed invoice creation is recoverable: retry it, or surface it to a human queue. A duplicate invoice is a legal document issued twice — it overstates your sales in GSTR-1, corrupts the customer's ledger (and their input tax credit claim, if B2B), and can only be unwound with a formal credit note that itself must be issued and reported. If the duplicate also obtained an IRN, you are inside a 24-hour cancellation window or issuing corrective documents. The practical rule: when in doubt, prefer a missing invoice you can detect over a duplicate you must legally unwind. Then build detection so nothing stays missing.
Idempotency keys done right
An idempotency key is a unique value you attach to each logical create request. If the platform sees the same key again, it returns the original result instead of creating a second invoice. The key rules:
- Derive the key from your domain, not from randomness. A random UUID per attempt defeats the purpose — a retry would carry a new key. Derive it from the business event: the order ID, or subscription ID plus billing period. One logical invoice, one key, forever.
- Store the key with your record before calling. Persist "invoice pending, key X" on your side first, so a crash between call and response still leaves you able to re-ask with the same key.
- Same key must mean same payload. If you need to correct the data, that is a new business decision — not a reuse of the old key with different line items. Good platforms reject key reuse with a changed payload; treat that error as a bug in your code.
- Know the dedupe window. Platforms keep idempotency records for a finite period. Check it covers your longest realistic retry horizon (including a weekend outage).
The retry decision tree
Not every error deserves a retry. Classify the response and act accordingly:
- Validation errors (4xx: bad GSTIN, missing HSN, malformed payload) — never retry unchanged; the same request will fail the same way. Route to a human or fix the master data, then submit as the same idempotency key with corrected data only if the platform's contract allows it, otherwise as a new keyed request after voiding the old one.
- Rate limiting — back off and retry with the same key. Honour any retry-after hint; add jitter so a fleet of workers doesn't retry in lockstep.
- Server errors and timeouts (the ambiguous cases) — this is what idempotency keys exist for. You do not know whether the invoice was created. Retry with the same key: if it was created, you get the original back; if not, it is created once. Cap the attempts (three to five with exponential backoff is typical) and then park the item in a dead-letter queue that a human reviews the same day — silence is the failure mode that costs money.
One subtlety worth encoding: distinguish connection failures before the request was sent (safe to retry even without idempotency) from timeouts after sending (retry only with the key). If you cannot tell them apart, treat everything as the dangerous case.
Webhooks vs polling for invoice status
For anything the platform does asynchronously — batch runs, IRN generation, payment application — you need to learn the outcome. Polling (ask every N seconds) is tempting because it is easy, but it scales badly, adds latency, and burns rate limit exactly when you are busiest. Webhooks invert the flow: the platform calls you when state changes. In practice you want both, in a specific shape:
- Webhooks as the primary channel — invoice created, IRN generated or failed, batch completed, payment recorded.
- A reconciliation poll as the safety net — a periodic job that lists recent invoices and diffs them against your records, catching any webhook that was lost. Webhook delivery is at-least-once, not exactly-once and not guaranteed-forever; the poll is what makes your integration self-healing.
Writing a webhook handler that survives production
The handler is simple code with strict rules:
- Verify the signature. Every serious platform signs webhook payloads with a shared secret. Verify before parsing; an unauthenticated endpoint that updates financial state is an attack surface.
- Acknowledge fast, process later. Persist the event and return success immediately; do the real work (updating orders, notifying customers) in a background job. Slow handlers get timed out and re-delivered, compounding load.
- Be idempotent — again. The same event will occasionally arrive twice. Key your processing on the event ID so the second delivery is a no-op.
- Tolerate disorder. Events can arrive out of order. Trust the state carried in the event (or re-fetch the resource) rather than assuming sequence.
- Monitor the endpoint. Alert on error rates and on silence — a webhook endpoint that has heard nothing on billing day is itself an incident.
Design the reconciliation job on day one
Every safeguard above can still leak in rare combinations, so close the loop with an invariant check: a scheduled job that compares what your system believes was invoiced against what the platform reports, over a rolling window, alerting on any diff. This is a few hours of work that converts "we think billing works" into "billing is proven complete every day". At subscription scale this job is non-negotiable — see automating recurring invoices for how it anchors a burst billing run.
The contract to look for
When evaluating an invoicing API, this whole article compresses into four contract questions: native idempotency keys with a documented dedupe window; a documented error taxonomy separating retryable from non-retryable; signed webhooks for every asynchronous outcome; and list/read endpoints suitable for reconciliation. The broader evaluation — auth, GST field handling, ledger posting — is in our complete invoicing API integration guide.
These guarantees are built into Invodo's API contract — idempotent creation, signed lifecycle webhooks, and batch completion reports designed for exactly the reconciliation pattern above. Explore the platform to see what an accounting-grade contract looks like in practice.
Put this into practice with Invodo
GST-compliant invoicing, e-invoicing, and purchase management built for Indian businesses.
Invodo Editorial
Reviewed by a Chartered Accountant
The Invodo editorial team writes practical, India-specific guides on GST and business finance. Compliance content is reviewed by a practising Chartered Accountant.