Shopify GST Invoicing App
Auto-generate GST-compliant invoices for every Shopify order. CGST/SGST/IGST is derived from the shipping address, buyer GSTIN flows from checkout note attributes, and every webhook is HMAC-verified per store.
Endpoint: POST /api/public/v1/shopify/webhook
Zero-touch invoicing
An invoice is created the moment Shopify marks an order as paid.
Correct tax split
CGST/SGST when shipping state == your state, IGST otherwise. Always.
Per-store HMAC secret
Each store gets its own signing secret. Mismatch = 401.
Idempotent
Retries from Shopify never produce duplicate invoices.
Setup in 4 steps
From sign-in to your first auto-generated invoice in under five minutes.
- 1
Configure your supplier details
In Settings, save your company name, GSTIN, and state. Your state determines the CGST/SGST vs IGST split for every Shopify order.
- 2
Connect your Shopify store
Open Shopify, click Connect store, and paste your
<store>.myshopify.comdomain. We'll issue a webhook URL and a per-store signing secret (shown once). - 3
Register the webhook in Shopify
In Shopify Admin, go to Settings → Notifications → Webhooks (or via Admin API). Create a webhook for the
orders/paidtopic, format JSON, URL and secret from step 2. - 4
Place a test order
Mark a test order as paid in Shopify. Within seconds, the invoice appears under Invoices with the correct tax split. The Last event column on the Shopify page updates too.
Authentication
There are two distinct authentication boundaries to think about.
1. Dashboard (you ↔ us)
You sign in to your GST Invoice account with email/password or Google. All store management — connecting stores, rotating secrets, toggling auto-create, viewing invoices — is gated by your session and protected by row-level security.
2. Webhook (Shopify ↔ us)
Shopify authenticates to our webhook endpoint by signing each request with a per-store secret. We verify the signature on the raw body before parsing — no signature, no processing.
Required webhook headers
| Header | Purpose |
|---|---|
| X-Shopify-Shop-Domain | Your store domain, e.g. my-store.myshopify.com — used to look up the signing secret. |
| X-Shopify-Topic | Event topic. We act on orders/paid and orders/create (per your settings). |
| X-Shopify-Hmac-Sha256 | Base64 HMAC-SHA256 of the raw request body using the per-store signing secret. Verified server-side; mismatches return 401. |
| Content-Type | application/json — Shopify sends the order payload as JSON. |
Reference: HMAC verification
You don't need to run this — we verify it server-side. It's here so you can audit the algorithm or replay payloads in a local proxy.
// Node.js — verify Shopify HMAC before processing
import { createHmac, timingSafeEqual } from "crypto";
function verify(rawBody, header, secret) {
const computed = createHmac("sha256", secret).update(rawBody, "utf8").digest("base64");
const a = Buffer.from(computed);
const b = Buffer.from(header ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}Security model
Per-store signing secret
A unique high-entropy secret is generated when you connect each store. Secrets are stored encrypted and shown to you exactly once.
Raw-body HMAC verification
We compute HMAC-SHA256 over the unparsed request body and compare with a timing-safe equality check. Tampered payloads are rejected with 401.
Idempotency keys
We enforce a unique constraint on (shop_domain, shopify_order_id). Shopify's at-least-once retries are safe by construction.
Topic gating
Only the topics you've explicitly enabled (orders/paid and/or orders/create) are processed. Anything else returns 200 skipped.
Secret rotation
Rotate a secret any time from the dashboard. The old secret stops working immediately — update Shopify with the new one.
Replay-safe processing
Even if Shopify replays an old, valid payload, idempotency prevents duplicate invoices. Old store disconnects return 404 shop_not_connected.
Treat the signing secret like a password.
Anyone with the secret can post invoice-creating webhooks for your store. Don't paste it into chat, screenshots, or shared docs. If exposed, rotate immediately.
Webhook payload & responses
We accept Shopify's standard order JSON — no custom transformation required on your side.
Sample inbound webhook
// Example: Shopify orders/paid → our /api/public/v1/shopify/webhook
// Headers (sent by Shopify, verified by us):
// X-Shopify-Shop-Domain: my-store.myshopify.com
// X-Shopify-Topic: orders/paid
// X-Shopify-Hmac-Sha256: <base64 hmac>
//
// Body (excerpt — Shopify's standard order JSON):
{
"id": 8123456789,
"currency": "INR",
"total_price": "5900.00",
"line_items": [
{ "title": "Cotton Tee", "quantity": 2, "price": "2500.00", "sku": "TEE-M-BLK" }
],
"shipping_address": { "province_code": "KA", "country_code": "IN" },
"note_attributes": [
{ "name": "gstin", "value": "29AAGCB1234F1Z5" },
{ "name": "place_of_supply", "value": "29" }
]
}Responses
// 201 Created — invoice generated
{
"ok": true,
"invoice_id": "5b1e...c0",
"invoice_number": "INV-2026-000142",
"warnings": []
}
// 200 OK — duplicate (idempotent retry)
{ "ok": true, "duplicate": true, "invoice_id": "5b1e...c0" }
// 401 Unauthorized — HMAC mismatch
{ "error": { "code": "invalid_hmac", "message": "Webhook signature verification failed" } }Note attributes we read
Set these in Shopify checkout (Liquid, Checkout UI Extensions, or via the Storefront API). Any unrecognised attributes are ignored.
| Key | Meaning |
|---|---|
| gstin | Buyer GSTIN (15 chars). Stored on the invoice; enables B2B treatment. |
| place_of_supply | Optional override — 2-digit GST state code (e.g. 27 for Maharashtra). Defaults to the shipping address province. |
| legal_name | Optional — overrides the customer name printed on the invoice (useful for company billing names). |
State code mapping (sample)
Shopify province_code is translated to the 2-digit GST state code we need for the place of supply.
| Shopify code | GST state code | State |
|---|---|---|
| MH | 27 | Maharashtra |
| KA | 29 | Karnataka |
| DL | 07 | Delhi |
| TN | 33 | Tamil Nadu |
| GJ | 24 | Gujarat |
| TG | 36 | Telangana |
| WB | 19 | West Bengal |
| UP | 09 | Uttar Pradesh |
| KL | 32 | Kerala |
All 36 Indian states and UTs are supported — these are just examples. Override anything with the place_of_supply note attribute.
FAQ
Ready to auto-invoice every Shopify order?
Connect a store, paste one webhook, and you're done.