A D2C home-fragrance brand on Shopify did ₹40 lakh in a normal month and 3x that during the Diwali run. They closed books once a month, and reconciling a festive month against Shopify payouts took their accountant four full days. We built an n8n flow that pushes every paid Shopify order into Tally as a sales voucher in near-real-time, and WhatsApps the owner a one-screen summary at 9pm daily. Books now close in minutes, every day. Here's the build — node JSON, the Tally XML, credentials, error handling, and cost.
The Answer in 60 Words
A Shopify webhook on orders/paid hits an n8n Shopify Trigger. n8n builds a Tally sales-voucher XML and POSTs it to TallyPrime's HTTP server on port 9000. A nightly Schedule Trigger at 9pm IST tallies the day's orders, payouts, and GST, and sends the owner a WhatsApp summary. The hot-path keeps Tally current; the EOD job is the daily close.
Why This Matters Now (August 2025)
Festive season is when monthly book-closing breaks. Order volume triples, COD and prepaid mix shifts, and the gap between Shopify's payout report and Tally's ledger turns into a multi-day reconciliation. Pushing each order into Tally as it's paid means the ledger is never more than a day behind. TallyPrime's XML integration over its built-in HTTP server (port 9000) is the same mechanism Tally has supported for years — stable, documented, no third-party connector needed. We tested this on TallyPrime 4.x with n8n v1.62 in August 2025.
This is the festive-D2C cousin of two flows we've shipped: our Shopify + Razorpay + Tally 4-way Diwali sync and our Shopify + Tally + WhatsApp daily sync. This one leans harder on the webhook hot-path and the owner summary.
What You'll Need (Prerequisites)
- n8n v1.6x with a public HTTPS URL for the Shopify webhook
- A Shopify store with admin API access (custom app) for the orders scope
- TallyPrime running with its HTTP server enabled on port 9000, reachable from n8n
- A static IP or tunnel to the machine running Tally (it's usually on-prem)
- A WhatsApp Business Cloud account with one approved utility template
- Your Tally company name and the exact sales ledger names, spelled identically
How Do You Set Up the Credentials?
Shopify: In Shopify admin, create a custom app with read_orders scope and grab the admin API access token. n8n's Shopify Trigger node uses an Access Token credential plus your shop subdomain.
Tally: No credential object — Tally's HTTP server has no auth. You secure it at the network layer (the tunnel above). In Tally: F1 → Settings → Connectivity → enable "TallyPrime acts as Server", port 9000.
WhatsApp Cloud API: Same as any WhatsApp Cloud build — access token (permanent, via a System User) and phone number ID from Meta's dashboard, into n8n's WhatsApp Business Cloud credential.
The Trigger Config: Shopify orders/paid Webhook
The Shopify Trigger registers the webhook automatically when the workflow activates. We listen for orders/paid, not orders/create — an unpaid or pending COD order shouldn't post a sales voucher yet.
{
"parameters": {
"topic": "orders/paid"
},
"id": "shopify-trigger-orders-paid",
"name": "Shopify — Order Paid",
"type": "n8n-nodes-base.shopifyTrigger",
"typeVersion": 1,
"position": [240, 300],
"credentials": {
"shopifyApi": { "id": "7", "name": "Shopify store" }
}
} How Do You Build a Tally Sales Voucher XML in n8n?
This is the heart of the flow. TallyPrime imports data via an ENVELOPE containing a HEADER and BODY; the voucher lives inside a TALLYMESSAGE tag, and the import report ID is "Vouchers" per the Tally developer reference. We build the XML in a Code node from the Shopify order, then POST it.
The Code node that assembles the voucher XML:
{
"parameters": {
"jsLanguage": "javaScript",
"jsCode": "const o = $json;\nconst date = new Date(o.created_at).toISOString().slice(0,10).replace(/-/g,'');\nconst amount = Number(o.total_price).toFixed(2);\nconst company = $env.TALLY_COMPANY;\nconst xml = '<ENVELOPE><HEADER><TALLYREQUEST>Import Data</TALLYREQUEST></HEADER><BODY><IMPORTDATA><REQUESTDESC><REPORTNAME>Vouchers</REPORTNAME><STATICVARIABLES><SVCURRENTCOMPANY>' + company + '</SVCURRENTCOMPANY></STATICVARIABLES></REQUESTDESC><REQUESTDATA><TALLYMESSAGE><VOUCHER VCHTYPE=\"Sales\" ACTION=\"Create\"><DATE>' + date + '</DATE><VOUCHERTYPENAME>Sales</VOUCHERTYPENAME><VOUCHERNUMBER>' + o.order_number + '</VOUCHERNUMBER><PARTYLEDGERNAME>Shopify Online Sales</PARTYLEDGERNAME><ALLLEDGERENTRIES.LIST><LEDGERNAME>Shopify Online Sales</LEDGERNAME><ISDEEMEDPOSITIVE>Yes</ISDEEMEDPOSITIVE><AMOUNT>-' + amount + '</AMOUNT></ALLLEDGERENTRIES.LIST><ALLLEDGERENTRIES.LIST><LEDGERNAME>Sales Accounts</LEDGERNAME><ISDEEMEDPOSITIVE>No</ISDEEMEDPOSITIVE><AMOUNT>' + amount + '</AMOUNT></ALLLEDGERENTRIES.LIST></VOUCHER></TALLYMESSAGE></REQUESTDATA></IMPORTDATA></BODY></ENVELOPE>';\nreturn [{ json: { xml: xml, order: o.order_number } }];"
},
"name": "Code — Build Tally Voucher XML",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [460, 300]
} Then the HTTP Request node POSTs that XML to Tally on port 9000 with content-type text/xml:
{
"parameters": {
"method": "POST",
"url": "http://{{ $env.TALLY_HOST }}:9000",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Content-Type", "value": "text/xml;charset=utf-8" }
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "text/xml",
"body": "={{ $json.xml }}"
},
"name": "Tally — Post Sales Voucher",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 8000,
"position": [680, 300]
} The EOD WhatsApp Owner Summary
The nightly Schedule Trigger at 9pm IST (0 21 *) pulls the day's paid orders from Shopify, computes gross sales, order count, average order value, and estimated GST, then sends the owner a WhatsApp template. The owner wanted one screen, no login.
{
"parameters": {
"resource": "message",
"operation": "send",
"phoneNumberId": "={{ $env.WA_PHONE_ID }}",
"recipientPhoneNumber": "={{ $env.OWNER_PHONE }}",
"messageType": "template",
"template": "daily_books_summary",
"templateLanguage": "en",
"components": {
"componentValues": [
{ "type": "body", "parameter": "={{ $json.summaryText }}" }
]
}
},
"name": "WhatsApp — Owner EOD Summary",
"type": "n8n-nodes-base.whatsApp",
"typeVersion": 1,
"position": [1120, 460],
"credentials": {
"whatsAppApi": { "id": "6", "name": "WhatsApp Cloud" }
}
}What Goes in the EOD Owner Summary?
The owner gave us one constraint: it has to fit on a phone screen without scrolling, and it has to mean something at a glance. We landed on six lines, computed in a Code node before the WhatsApp send.
The reconciliation line is the one the owner cares about most. It turns "are my books right?" from a month-end audit into a yes/no he reads before bed. Everything else on the summary is useful; that line is why he opens it.
How Do You Handle Errors in a Tally Sync?
Tally syncs fail in specific ways: the on-prem machine sleeps, a ledger name doesn't match, or Tally returns a 200 with an error inside the XML response.
1. Parse the Tally response, don't trust the HTTP 200. Tally returns <LINEERROR> inside a 200 body when a ledger name is wrong. We parse the response for <EXCEPTIONS>1</EXCEPTIONS> or LINEERROR and treat it as a failure even though HTTP says OK.
2. Dead-letter failed vouchers. Any order that fails to post goes to a "tally_retry" Google Sheet with the full XML. A 15-minute cron re-attempts. During a festive rush when the Tally machine occasionally hangs, this caught about 30 orders over Diwali week — all re-posted automatically.
3. Reconcile in the EOD job. The 9pm summary counts Shopify paid orders versus vouchers in Tally for the day. If they don't match, the owner's message says so in red, so a gap never hides until month-end.
What Does This Cost Per Month?
Shopify and Tally are already paid for. The tunnel runs on Tailscale's free tier for a single point-to-point link. The WhatsApp summary is one message a day at India's utility rate. The whole sync costs less than a single hour of the accountant's festive-season overtime it eliminates.
One caveat on the infra slice: a festive 3x order spike doesn't change the Hetzner cost, because the webhook hot-path is lightweight — building and POSTing one XML voucher is milliseconds of work. The same box that handles a quiet Tuesday handles Diwali night at 4,200 orders. You're not paying per transaction anywhere in this stack except the per-message WhatsApp summary, and that's one message regardless of volume. That flat cost curve is the real argument for self-hosting this rather than renting a per-order connector that bills you more exactly when you're busiest.
When NOT to Build This
Skip the webhook hot-path if you do under ~20 orders a day — a once-nightly batch import of the day's orders is simpler and the ledger being a few hours behind doesn't matter at that volume. Skip the whole thing if your accountant uses Zoho Books or another cloud accounting tool instead of Tally; their native Shopify integrations are good enough that a custom n8n sync is overkill. And don't attempt it if you can't put Tally behind a secure tunnel — exposing port 9000 to the internet is a genuine security hole, since it accepts writes with no authentication.
Real Example: The Festive D2C Brand
The home-fragrance D2C brand (₹40 lakh/month baseline, Mumbai, 3-person team) came to our automation team three weeks before Diwali, panicking about reconciliation. We shipped this in 5 working days. Over Diwali week they did ~₹1.1 cr across 4,200 orders; the accountant closed each day in under 15 minutes instead of saving it all for a 4-day month-end slog. The error-handling dead-letter caught ~30 orders that failed during brief Tally hangs and re-posted them with zero manual intervention. We brought the same data-integrity discipline from AppliedView, where reconciling two systems that disagree is the whole job. The pattern is the same one behind our Razorpay + Tally 22-minute daily close.
"Last Diwali, reconciliation took four days in January. This year my books were closed by the time I went to bed each night. The WhatsApp summary is the first thing I check."
For Indian SMBs weighing custom builds like this, our founder Vivek Singh writes about the buy-vs-build calculus from a builder's perspective.
Frequently Asked Questions
Why post to Tally on orders/paid instead of orders/create?
A COD order fires orders/create the moment it's placed but may never be paid — it can be returned to origin. Posting a sales voucher for it pollutes your ledger. orders/paid fires only when money is actually collected, so the voucher always reflects real revenue.
How does n8n talk to an on-prem TallyPrime?
TallyPrime runs an HTTP server on port 9000 on the local machine. n8n POSTs voucher XML to it. Because Tally is on a LAN and has no auth, you connect n8n to it through a secure tunnel like Tailscale or a VPN — never by exposing port 9000 to the internet.
Why does my voucher POST return 200 but nothing appears in Tally?
Tally returns HTTP 200 even on failure, with a LINEERROR inside the XML body — usually because a ledger name in your XML doesn't match Tally exactly. Parse the response for EXCEPTIONS or LINEERROR and copy ledger names character-for-character out of Tally.
How do you stop duplicate Shopify webhooks double-booking?
Set the Tally voucher number to the Shopify order number. If a duplicate webhook arrives, the second POST carries the same voucher number, and Tally rejects it as a duplicate rather than creating a second sales entry. The voucher is idempotent by design.
Can this handle GST and multiple tax rates?
Yes, by adding ledger entries for CGST/SGST/IGST in the voucher XML and splitting line items by HSN. For a single-rate catalogue it's one tax ledger pair. For mixed rates you compute the split in the Code node before building the XML. We scope that per client.
What if the Tally machine is switched off at night?
The hot-path dead-letters any failed voucher to a retry sheet and re-attempts every 15 minutes, so orders posted while Tally was down sync the moment it's back. The 9pm EOD job also flags any Shopify-vs-Tally count mismatch so nothing hides until month-end.
Want your Shopify books closing daily this festive season?
We build the Shopify → Tally → WhatsApp sync, set up the secure tunnel, and self-host it on your infra — for Indian D2C brands in 5–7 working days. Suitable if you do 20+ orders a day on Shopify and run Tally. No slides — just your store and our honest take.
Book a 20-min Call
Written by Hrishikesh Baidya, CTO at Softechinfra. Want this before Diwali? Email contact@softechinfra.com.
