Softechinfra
Productivity

n8n + Shopify + Razorpay + Tally: The 4-Way Sync That Lets a Diwali D2C Brand Close Books Daily

A 19-node n8n workflow that joins Shopify orders, Razorpay payments, refunds, and Tally vouchers — with GST 2.0 mapping after the 22 Sep cutover. Real node JSON, low monthly cost, festive-tested.

Hrishikesh BaidyaHrishikesh Baidya
October 13, 202514 min read
n8n + Shopify + Razorpay + Tally: The 4-Way Sync That Lets a Diwali D2C Brand Close Books Daily

A Pune D2C personal-care brand on Shopify (₹4.2 cr Q3 GMV) was closing books once a week, twice a week during festive. Their accountant manually exported Shopify orders, downloaded Razorpay settlements, hand-matched against bank credits, then keyed every voucher into Tally. The Diwali week 2024 close took 11 hours and missed two refunds that surfaced in the November GSTR-1. We built a 19-node n8n workflow that runs every night at 23:45 IST, joins all four systems, posts the day's vouchers into Tally, and emails the founder a 1-page reconciliation. Diwali week 2025 close: 18 minutes per day. Zero refunds missed.

19
n8n nodes (incl. error and refund branches)
18 min
Daily close vs 11 hours weekly grind
99.4%
Auto-match rate during Diwali week (1,720 orders)

TL;DR — what this 4-way sync actually does

At 23:45 IST every night, n8n pulls Shopify orders for the day via the Admin GraphQL API, pulls matching Razorpay payments + settlements, pulls bank statement entries (CSV uploaded daily by the accountant), and posts vouchers into Tally Server-9 over the LAN — one Receipt voucher per payment, one Sales voucher per Shopify order, one Payment voucher per refund, all tied by a synthetic reference key. GST 2.0 mapping is read from a Google Sheet so the founder can flag exceptions without a redeploy. The founder gets a 1-page email at 06:00 next morning.

Why this matters now — October 2025

Three triggers stacked. GST 2.0 went live on 22 September 2025 — the 12% and 28% slabs collapsed into 5% and 18%, with a 40% slab for sin goods. Personal-care products (the client's category) mostly moved from 18% to 5% on essentials and stayed at 18% on premium SKUs. Any reconciliation flow built before 22 September had hardcoded slab logic that broke. Shopify's GraphQL Admin API for orders is now the default (REST is being deprecated). And the Diwali festive window (29 October - 5 November 2025) was projected to triple this client's normal volume.

A recent r/shopify thread from a Surat textile D2C brand shows the same architecture decision — Shopify GraphQL → n8n → Razorpay enrichment → Tally — with the same GST 2.0 cutover pain in late September.

The 4-block architecture

SHOP
Block 1: Shopify orders pull
Cron at 23:45 IST. Shopify Admin GraphQL API for orders created in the past 24h. Pagination via cursor.
PAY
Block 2: Razorpay enrichment
For each Shopify order, fetch the linked Razorpay payment + settlement. Compute MDR, GST on MDR, net bank credit.
GST
Block 3: GST 2.0 slab lookup
Per line item, look up the GST slab from a live Google Sheet. Exceptions flagged for founder review.
TDL
Block 4: Tally voucher post
Three vouchers per order: Sales (with GST split), Receipt (Razorpay), Payment (refund if any). XML to Tally Server-9.

The 3 critical n8n nodes

Node 1 — Shopify orders via GraphQL (HTTP Request)

{
    "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.

Tally's HTTP listener has no auth. Anyone with network access to port 9000 on the Tally Server-9 box can post arbitrary vouchers. Keep n8n on the same LAN as Tally (do not expose 9000 to the internet via tunnel without a reverse proxy that requires auth). We saw exactly one Pune client's testing instance get scanned within 6 hours of being exposed publicly. Block port 9000 at the router; let only the n8n host's IP through.

The 19-node graph

  • 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

The cost in October 2025

The Hetzner box is dedicated (compliance with the client's investor agreement). Tally Server-9 license is the only paid software. Shopify Admin API is free at this volume; Razorpay API is free.

The pre-flight checklist (run before festive season)

  • 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

Common mistakes — symptoms first

Symptom: "Tally rejects half the Sales vouchers with LINEERROR after 22 September." Cause: GST ledger names hardcoded for the old slabs ("CGST 6%" no longer exists for the SKUs that moved to 5%). Fix: rename ledgers in Tally to match the new slabs, or add a per-SKU ledger override in the voucher builder. We chose the second path so the GST sheet is the only source of truth.

Symptom: "interstate orders booked as intrastate (CGST+SGST instead of IGST)." Cause: state-code lookup defaults to the company's state when shipping address is missing. Fix: explicit IF check — if shipping state is missing, route to exception sheet. Do not assume.

Symptom: "Shopify GraphQL returns less data than expected." Cause: API version was changed by Shopify; deprecated fields silently dropped. Fix: pin the version (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. Suitable for any Shopify D2C brand doing 100+ orders/day with Tally Server-9 on the LAN.

Book a 20-min Call

For the cost-of-doing-nothing perspective during festive, see our Q4 survival kit case from earlier this week. For the broader engineering services context, see our AI automation team page. As Hrishikesh, our CTO, points out, a daily close beats a weekly close not because daily is better, but because the next-day fix is cheaper than the next-week fix.

Tags:
n8nShopifyRazorpayTallyGSTDiwaliD2CReconciliation
Share this post:
Hrishikesh Baidya

Hrishikesh Baidya

CTO at Softechinfra specializing in Python, system architecture, and building secure, scalable software solutions.