Our D2C client in Indore was reconciling ~1,400 Shopify orders into Tally manually every month. Their accountant called it "the second job." We rebuilt it on n8n: Shopify webhook → CGST/SGST/IGST split based on the buyer's shipping state → Tally-importable XML → Drive upload → daily Slack digest of the day's vouchers. 20 nodes, 14 hours of manual entry a month vanished. The whole thing has been live for 14 weeks; not a single GSTR-1 mismatch in that window.
TL;DR
A Shopify orders/create webhook fires when an order is placed (paid status). n8n picks up the order JSON, runs a state-code check against the buyer's shipping address vs the seller's registered state, splits the tax into CGST+SGST (intra-state) or IGST (inter-state), generates a Tally-compatible XML voucher with the right line items and ledgers, and uploads it to a dated Drive folder. The accountant clicks Import once a day in Tally. End-to-end runtime per order: ~4 seconds.
Why this matters now — April 2026
Shopify shipped GraphQL Admin API 2026-04 with cleaner tax-line objects (CGST/SGST/IGST are now reported as separate taxLines entries with proper rate metadata, not embedded in a single tax amount). The orderCreate idempotency-key requirement (from 2026-04) made retry-safe pipelines viable.
On the Tally side, TallyPrime 6.1 (Feb 2026) cleaned up the XML schema for GST classification on ledgers, so Sales vouchers with the right output-tax ledgers flow through without manual repair. The combination — Shopify clean tax data + Tally clean ingestion — is what made this workflow deployable in 2026 vs janky in 2024.
A long r/IndianAccounting thread from March 2026 on Shopify+Tally sync had 40+ comments and zero good answers — most CAs were quoting steep fees for custom integrations. We built it for our client at a fraction of that and open-sourced the workflow structure below.
The 5-block workflow
Prerequisites
- n8n v1.121+ (the Shopify Trigger node v2.2 ships order GraphQL by default)
- Shopify store on Basic or above (webhooks free)
- Shopify Admin API access token with read_orders and read_customers scopes
- Your seller GSTIN and registered-business state code (e.g. Maharashtra = 27)
- TallyPrime 6.1+ with company configured for GST
- Pre-created Tally output tax ledgers: Output CGST @ 9%, Output SGST @ 9%, Output IGST @ 18%, etc., for each rate slab you sell
- Google Drive folder structure ready: /Tally Imports/Sales/YYYY-MM/
Step 1 — Shopify webhook + signature verify
Set the webhook in Shopify Admin → Notifications → Webhooks. Topic: Order payment (fires after the order is paid, not just created — avoids the "abandoned cart" trap). Format: JSON. URL: https://n8n.softechinfra.com/webhook/shopify-order.
Shopify signs with HMAC-SHA256 over the raw body using your shared secret in header X-Shopify-Hmac-Sha256 (base64). Verify before doing anything else.
const crypto = require('crypto');
const secret = $env.SHOPIFY_WEBHOOK_SECRET;
const signature = $json.headers['x-shopify-hmac-sha256'];
const body = $json.body; // raw string
const computed = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('base64');
if (computed !== signature) {
throw new Error('Invalid Shopify signature');
}
const order = JSON.parse(body);
return { json: order };Webhook node config:
{
"parameters": {
"httpMethod": "POST",
"path": "shopify-order",
"responseMode": "responseNode",
"options": {
"rawBody": true
}
},
"id": "shopify-webhook-001",
"name": "Shopify Order Paid",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [240, 300]
} Idempotency: Shopify retries failed webhooks for ~48 hours. The order's id is the natural idempotency key. We push it to a small Redis-backed Set node (or to a Google Sheets-based dedupe if you do not have Redis) and skip if seen.
Step 2 — The GST split logic
This is the only block that requires real accounting attention. The rule under India's GST framework:
Shopify's 2026-04 taxLines array gives you the rate and title (e.g. { title: "CGST", rate: 0.09, price: "45.00" }) — but Shopify's tax engine isn't always correctly configured. We never trust Shopify's split. We re-derive from the buyer's shipping address.
The Code node:
const SELLER_STATE_CODE = '27'; // Maharashtra
const SELLER_GSTIN = '27ABCDE1234F1Z5';
const STATE_BY_NAME = {
'maharashtra': '27', 'karnataka': '29', 'tamil nadu': '33', 'delhi': '07',
'haryana': '06', 'uttar pradesh': '09', 'gujarat': '24', 'rajasthan': '08',
'west bengal': '19', 'telangana': '36', 'andhra pradesh': '37',
'kerala': '32', 'madhya pradesh': '23', 'punjab': '03', 'bihar': '10',
'odisha': '21', 'chhattisgarh': '22', 'jharkhand': '20', 'assam': '18',
// ... fill out for all states
};
const order = $json;
const shipping = order.shipping_address || order.billing_address;
const stateName = (shipping.province || '').toLowerCase().trim();
const stateCode = STATE_BY_NAME[stateName];
const country = (shipping.country_code || 'IN').toUpperCase();
let transactionType;
if (country !== 'IN') {
transactionType = 'export';
} else if (!stateCode) {
transactionType = 'unknown_state'; // route to manual review
} else if (stateCode === SELLER_STATE_CODE) {
transactionType = 'intra';
} else {
transactionType = 'inter';
}
// Aggregate tax across line items
let totalTaxable = 0;
let totalTax = 0;
let gstRate = null;
for (const li of order.line_items) {
totalTaxable += parseFloat(li.price) * li.quantity;
for (const tl of li.tax_lines || []) {
totalTax += parseFloat(tl.price);
if (!gstRate) gstRate = Math.round(tl.rate * 100);
}
}
// Detect mixed rates (e.g. some products 5%, some 18%)
const rates = new Set();
for (const li of order.line_items) {
for (const tl of li.tax_lines || []) rates.add(Math.round(tl.rate * 100 * 2)); // doubled because intra-state is per-half
}
const mixedRates = rates.size > 1;
let cgst = 0, sgst = 0, igst = 0;
if (transactionType === 'intra') {
cgst = totalTax / 2;
sgst = totalTax / 2;
} else if (transactionType === 'inter') {
igst = totalTax;
}
return {
json: {
...order,
gst: {
transaction_type: transactionType,
buyer_state: shipping.province,
buyer_state_code: stateCode,
seller_state_code: SELLER_STATE_CODE,
taxable_amount: totalTaxable,
cgst, sgst, igst,
total_tax: totalTax,
gst_rate_pct: gstRate,
mixed_rates: mixedRates,
requires_review: mixedRates || transactionType === 'unknown_state'
}
}
}; Cases where requires_review = true route to a Slack channel for the accountant to validate, not to auto-import. About 2-3% of orders flag — usually mixed-rate carts or buyers with international shipping addresses.
Step 3 — Tally Sales Voucher XML
Tally Sales voucher XML is more complex than the Purchase XML from Post 4 because it must include the customer party ledger with their state code and (if registered) GSTIN, plus the inventory-allocation block per line item.
A minimal Sales voucher for an intra-state order:
<ENVELOPE>
<HEADER><TALLYREQUEST>Import Data</TALLYREQUEST></HEADER>
<BODY><IMPORTDATA>
<REQUESTDESC>
<REPORTNAME>All Masters</REPORTNAME>
<STATICVARIABLES><SVCURRENTCOMPANY>Indore D2C Pvt Ltd</SVCURRENTCOMPANY></STATICVARIABLES>
</REQUESTDESC>
<REQUESTDATA><TALLYMESSAGE xmlns:UDF="TallyUDF">
<VOUCHER VCHTYPE="Sales" ACTION="Create">
<DATE>20260427</DATE>
<VOUCHERNUMBER>#2891</VOUCHERNUMBER>
<REFERENCE>SHOPIFY-2891</REFERENCE>
<NARRATION>Shopify order #2891 — Riya Sharma, Maharashtra</NARRATION>
<VOUCHERTYPENAME>Sales</VOUCHERTYPENAME>
<PARTYLEDGERNAME>Riya Sharma - 27</PARTYLEDGERNAME>
<BASICBUYERNAME>Riya Sharma</BASICBUYERNAME>
<STATENAME>Maharashtra</STATENAME>
<COUNTRYNAME>India</COUNTRYNAME>
<ALLLEDGERENTRIES.LIST>
<LEDGERNAME>Riya Sharma - 27</LEDGERNAME>
<ISDEEMEDPOSITIVE>Yes</ISDEEMEDPOSITIVE>
<AMOUNT>-1180.00</AMOUNT>
</ALLLEDGERENTRIES.LIST>
<ALLLEDGERENTRIES.LIST>
<LEDGERNAME>Sales — Online</LEDGERNAME>
<ISDEEMEDPOSITIVE>No</ISDEEMEDPOSITIVE>
<AMOUNT>1000.00</AMOUNT>
</ALLLEDGERENTRIES.LIST>
<ALLLEDGERENTRIES.LIST>
<LEDGERNAME>Output CGST @ 9%</LEDGERNAME>
<ISDEEMEDPOSITIVE>No</ISDEEMEDPOSITIVE>
<AMOUNT>90.00</AMOUNT>
</ALLLEDGERENTRIES.LIST>
<ALLLEDGERENTRIES.LIST>
<LEDGERNAME>Output SGST @ 9%</LEDGERNAME>
<ISDEEMEDPOSITIVE>No</ISDEEMEDPOSITIVE>
<AMOUNT>90.00</AMOUNT>
</ALLLEDGERENTRIES.LIST>
</VOUCHER>
</TALLYMESSAGE></REQUESTDATA>
</IMPORTDATA></BODY>
</ENVELOPE>Things to note:
{name} - {state_code} so two buyers named "Rahul Kumar" from different states do not collide. Tally needs unique ledger names; this is a convention that works for B2C.ISDEEMEDPOSITIVE is "Yes" for the party (debit — money is receivable) and "No" for sales + output tax (credit). The amounts are negative on debit and positive on credit.Output IGST @ 18% line at the full tax amount.PARTYGSTIN and STATENAME so Tally captures it for GSTR-1.The Code node that emits the XML:
const o = $json;
const g = o.gst;
const dateTally = o.processed_at.split('T')[0].split('-').join(''); // YYYYMMDD
const partyName = (o.customer?.first_name + ' ' + (o.customer?.last_name || '')).trim();
const partyLedger = partyName + ' - ' + g.buyer_state_code;
let taxXml = '';
if (g.transaction_type === 'intra') {
taxXml = `
<ALLLEDGERENTRIES.LIST>
<LEDGERNAME>Output CGST @ ${g.gst_rate_pct/2}%</LEDGERNAME>
<ISDEEMEDPOSITIVE>No</ISDEEMEDPOSITIVE>
<AMOUNT>${g.cgst.toFixed(2)}</AMOUNT>
</ALLLEDGERENTRIES.LIST>
<ALLLEDGERENTRIES.LIST>
<LEDGERNAME>Output SGST @ ${g.gst_rate_pct/2}%</LEDGERNAME>
<ISDEEMEDPOSITIVE>No</ISDEEMEDPOSITIVE>
<AMOUNT>${g.sgst.toFixed(2)}</AMOUNT>
</ALLLEDGERENTRIES.LIST>`;
} else if (g.transaction_type === 'inter') {
taxXml = `
<ALLLEDGERENTRIES.LIST>
<LEDGERNAME>Output IGST @ ${g.gst_rate_pct}%</LEDGERNAME>
<ISDEEMEDPOSITIVE>No</ISDEEMEDPOSITIVE>
<AMOUNT>${g.igst.toFixed(2)}</AMOUNT>
</ALLLEDGERENTRIES.LIST>`;
}
const total = (parseFloat(o.total_price)).toFixed(2);
const xml = `<ENVELOPE>
<HEADER><TALLYREQUEST>Import Data</TALLYREQUEST></HEADER>
<BODY><IMPORTDATA>
<REQUESTDESC>
<REPORTNAME>All Masters</REPORTNAME>
<STATICVARIABLES><SVCURRENTCOMPANY>${$env.TALLY_COMPANY_NAME}</SVCURRENTCOMPANY></STATICVARIABLES>
</REQUESTDESC>
<REQUESTDATA><TALLYMESSAGE xmlns:UDF="TallyUDF">
<VOUCHER VCHTYPE="Sales" ACTION="Create">
<DATE>${dateTally}</DATE>
<VOUCHERNUMBER>#${o.order_number}</VOUCHERNUMBER>
<REFERENCE>SHOPIFY-${o.order_number}</REFERENCE>
<NARRATION>Shopify order #${o.order_number} — ${partyName}, ${g.buyer_state}</NARRATION>
<VOUCHERTYPENAME>Sales</VOUCHERTYPENAME>
<PARTYLEDGERNAME>${partyLedger}</PARTYLEDGERNAME>
<BASICBUYERNAME>${partyName}</BASICBUYERNAME>
<STATENAME>${g.buyer_state}</STATENAME>
<COUNTRYNAME>India</COUNTRYNAME>
<ALLLEDGERENTRIES.LIST>
<LEDGERNAME>${partyLedger}</LEDGERNAME>
<ISDEEMEDPOSITIVE>Yes</ISDEEMEDPOSITIVE>
<AMOUNT>-${total}</AMOUNT>
</ALLLEDGERENTRIES.LIST>
<ALLLEDGERENTRIES.LIST>
<LEDGERNAME>Sales — Online</LEDGERNAME>
<ISDEEMEDPOSITIVE>No</ISDEEMEDPOSITIVE>
<AMOUNT>${g.taxable_amount.toFixed(2)}</AMOUNT>
</ALLLEDGERENTRIES.LIST>${taxXml}
</VOUCHER>
</TALLYMESSAGE></REQUESTDATA>
</IMPORTDATA></BODY>
</ENVELOPE>`;
return {
json: { ...o, tally_xml_filename: 'order-' + o.order_number + '.xml' },
binary: { data: { data: Buffer.from(xml).toString('base64'), mimeType: 'application/xml', fileName: 'order-' + o.order_number + '.xml' } }
};Step 4 — Drive upload + daily digest
The Google Drive node uploads to /Tally Imports/Sales/2026-04/, naming files order-{order_number}.xml for traceability.
A separate sister workflow runs every weekday at 7 PM IST (Cron) — it lists files added to that day's Drive folder, totals the order values, and posts a Slack digest to #accounts:
:moneybag: *Sales digest — 27 Apr*
38 orders ready to import. Total: ₹4,18,640.
Intra-state: 22 orders (₹2,38,210) · Inter-state: 14 orders (₹1,68,930) · Export: 2 orders (₹11,500)
Mixed-rate / review needed: 1 — <link to order>
:google-drive: <Folder link>The accountant imports the day's XMLs in one go via Tally's Bulk Import (or via TDL-driven scripted import on Tally Connect for fully automated end-to-end).
Cost comparison
For 1,400 orders/month (our Indore client's typical volume):
When NOT to do this
You sell across multiple Shopify markets. If you have a US, UK, and India Shopify Markets setup, the tax engine differs per market. The workflow needs branches per market — and you should engage a tax consultant for export-LUT mechanics before automating.
Your sales mix is mostly B2B with complex e-invoicing. B2B orders > ₹50 lakh aggregate / year trigger e-invoice generation requirements (IRN, QR code). The XML schema needs additional fields. Build a separate B2B branch with IRP API integration.
You are on Tally Connect or Tally on Cloud. Both have direct REST APIs that beat XML import. Use those (we have a separate workflow pattern for them — drop the XML, hit the API directly).
Real case — Indore D2C apparel brand, ₹4 Cr GMV
The client: a 9-person apparel brand selling shapewear PAN-India via Shopify. ~1,400 orders a month, accountant Vipul (CA, in-house) was spending Wednesdays and Saturdays just on data entry.
After 14 weeks live:
tax_lines.The Shopify+Tally pattern is a building block we use across AI automation engagements for Indian D2C brands. Founded by Vivek Kumar, Softechinfra's been shipping these accounting-integration projects since 2022; we have a library of ~40 vendor-mapping templates pre-built.
Common mistakes
Mistake 1 — Trusting Shopify's tax_lines blindly. Shopify Markets occasionally misclassifies state codes, especially for newly-added states or address normalisation edge cases. Always re-derive from shipping_address.province.
Mistake 2 — Skipping the orders/paid topic and using orders/create. Orders/create fires on unpaid orders too — you will book Tally entries for orders that never get paid, and have to reverse them. Use orders/paid.
Mistake 3 — Auto-creating customer ledgers without state code. Without the state suffix, you collide ledgers across states. Always tag with {name} - {state_code}.
Mistake 4 — Forgetting refunds. Shopify's refunds/create webhook needs a parallel workflow that generates a Credit Note voucher in Tally with the opposite GST signs. About 4% of our client's orders refund; without this, books drift.
Mistake 5 — Naming XML files with order number only. Two orders in different months can share order numbers in Tally if you have multiple Shopify stores. Use {store_id}-{order_number}-{date}.xml to keep file names unique.
FAQ
Does this work for Shopify Plus?
Yes — Plus has the same webhooks, just higher rate limits. For Plus stores doing 10k+ orders/day we use multi-region n8n with horizontal scaling, but the workflow JSON is unchanged.
What about Magento, WooCommerce, Razorpay Magic Checkout?
Magento: webhook payload differs but the GST split logic transfers. We have a Magento variant. WooCommerce: WP webhooks need an Application Password; sometimes JSON is malformed — add a sanitise step. Razorpay Magic: their order completion webhook is cleaner than Shopify's; same XML emit at the end.
How do I handle e-invoicing (IRN) for orders over the threshold?
Add a branch before the Tally XML emit: if order.total > ₹50,000 and customer has GSTIN, hit the IRP API (NIC's Invoice Registration Portal) for the IRN + QR code, then include them in the Tally XML's <INVOICEDETAILS.LIST> block. Adds ~6 nodes.
What if my Shopify store does not collect GST correctly?
Then your tax-line data will be wrong before our workflow even sees it. Fix Shopify first: in Settings → Taxes → India, configure each rate slab and apply it to the right product collections. Our workflow can flag mismatches but cannot fix bad source data.
Does this work with composition-scheme businesses?
No. Composition-scheme businesses do not charge GST line-by-line and file GSTR-4 quarterly with a fixed turnover-based tax. The XML schema is different (no tax breakdown, just bill_amount). We have a composition variant — different workflow, simpler.
Can I roll-up multiple orders into one Tally voucher per day?
Yes, but we recommend against it. Per-order vouchers give you cleaner audit trails and easier refund handling. The daily digest gives you the rollup view without polluting Tally's voucher register.
What is the audit trail if a tax officer asks?
Every order's XML file in Drive is named with the order number. Tally's voucher narration includes the Shopify reference. Click any voucher in Tally → see the Shopify order ID → look up the original Shopify order screen. That round-trip is exactly what auditors want.
Want this Shopify-to-Tally sync set up for your store?
We ship the full 20-node workflow above, configured to your seller state, your existing Tally ledgers, and your Shopify product tax slabs. Includes the refunds workflow and a state-mapping validation against your last 200 orders. Suitable if you do 300+ orders/month and your accountant is data-entering on weekends. No slides — send us a Shopify CSV export and we will show you the Tally XML that comes out.
Book a 20-min Call