GST Invoicing on Razorpay Builds: Where Indian E-Commerce Sites Quietly Break

GST Invoicing on Razorpay Builds: Where Indian E-Commerce Sites Quietly Break

A D2C skincare founder called us three weeks after launch — her CA had rejected eight months of Razorpay receipts as non-compliant invoices. The payment gateway was not the problem; treating GST invoicing as a gateway feature instead of an accounting workflow was.

29 May 2026·10 min read·Ritwik Bhattacharya

A D2C skincare founder rang us three weeks after launch, voice tight, because her CA had just rejected eight months of Razorpay payment receipts as non-compliant invoices. Two things had gone sideways. Sequential invoice numbering was broken across the March 31 boundary — the counter had reset on April 1 the way the developer thought it should, but then ran in parallel with old numbers that were still being generated for refunds and partial captures. And IGST had been charged on a Karnataka customer order from a Haryana-registered seller, when the correct split was CGST plus SGST, because the code was reading the billing address from the Razorpay payload instead of the shipping address from WooCommerce. Eight months of transactions. One very unhappy chartered accountant.

This was not a Razorpay problem. Razorpay had done what Razorpay does — moved money and emitted a receipt. The problem was that the build had treated GST invoicing as a payment-gateway feature when it is, and always was, an accounting workflow that happens to be triggered by a payment event. We have seen this go the same way on several SMB builds we have either shipped or inherited over the last two years, and the failure points are remarkably consistent.

What the Payment Gateway Actually Gives You — and What It Doesn't

Razorpay Standard Checkout, Razorpay Payment Pages, Cashfree Payment Gateway 2022 API — all of them give you a payment confirmation. That is a receipt that says money moved from a card or UPI handle into your settlement account, with a transaction ID, a timestamp, an amount, and usually a customer email. It is not a tax invoice. It does not carry your GSTIN in the required format, it does not carry the buyer's GSTIN when the buyer is a registered business, it does not split CGST/SGST/IGST on the line items, it does not carry HSN or SAC codes, and the invoice number on it — if there is one at all — is the gateway's own reference, not a number that belongs to your books.

Rule 46 of the CGST Rules is fairly specific about what a tax invoice has to contain. A consecutive serial number, unique for a financial year, not exceeding sixteen characters, in one or multiple series. Name, address and GSTIN of the supplier. Name, address and GSTIN of the recipient if registered. HSN code for goods, SAC for services. Taxable value, rate, and amount of tax charged under each head. Place of supply, with the State name and code, for inter-state transactions. Signature or digital signature. None of that comes out of the gateway. The gateway is plumbing. The invoice is accounting.

The Razorpay Invoices product — yes, we know it exists — gets you closer, but it still expects you to feed it correct HSN/SAC, correct place-of-supply, correct GSTIN capture at checkout, and a numbering strategy that holds across financial years. If you have not designed those upstream, the Razorpay Invoices output is just a prettier version of the same broken data.

The Four Places GST Logic Breaks on a Live Site

In roughly the order they blow up:

1. Sequential numbering across April 1. Rule 46 wants a consecutive, unique series within a financial year. The cleanest pattern we use is something like WY/2024-25/0001, resetting to WY/2025-26/0001 on April 1, with the FY prefix encoded in the number itself so there is no ambiguity. Where this breaks: refund credit notes that re-use invoice numbers, partial captures that mint a second invoice for the same order, staging environments that share the production database and silently increment the counter, and — the classic — a developer who decides on March 28 that the counter should reset, ships it, and then discovers on April 3 that three orders from March 30 are now sharing numbers with April orders because of a timezone bug between IST and UTC. Anchor the counter to IST. Always.

2. Place-of-supply derived from the wrong address. For goods, place of supply is generally the delivery address — the shipping address, not the billing address. If your seller is registered in Haryana and the customer is shipping to Karnataka, that is an inter-state supply and you charge IGST. If shipping to Gurgaon, intra-state, you charge CGST plus SGST. The Razorpay checkout payload gives you billing address by default. WooCommerce stores both. We have lost count of the builds where the invoice generator was reading $order->get_billing_state() when it should have been reading $order->get_shipping_state() — and the bug only surfaces when a Mumbai-based gifter ships to their parents in Chennai.

3. HSN for goods, SAC for services, on the same invoice. A skincare brand selling a serum and bundling a 1:1 consultation call is selling goods and a service. The serum needs an HSN code, the consultation needs a SAC code, and they sit on the same invoice with potentially different GST rates. Most WooCommerce setups we inherit have a single "tax class" field per product and no separate HSN/SAC column at all. The fix is a custom product meta field for each, with a sensible default per product category, and a validation rule that refuses to publish a product without one.

4. Export orders and reverse charge. If the skincare brand ships a serum to a buyer in Singapore, that is a zero-rated supply under GST — usually invoiced under LUT (Letter of Undertaking) with zero tax, or with IGST paid and refunded. The invoice still has to be generated, it still has to carry a sequential number from the same series, it still has to flag the supply as export with or without payment of tax, and the place of supply has to be recorded as the foreign country with code "96" or "97" depending on which return you are filing. For inbound services from foreign vendors — say a Figma subscription billed to the company — reverse charge applies and the SMB has to self-invoice. Almost no SMB build we have seen handles this on the website itself; it gets handled in Tally or Zoho Books later. That is fine, as long as somebody knows it has to happen and there is a written handoff between the site and the books.

A concrete example. A Haryana-registered seller ships a ₹4,000 (taxable value) serum to a customer in Bengaluru. GST rate 18%. Because Haryana to Karnataka is inter-state, the invoice carries IGST at 18% — ₹720 of IGST, total ₹4,720. Same order, same seller, same product, shipped instead to a customer in Faridabad: intra-state, so the ₹720 splits as ₹360 CGST plus ₹360 SGST, total still ₹4,720. The customer pays the same amount either way. The accounting is completely different, and the returns filed in GSTR-1 are completely different. Get this wrong and you are either over-claiming input credit in one state or under-paying tax in another, and both are caught at reconciliation.

For B2B orders — say a corporate gifting buyer who wants the GST input credit — the checkout has to capture the buyer's GSTIN, validate its 15-character format, and ideally hit the GSTN public API to confirm the legal name matches. We add a simple "Buying for a business?" toggle on the checkout that reveals a GSTIN field and a company-name field, and we store both on the order. Without buyer GSTIN on the invoice, the buyer cannot claim input credit, and you will get an angry email from their finance team within the week.

How We Wire Invoicing Into a Six-Week Build

The honest answer: three calls in discovery week, before a single line of gateway code is written.

First, the numbering scheme. FY-prefixed, IST-anchored, separate series for invoices and credit notes, with the counter living in the database (not in WooCommerce options, which can desync on restore-from-backup). We write a small unit test that simulates an April 1 rollover and a refund issued on March 31 for a March 15 order, just to be sure the credit note picks up the old FY series.

Second, place-of-supply derivation. One function. One source of truth. Reads shipping state for goods, billing state for services, with explicit handling for export destinations. Everything else — invoice PDF, GSTR-1 export, refund credit note — calls that one function. When a regulator query lands six months later, you want one place to point at, not four.

Third, GSTIN capture at checkout. B2B toggle, format validation client-side, GSTN lookup server-side with a 24-hour cache so we are not hammering the API on every checkout. If the lookup fails we still accept the order, but we flag it for manual review before the invoice is finalised. Who owns that review queue — founder, ops, external CA — gets named in the discovery doc, not assumed.

On the tooling question: WooCommerce PDF Invoices & Packing Slips by Ewout Fernhout, with the Professional or GST-focused add-ons, gets you about 70% of the way there for a straightforward goods-only D2C store under ₹2 crore in turnover. It handles templates, sequential numbering, and basic tax breakdowns. Where it falls short, in our experience: complex place-of-supply rules for mixed goods/services carts, B2B GSTIN validation, export invoicing with LUT references, and credit-note numbering tied to original invoice FY. For anything beyond a single-state goods seller, we end up writing a custom invoice service — usually a small Node.js or Laravel module sitting alongside the WooCommerce site, listening on the woocommerce_order_status_completed webhook, generating the PDF with a templating engine, storing it in S3, and pushing the metadata to Zoho Books or Tally Prime via their respective APIs. The plugin route is cheaper to start and harder to extend. The custom service is the reverse. Pick based on where you expect to be in 18 months, not where you are today.

One more thing that creeps up on people: e-invoicing. As of the current threshold, businesses with aggregate annual turnover above ₹5 crore have to generate e-invoices through the IRP (Invoice Registration Portal) and get an IRN (Invoice Reference Number) and a signed QR code back, which then has to appear on the printed invoice. The threshold has been ratcheting down — ₹500 crore in 2020, then ₹100 crore, then ₹50, then ₹20, then ₹10, now ₹5. If you are at ₹3.5 crore today and growing, build the IRP integration hook now, even if you do not turn it on. Retrofitting it after you cross the threshold while running live transactions is not a weekend job.

And finally, retention. The CGST Act requires you to keep invoices and related records for 8 years from the due date of the annual return for the relevant FY. That means the S3 bucket your invoice PDFs live in needs a lifecycle policy that does not quietly delete anything for the better part of a decade, and the database row that ties an invoice number to an order ID needs to survive whatever WooCommerce migration the next agency runs. We have twice had to recover invoice PDFs from email backups because the previous developer had set objects to expire after 365 days. Not a good afternoon.

If you are still in discovery, decide invoice numbering, place-of-supply derivation, and GSTIN capture this week — before the gateway is wired in, and name the owner for each in writing. If you are already live and reading this with a sinking feeling, pull last quarter's transactions into a spreadsheet, sort by ship-to state, and check those four failure points against your invoices before your next GSTR-1 filing. Better you find it than your CA does.

Ritwik Bhattacharya

Written by

Ritwik Bhattacharya

Principal Engineer

Eleven years in front-end engineering, based in Gurgaon. Builds in Next.js and Node, reluctantly maintains the WordPress installs we inherit from older clients. Writes here about the engineering side of the practice — frameworks, performance budgets, and the bugs that keep coming back (usually hydration).

                    

Ready to build something that works?

Get a free homepage design for your business. No commitment, no fluff — just what it could look like.

Call us — picked up by a human+91 88820 82228WhatsApp us — replies in minutes+91 88820 82228