{
"parameters": {
"method": "POST",
"url": "=https://{{ $credentials.shopifyApi.shop }}.myshopify.com/admin/api/2025-07/graphql.json",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "X-Shopify-Access-Token", "value": "={{ $credentials.shopifyApi.accessToken }}" },
{ "name": "Content-Type", "value": "application/json" }
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"query\": \"query OrdersToday($cursor: String) { orders(first: 50, after: $cursor, query: \\\"created_at:>=$YESTERDAY_ISO$\\\", sortKey: CREATED_AT) { edges { cursor node { id name createdAt totalPriceSet { shopMoney { amount currencyCode } } subtotalPriceSet { shopMoney { amount } } totalTaxSet { shopMoney { amount } } customer { firstName lastName email } lineItems(first: 50) { edges { node { id name quantity sku originalUnitPriceSet { shopMoney { amount } } } } } transactions(first: 5) { id gateway authorizationCode amountSet { shopMoney { amount } } } refunds(first: 5) { id totalRefundedSet { shopMoney { amount } } refundLineItems(first: 10) { edges { node { quantity lineItem { sku } } } } } } } pageInfo { hasNextPage } } }\",\n \"variables\": { \"cursor\": null }\n}"
},
"name": "Shopify orders today",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [460, 300]
}
The Shopify Admin GraphQL API takes one POST and returns 50 orders with line items, transactions, and refunds in a single response — much faster than the REST equivalent's 3-4 round trips. The 2025-07 API version is the one stable in production at our test date; bump as needed when newer versions stabilise. The query filter created_at:>=... uses Shopify's search syntax. Pagination via the cursor variable; we use a Loop Over Items node to walk through pages until hasNextPage is false.
### Node 2 — GST 2.0 slab lookup from live sheet
{
"parameters": {
"jsCode": "// Read the GST mapping sheet (preceding Sheets node returned its rows as items)\nconst slabRows = $('GST Slab Sheet').all().map(i => i.json);\nconst slabBySku = {};\nfor (const r of slabRows) {\n slabBySku[r.sku.trim().toUpperCase()] = {\n rate: Number(r.gst_pct),\n cgst: Number(r.gst_pct) / 2,\n sgst: Number(r.gst_pct) / 2,\n igst: Number(r.gst_pct),\n isException: r.is_exception === 'YES'\n };\n}\n\n// Process each Shopify order\nconst orders = $input.all().map(i => i.json);\nconst out = [];\nfor (const order of orders) {\n const lines = order.lineItems.edges.map(e => e.node);\n const linesWithGst = lines.map(l => {\n const sku = (l.sku || '').trim().toUpperCase();\n const slab = slabBySku[sku] || { rate: 18, cgst: 9, sgst: 9, igst: 18, isException: true };\n const lineTotal = Number(l.originalUnitPriceSet.shopMoney.amount) * l.quantity;\n const taxableValue = lineTotal / (1 + slab.rate/100);\n const taxAmount = lineTotal - taxableValue;\n return {\n sku, name: l.name, qty: l.quantity, lineTotal, taxableValue, taxAmount,\n gstRate: slab.rate, isException: slab.isException\n };\n });\n out.push({ json: { ...order, gstLines: linesWithGst } });\n}\nreturn out;"
},
"name": "Apply GST 2.0 slabs",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [680, 300]
}
The GST mapping sheet has columns sku, gst_pct, is_exception, last_reviewed. The accountant edits the sheet directly when the founder approves a new SKU's slab. is_exception=YES rows trigger a CC to the accountant in the morning email, so they can audit. Any unknown SKU defaults to 18% with an exception flag — better to over-charge GST and refund than under-charge and explain to the GSTN.
### Node 3 — Tally Sales voucher with multi-line GST split
{
"parameters": {
"method": "POST",
"url": "http://192.168.10.5:9000",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Content-Type", "value": "application/xml" }
]
},
"sendBody": true,
"specifyBody": "raw",
"rawContentType": "application/xml",
"body": "={{ buildSalesVoucherXml($json) }}"
},
"name": "Tally Sales voucher",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [1100, 300]
}
The voucher body is built by a helper expression that returns a Tally TDL XML envelope with one Sales voucher containing Sundry Debtors (debit, gross), Sales (credit, taxable), and CGST/SGST or IGST (credit, tax). State of supply determines intra-state (CGST+SGST) vs inter-state (IGST). The state lookup is from the Shopify customer's shipping address. Get this wrong and the GSTR-1 will reject the line item — we caught two errors during week-1 of running this and added an explicit state validation step.
- 1. Schedule Trigger (cron
45 23 * * *Asia/Kolkata) - 2. HTTP — Shopify Admin GraphQL orders (paginated)
- 3. Loop Over Items — walk pages until hasNextPage = false
- 4. Sheets read — GST slab mapping
- 5. Code — apply GST 2.0 slabs per line item
- 6. Split In Batches — process 25 orders at a time (rate-limit guard)
- 7. HTTP — Razorpay /payments/{id} for each linked transaction
- 8. HTTP — Razorpay /settlements/{id} for net credit calc
- 9. Code — compute MDR, GST on MDR, net bank credit, build voucher payloads
- 10. IF — branch refunds to refund-voucher sub-flow
- 11. HTTP — Tally Sales voucher (XML POST)
- 12. HTTP — Tally Receipt voucher (Razorpay payment)
- 13. HTTP — Tally Payment voucher (refund branch only)
- 14. XML — parse Tally response, check for LINEERROR
- 15. IF — success vs error branch
- 16. Sheets append — success log with synthetic reference key
- 17. Sheets append — exception log (with raw payload for debugging)
- 18. Wait — until 06:00 next morning, then send founder email
- 19. Error workflow trigger — DM ops on any node failure
- Shopify Admin API access token created with read_orders, read_customers, read_inventory scopes
- Shopify GraphQL API version pinned (e.g., 2025-07) — do not use unstable
- Razorpay key_id / key_secret stored as n8n credential
- Tally Server-9 HTTP listener confirmed running on port 9000 from the n8n host
- GST 2.0 slab sheet reviewed by accountant — every active SKU has a row, no UNKNOWN at go-live
- State-code mapping table verified (29 states + 8 UTs, GSTIN state codes 01-38)
- All Tally ledger names typed exactly (Sundry Debtors, Sales, CGST 9%, SGST 9%, IGST 18%, etc.)
- Refund voucher pattern tested with a sandbox Shopify refund
- Cron timezone set to Asia/Kolkata explicitly
- Workflow exported to git as JSON the day of go-live
- Founder briefed: morning email at 06:00 is canonical, evening has caveat
2025-07 in our case), monitor Shopify's deprecation announcements, plan an upgrade once a year.
Symptom: "Razorpay settlement_id is null on some payments." Cause: payment captured but not yet settled (T+2 default). Fix: do not require settlement_id for the same-day Receipt voucher. The bank-credit reconciliation runs separately on the day the settlement actually lands.
Symptom: "founder's morning email shows 'no orders today' on Diwali day." Cause: the cron fired before midnight IST, fetched yesterday's data only. Fix: use cron 45 23 not 0 0 — runs 15 min before midnight, captures the trailing day's orders.
## Mini case study — Diwali week 2025
The first install ran on 14 September 2025 (one week before GST 2.0 cutover). We ran shadow-only for 8 days, then went live on 22 September. By Diwali week (29 Oct - 5 Nov) the workflow was processing roughly 240 orders/day. Diwali peak day (1 Nov) hit 380 orders. Auto-match rate held at 99.4%. Two refunds correctly routed to the refund branch. Founder's morning email was 1 page, scannable in 30 seconds.
For a smaller-scale variant of this 4-way sync, see our Shopify + Tally + WhatsApp daily-close post from December 2025 — adds WhatsApp founder push for very small SMBs who do not check email.
## When NOT to build this
Skip this if (a) your monthly Shopify volume is under ~250 orders — the manual export-and-paste is genuinely faster and your accountant prefers the control, (b) you do not have Tally Server-9 (Tally Prime Cloud and Tally Prime alone do not expose the HTTP listener) — fall back to Excel-import path, or (c) your finance team strongly prefers monthly closes — daily closes generate alerts the team must triage; if nobody triages, the value evaporates. We turned down two clients in 2025 for reason (c).
For the build pattern when the brand operates outside India and GST is irrelevant, see the Radiant Finance case — same Shopify + Razorpay backbone, different tax module.
## FAQ
### How long to build and deploy this 4-way sync?
For us, 11 working days end-to-end: 2 days on Shopify GraphQL exploration + token scopes, 2 days on Razorpay settlement enrichment, 3 days on Tally TDL XML mapping (Tally errors are sparse), 2 days on the GST 2.0 slab sheet design and accountant onboarding, 2 days on shadow run with the accountant.
### How did GST 2.0 affect this build?
We had to rewrite the slab logic from a hardcoded constant to a Sheet lookup. Two SKUs moved from 12% to 5% (essentials), one moved from 28% to 18% (premium). The accountant approved the price-change comms to customers. We re-ran the prior month's data through both old and new mappings to flag discrepancies — found two, both manually approved.
### Can this run on n8n Cloud instead of self-hosted?
Yes for Shopify and Razorpay, no for Tally Server-9 (LAN listener, no public IP). We strongly recommend self-hosting n8n on the same LAN as Tally for security; the listener has no auth.
### What happens during a refund?
Branch at the IF node after enrichment. Refund creates a Tally Payment voucher (debit Sundry Debtors, credit Razorpay Bank for the refund amount, credit MDR Refund and Input GST as Razorpay typically returns the MDR portion of refunds within 14 days as a separate settlement entry). The pattern is well-documented in our internal runbook.
### Why GraphQL instead of Shopify REST?
GraphQL returns the full order tree (line items, transactions, refunds) in one call. REST needs 3-4 round trips. At 240 orders/day the latency saving is small but meaningful; at 1,000+ orders/day it is essential.
### How do I handle a Shopify API version deprecation?
Shopify announces deprecations 12+ months in advance. We pin the API version in the URL (2025-07) and review every quarter against the deprecation calendar. Bump and re-test in a sandbox before pushing to prod.
### Can the same flow handle Cashfree or PayU instead of Razorpay?
Yes, with mechanical changes. The Razorpay payment + settlement HTTP nodes become Cashfree's equivalent (POST to /pg/orders for fetch, /pg/settlements for the payout). MDR percentages differ — Cashfree at this client's tier was 1.95% vs Razorpay's 2%. The Tally voucher structure is identical.
Want this 4-way sync built for your store?
We ship the Shopify + Razorpay + Tally daily close — n8n on your Hetzner box, full GST 2.0 slab mapping, refund branch, founder morning email — in 11 working days for ₹85,000. Suitable for any Shopify D2C brand doing 100+ orders/day with Tally Server-9 on the LAN.
Book a 20-min Call
