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 }));
1
Import
Data
Vouchers
YourCompanyName
20251223
Shopify daily import - Karnataka intra-state - 2025-12-23
Sales
SHOP/2025-26/1842
Shopify Sales - KA
Shopify Sales - KA
Accounting Voucher View
Shopify Sales - KA
Yes
-49350.00
Sales @ 5%
No
47000.00
CGST 2.5%
No
1175.00
SGST 2.5%
No
1175.00
{
"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]
}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]
}{
"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]
}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.
4. WhatsApp template send fails after import succeeded: import is the load-bearing step; the DM is informational. Failed DMs queue for a 9 am IST retry.
## The credentials 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
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](/blog/n8n-whatsapp-cloud-gstr-3b-reminders-ca-firm-monthly-cron). 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](/services/web-development) for full-stack integrations and [Hrishikesh's CTO writeups](/team/rishikesh-baidya), see related links. For another live case study of accounting-stack automation we shipped, see [our work with Radiant Finance](/projects/radiant-finance).
## FAQ
### Why not use a SaaS connector like Cleartax or Zoho Books bridge?
Three reasons. Cost: ₹2,500-₹8,000/month vs ₹1,184. 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 ($45/month) 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 for ₹95,000. Suitable for any D2C store on Shopify + Tally Prime doing 5+ orders/day.
Book a 20-min Call
