A Bengaluru-headquartered D2C apparel brand was closing books on the 12th of the next month. Their accountant pulled Shopify CSVs, manually split GST, keyed entries into Tally Prime, and reconciled bank-of-record matches over five days. We replaced that with a 24-node n8n workflow that runs nightly at 23:30 IST, pulls the day's Shopify orders, generates per-state Tally XML vouchers with correct CGST/SGST/IGST split, imports them via Tally's HTTP listener, and sends a 1-line WhatsApp summary to the founder. Books now close daily. Month-end is a 90-minute review, not a 5-day project.
TL;DR — what this 4-way sync actually does
At 23:30 IST every day, the workflow uses Shopify's GraphQL Admin API to pull all paid orders from the last 24 hours. A Code node groups orders by buyer state (the GST split logic — same state as seller = CGST + SGST, different state = IGST). For each state group, a separate Code node generates Tally TDL XML for a sales voucher. The XML POSTs to the client's Tally Prime instance running an HTTP listener on port 9000. A reconciliation step matches imported voucher count to source order count. Result: WhatsApp DM to the founder — "23 vouchers imported, ₹4.18 L gross, GST ₹76,438, reconciliation OK."
Why this matters now — December 2025
Three things converged. Shopify's GraphQL Admin API became the only supported API for new public apps from April 1, 2025, so the cleaner data shape is now the only path. Tally Prime added a stabilised HTTP-based XML import listener in mid-2025 (older versions needed ODBC or file-drop approaches). And the GST 2.0 changes from April 2025 made on-the-day reconciliation valuable — IMS hard-blocks ITC mismatches, so reconciling against your sales daily catches issues before they become a month-end fire.
A r/IndiaInvestments thread on Tally automation from October 2025 is the clearest community pulse — most teams use SaaS connectors (Cleartax, Zoho Books bridges). n8n self-hosted does the same job at a fraction of their cost.
The 5-block workflow
The GST split logic (the part that breaks most automations)
Every Indian D2C founder I have spoken to in 2025 has one specific frustration: third-party Shopify-Tally connectors get the GST split wrong on edge cases. Here is the logic in clean JavaScript that handles the common ones:
const orders = $input.first().json.data.orders.edges.map(e => e.node);
const sellerStateCode = '29'; // Karnataka in this case (Bengaluru seller)
// Group by buyer state
const byState = orders.reduce((acc, order) => {
const shipState = order.shippingAddress?.provinceCode || 'unknown';
acc[shipState] = acc[shipState] || [];
acc[shipState].push(order);
return acc;
}, {});
const vouchers = Object.entries(byState).map(([stateCode, stateOrders]) => {
const isIntraState = stateCode === sellerStateCode;
// Compute totals per HSN code (different GST rates per HSN)
const hsnGroups = {};
stateOrders.forEach(order => {
order.lineItems.edges.forEach(li => {
const item = li.node;
const hsn = item.product?.metafield?.value || '6109'; // T-shirt default
const gstRate = getGSTRate(hsn); // 5, 12, 18, or 28
const taxableValue = Number(item.discountedTotalSet.shopMoney.amount);
hsnGroups[hsn] = hsnGroups[hsn] || { taxableValue: 0, gstRate, qty: 0 };
hsnGroups[hsn].taxableValue += taxableValue;
hsnGroups[hsn].qty += item.quantity;
});
});
// Compute tax components
const lines = Object.entries(hsnGroups).map(([hsn, g]) => {
const totalTax = g.taxableValue * (g.gstRate / 100);
return {
hsn,
taxableValue: Math.round(g.taxableValue * 100) / 100,
cgst: isIntraState ? Math.round(totalTax / 2 * 100) / 100 : 0,
sgst: isIntraState ? Math.round(totalTax / 2 * 100) / 100 : 0,
igst: isIntraState ? 0 : Math.round(totalTax * 100) / 100,
gstRate: g.gstRate,
qty: g.qty
};
});
return {
stateCode,
stateName: stateCodeToName(stateCode),
isIntraState,
lines,
totalTaxable: lines.reduce((s, l) => s + l.taxableValue, 0),
totalCgst: lines.reduce((s, l) => s + l.cgst, 0),
totalSgst: lines.reduce((s, l) => s + l.sgst, 0),
totalIgst: lines.reduce((s, l) => s + l.igst, 0)
};
});
return vouchers.map(v => ({ json: v }));Three things to notice. We group by HSN code first (each HSN has its own GST rate — 5%, 12%, 18%, 28%), then by state second. We round to 2 decimal places to prevent Tally from rejecting fractional-paise lines. We default missing HSN codes to a known-safe HSN per category, and log unknowns to a separate Sheet for cleanup.
The actual Tally TDL XML voucher (sample)
This is the exact XML that goes to Tally's HTTP listener for a sales voucher. We generate this string per state group in a Code node, then POST.
<ENVELOPE>
<HEADER>
<VERSION>1</VERSION>
<TALLYREQUEST>Import</TALLYREQUEST>
<TYPE>Data</TYPE>
<ID>Vouchers</ID>
</HEADER>
<BODY>
<DESC>
<STATICVARIABLES>
<SVCURRENTCOMPANY>YourCompanyName</SVCURRENTCOMPANY>
</STATICVARIABLES>
</DESC>
<DATA>
<TALLYMESSAGE>
<VOUCHER VCHTYPE="Sales" ACTION="Create">
<DATE>20251223</DATE>
<NARRATION>Shopify daily import - Karnataka intra-state - 2025-12-23</NARRATION>
<VOUCHERTYPENAME>Sales</VOUCHERTYPENAME>
<VOUCHERNUMBER>SHOP/2025-26/1842</VOUCHERNUMBER>
<PARTYLEDGERNAME>Shopify Sales - KA</PARTYLEDGERNAME>
<BASICBASEPARTYNAME>Shopify Sales - KA</BASICBASEPARTYNAME>
<PERSISTEDVIEW>Accounting Voucher View</PERSISTEDVIEW>
<ALLLEDGERENTRIES.LIST>
<LEDGERNAME>Shopify Sales - KA</LEDGERNAME>
<ISDEEMEDPOSITIVE>Yes</ISDEEMEDPOSITIVE>
<AMOUNT>-49350.00</AMOUNT>
</ALLLEDGERENTRIES.LIST>
<ALLLEDGERENTRIES.LIST>
<LEDGERNAME>Sales @ 5%</LEDGERNAME>
<ISDEEMEDPOSITIVE>No</ISDEEMEDPOSITIVE>
<AMOUNT>47000.00</AMOUNT>
</ALLLEDGERENTRIES.LIST>
<ALLLEDGERENTRIES.LIST>
<LEDGERNAME>CGST 2.5%</LEDGERNAME>
<ISDEEMEDPOSITIVE>No</ISDEEMEDPOSITIVE>
<AMOUNT>1175.00</AMOUNT>
</ALLLEDGERENTRIES.LIST>
<ALLLEDGERENTRIES.LIST>
<LEDGERNAME>SGST 2.5%</LEDGERNAME>
<ISDEEMEDPOSITIVE>No</ISDEEMEDPOSITIVE>
<AMOUNT>1175.00</AMOUNT>
</ALLLEDGERENTRIES.LIST>
</VOUCHER>
</TALLYMESSAGE>
</DATA>
</BODY>
</ENVELOPE>Notice the negative amount on the party ledger line — Tally's convention is that the receivable side is negative when the customer is the source of the credit (a Sales voucher debits the customer). Get this sign wrong and Tally accepts the voucher but the trial balance is off. The reconciliation step catches this — that is exactly why we built it.
For inter-state orders, swap CGST + SGST for IGST 5% at the full rate, and change the party ledger to "Shopify Sales - IGST".
The actual n8n node JSON (3 critical nodes)
Node 1 — Shopify GraphQL pull
{
"parameters": {
"method": "POST",
"url": "https://your-store.myshopify.com/admin/api/2025-01/graphql.json",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "X-Shopify-Access-Token", "value": "{{ $env.SHOPIFY_ADMIN_TOKEN }}" },
{ "name": "Content-Type", "value": "application/json" }
]
},
"sendBody": true,
"bodyContentType": "json",
"jsonBody": "={\n \"query\": \"query { orders(first: 100, query: \\\"financial_status:paid created_at:>{{ $json.lastRunIso }}\\\") { edges { node { id name createdAt totalPriceSet { shopMoney { amount } } shippingAddress { provinceCode } lineItems(first: 20) { edges { node { quantity discountedTotalSet { shopMoney { amount } } product { id title metafield(namespace: \\\"tax\\\", key: \\\"hsn\\\") { value } } } } } } } } }\"\n}",
"options": { "timeout": 30000 }
},
"name": "Pull paid Shopify orders",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [460, 300]
} We use API version 2025-01 (the latest stable as of December 2025). HSN codes live as a product metafield in the tax.hsn namespace — set this once per product when uploading; missing HSN defaults safely.
Node 2 — Tally HTTP listener POST
{
"parameters": {
"method": "POST",
"url": "http://{{ $env.TALLY_HOST }}:9000",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Content-Type", "value": "text/xml; charset=utf-8" }
]
},
"sendBody": true,
"bodyContentType": "raw",
"rawContentType": "text/xml",
"body": "={{ $json.tallyXml }}",
"options": { "timeout": 30000 }
},
"name": "POST to Tally",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [900, 300]
}The Tally listener runs on the client's Windows machine (Tally Prime + ngrok or a Tailscale tunnel for the n8n box to reach it). Port 9000 is Tally's default ODBC/HTTP port. Critical: the n8n instance must be in the same VPN as the Tally host, or you expose Tally's port to the internet (do not).
Node 3 — Reconciliation + WhatsApp DM
{
"parameters": {
"method": "POST",
"url": "https://graph.facebook.com/v21.0/{{ $env.WA_PHONE_ID }}/messages",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Authorization", "value": "Bearer {{ $env.WA_TOKEN }}" },
{ "name": "Content-Type", "value": "application/json" }
]
},
"sendBody": true,
"bodyContentType": "json",
"jsonBody": "={\n \"messaging_product\": \"whatsapp\",\n \"to\": \"{{ $env.FOUNDER_PHONE }}\",\n \"type\": \"template\",\n \"template\": {\n \"name\": \"daily_books_summary\",\n \"language\": { \"code\": \"en\" },\n \"components\": [\n {\n \"type\": \"body\",\n \"parameters\": [\n { \"type\": \"text\", \"text\": \"{{ $json.summary.date }}\" },\n { \"type\": \"text\", \"text\": \"{{ $json.summary.voucherCount }}\" },\n { \"type\": \"text\", \"text\": \"{{ $json.summary.gross }}\" },\n { \"type\": \"text\", \"text\": \"{{ $json.summary.totalGst }}\" },\n { \"type\": \"text\", \"text\": \"{{ $json.summary.reconStatus }}\" }\n ]\n }\n ]\n }\n}",
"options": { "timeout": 15000 }
},
"name": "WhatsApp founder DM",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [1340, 300]
}The screen recording — one frame at a time
We screen-recorded this workflow running on December 22, 2025 at 23:30 IST. Frame-by-frame:
Total wall-clock: 16 seconds.
The failure modes and where they hit
The 4 modes we hit and how we route them:
first: 100 and use cursor pagination if a day has more orders. Shopify's GraphQL leaky-bucket rate limiter is generous (1,000 cost points refilling at 50/sec); we have not hit it on the 23-100 orders/day clients.The credentials checklist
The cost — one D2C client, daily run for a month
Compare to the alternative: an accountant spending 2 hours/day on it. The workflow pays back in under 3 days.
The pre-flight checklist
- Tally HTTP listener confirmed accessible from n8n via curl test
- All required ledgers exist in Tally (run a 1-row test voucher import first)
- Shopify admin token has read_orders, read_products, read_locations scopes
- HSN metafields populated on every product (run a "missing HSN" Shopify report)
- GST split logic tested with a synthetic order from each of 28 states
- Reconciliation match logic tested with a deliberate mismatch (drop one line)
- WhatsApp template approved in Utility category
- Tailscale tunnel between n8n box and Windows Tally box verified stable for 7 days
- Error workflow points at ops Slack channel with full XML payload on failure
- Backup: a daily Shopify CSV export to S3 — recovery path if Tally was unreachable for 24+ hrs
Common mistakes — symptoms first, fixes second
Symptom: "Tally rejects voucher with 'Could not set the value' error." Cause: a referenced ledger does not exist. Fix: pre-create all GST + sales ledgers in Tally before the first run. We provide a Tally TDL file that creates them all in one shot.
Symptom: "Voucher imports but Tally trial balance is off." Cause: party ledger amount sign is wrong. Sales voucher = customer debited (positive amount on customer line if you do not use the negative convention). Fix: read Tally's official voucher format docs and align with the convention you started with. We use ISDEEMEDPOSITIVE = Yes with negative amounts on receivables.
Symptom: "Shopify orders appear duplicated in Tally." Cause: previous workflow run did not complete cleanly, but n8n re-pulled the same orders next run. Fix: store last-successful-run timestamp in a Postgres watermark table. Pull only orders created_at > last_run.
Symptom: "Tally rejects with 'company not loaded'." Cause: Tally Prime closed or paused on the Windows machine. Fix: client must keep Tally Prime open with the right company loaded. We add a daily "is Tally responsive?" healthcheck that fires at 22:00 IST as a heads-up.
Symptom: "WhatsApp template fails with 132012 — error in template parameters." Cause: a parameter is null or contains a newline. Fix: sanitise all template parameters in a Code node — strip newlines, replace null with "n/a", truncate to 60 chars.
Symptom: "GST split is wrong for IGST orders to UT (Union Territory)." Cause: UT GST is UTGST not SGST in same-UT cases (intra-UT sales). Fix: add a state code lookup that flags UT codes (DL, CH, AN, etc) and emits UTGST instead of SGST when seller is also in that UT. Most D2C clients ship out of metros so this is rare but worth handling.
Mini case study — Bengaluru D2C apparel, 5 months live
The first install ran from August 2, 2025. As of today (December 23, 2025), 138 nightly runs — 134 fully automated, 4 with manual intervention (3 Tally crashes after Windows updates, 1 power outage at the client's office). Monthly close compression: previously 5 days into the next month, now happens by the 2nd. The CFO's exact email after October close: "we filed GSTR-3B by the 14th this month and it took us under 3 hours total. We were the third in our cohort to file." Useable.
For a related accounting-flow we built, see our GSTR-3B reminders for CA firms. The Tally piece is the same listener pattern, different upstream trigger.
When not to build this
Skip this if (a) your daily order volume is under 5 — the engineering effort does not amortise against 30 minutes of manual entry, (b) you do not own a self-hosted n8n + a stable Windows machine that runs Tally 24/7 — Tally on a part-time-on laptop is a recipe for missed runs, or (c) your SKU catalogue lacks HSN codes and you have no plan to add them — the GST split fundamentally needs HSN per SKU. We turned down a client in November for reason (c) — they had 2,400 SKUs with no HSN and no appetite to backfill. The connector would have produced wrong GST splits from day one.
For more context on the Softechinfra services for full-stack integrations and Hrishikesh's CTO writeups, see related links. For another live case study of accounting-stack automation we shipped, see our work with Radiant Finance.
FAQ
Why not use a SaaS connector like Cleartax or Zoho Books bridge?
Three reasons. Cost: SaaS connectors cost several times more per month. Customisation: SaaS connectors hide the GST split logic — when an edge case breaks, you cannot debug or fix without their support team. Data ownership: SaaS routes your sales data through their servers; n8n self-hosted keeps it on your infra.
Can this work with BUSY or Marg ERP instead of Tally?
BUSY has an XML import similar to Tally; the voucher schema is different but the n8n flow shape is identical. Marg ERP has a less mature integration path — we have not built it, but it is feasible. Budget 3-5 extra days for the schema mapping.
What about returns and credit notes?
Same workflow with a different trigger (Shopify refunds/create webhook) and a different Tally voucher type (CREDIT NOTE). We ship the credit-note variant as part of the same engagement. Returns net out the original sales in the same period if filed before GSTR-1; otherwise, they go to the next period.
How does the workflow handle COD orders?
COD orders show as financialStatus: pending in Shopify until delivered. Our query filters to financial_status:paid, so COD orders are excluded until the payment is captured (usually on delivery). The accountant reviews COD-pending separately monthly.
Do I need internet on the Tally machine?
Yes — Tally needs to be reachable by n8n via Tailscale or VPN. If your office internet is unreliable, run Tally on a small DigitalOcean Windows droplet with Tally Cloud licence. We have done this for one client; works flawlessly.
What about multi-company or multi-GSTIN setups?
The flow takes a company name as a workflow parameter. For multi-company, run multiple workflow copies, one per company. We have a client with 3 GSTINs (one per state of operation) running 3 parallel n8n workflows on the same Tally instance.
Is the XML format the same as Tally Prime 4.0 vs 5.0?
Yes, the core voucher XML format is stable across Tally Prime 3.0 → 5.0. Tally has been careful with backward compatibility. We test against both 4.x and 5.x in our QA; no issues to date.
Want this 4-way sync set up for your store?
We ship the Shopify + Tally + WhatsApp daily-close flow — n8n on your infra, Tally HTTP listener setup, GST split per HSN per state, reconciliation, founder DM — in 7 working days. Suitable for any D2C store on Shopify + Tally Prime doing 5+ orders/day.
Book a 20-min CallFor the founder-perspective on accounting workflow ownership in Indian SMBs, see Vivek Kumar's blog.
