payment.captured events, normalises split payouts from settlements, posts receipts into Tally via the XML import API, and emails the CFO a one-page reconciliation report. The accountant's morning task dropped from four hours to 22 minutes — only the exceptions.
order_id and a fee field, but the net credit you actually receive in your bank depends on the settlement schedule (T+2 by default), MDR (Merchant Discount Rate, 2% for cards in this client's tier), and the 18% GST on that MDR. Naive sync scripts that just take payment.entity.amount and post it to a single Tally ledger will be wrong by exactly the fee amount, every day. The error compounds and shows up as a "missing ₹38,400 this month" surprise during the GST filing on the 20th. We have cleaned up exactly that mess for three Indian SMBs in the last quarter.
The other September 2025 trigger: GST 2.0 went live on 22 September 2025 — the 12% and 28% slabs were collapsed into 5% and 18% (with a 40% sin slab). Any reconciliation flow built before that date had hard-coded slab logic that broke overnight. We rebuilt this client's mapping table on 21 September. Two weeks later the next quarterly GST filing cleared on the first attempt.
## The 4-block architecture
{
"parameters": {
"method": "GET",
"url": "https://api.razorpay.com/v1/payments",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendQuery": true,
"queryParameters": {
"parameters": [
{ "name": "from", "value": "={{ Math.floor(new Date(new Date().setHours(0,0,0,0)).getTime()/1000) }}" },
{ "name": "to", "value": "={{ Math.floor(new Date(new Date().setHours(23,59,59,999)).getTime()/1000) }}" },
{ "name": "count", "value": "100" }
]
},
"options": {
"response": { "response": { "responseFormat": "json" } }
}
},
"name": "Razorpay payments today",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [460, 300]
}
Two things to know. The auth type is HTTP Basic Auth — Razorpay's key_id goes in the username, key_secret in the password. Never put the key_secret into a query string; we have seen it leak via n8n execution logs that were screen-shared during a postmortem. The from/to are Unix seconds, not milliseconds. Razorpay returns 400 if you pass milliseconds; the error message is unhelpful ("Invalid timestamp"). We added a comment in the workflow.
### Node 2 — Settlement enrichment + 4-line split (Code node)
{
"parameters": {
"jsCode": "const items = $input.all();\nconst out = [];\nconst MDR_PCT = 2.0; // client's negotiated card MDR\nconst GST_ON_MDR = 18.0; // post Sep-22 GST 2.0\nfor (const it of items) {\n const p = it.json;\n if (p.status !== 'captured') continue;\n const grossPaise = p.amount;\n const mdrPaise = Math.round(grossPaise MDR_PCT / 100);\n const gstPaise = Math.round(mdrPaise GST_ON_MDR / 100);\n const netPaise = grossPaise - mdrPaise - gstPaise;\n out.push({\n json: {\n payment_id: p.id,\n order_id: p.order_id,\n method: p.method,\n gross: grossPaise / 100,\n mdr: mdrPaise / 100,\n gst_on_mdr: gstPaise / 100,\n net_credit: netPaise / 100,\n captured_at: new Date(p.created_at * 1000).toISOString(),\n contact: p.contact,\n email: p.email\n }\n });\n}\nreturn out;"
},
"name": "Split into 4 ledger lines",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [680, 300]
}
The MDR and GST percentages are constants here for clarity. In the production version they are read from a Google Sheet that the CFO can edit — when Razorpay re-tiered MDR for this client in November, we changed the sheet, not the code. The split is rounded in paise, not rupees, then divided by 100 only at output. Rounding in rupees first leaves a 1-2 paise drift that compounds over a month into a ₹40 mismatch the CFO will notice.
### Node 3 — Tally XML voucher POST (HTTP Request)
{
"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": "=<ENVELOPE><HEADER><TALLYREQUEST>Import Data</TALLYREQUEST></HEADER><BODY><IMPORTDATA><REQUESTDESC><REPORTNAME>Vouchers</REPORTNAME><STATICVARIABLES><SVCURRENTCOMPANY>Pune Distributors Pvt Ltd</SVCURRENTCOMPANY></STATICVARIABLES></REQUESTDESC><REQUESTDATA><TALLYMESSAGE><VOUCHER VCHTYPE='Receipt' ACTION='Create'><DATE>{{ $json.captured_at.slice(0,10).replaceAll('-','') }}</DATE><NARRATION>Razorpay {{ $json.payment_id }} order {{ $json.order_id }}</NARRATION><VOUCHERTYPENAME>Receipt</VOUCHERTYPENAME><ALLLEDGERENTRIES.LIST><LEDGERNAME>Razorpay Bank</LEDGERNAME><ISDEEMEDPOSITIVE>Yes</ISDEEMEDPOSITIVE><AMOUNT>-{{ $json.net_credit }}</AMOUNT></ALLLEDGERENTRIES.LIST><ALLLEDGERENTRIES.LIST><LEDGERNAME>MDR Expense</LEDGERNAME><ISDEEMEDPOSITIVE>Yes</ISDEEMEDPOSITIVE><AMOUNT>-{{ $json.mdr }}</AMOUNT></ALLLEDGERENTRIES.LIST><ALLLEDGERENTRIES.LIST><LEDGERNAME>Input GST 18%</LEDGERNAME><ISDEEMEDPOSITIVE>Yes</ISDEEMEDPOSITIVE><AMOUNT>-{{ $json.gst_on_mdr }}</AMOUNT></ALLLEDGERENTRIES.LIST><ALLLEDGERENTRIES.LIST><LEDGERNAME>Sundry Debtors</LEDGERNAME><ISDEEMEDPOSITIVE>No</ISDEEMEDPOSITIVE><AMOUNT>{{ $json.gross }}</AMOUNT></ALLLEDGERENTRIES.LIST></VOUCHER></TALLYMESSAGE></REQUESTDATA></IMPORTDATA></BODY></ENVELOPE>"
},
"name": "Tally Receipt voucher",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [1100, 300]
}
The endpoint is the LAN IP of the Tally Server-9 machine on port 9000 — Tally's HTTP listener. The voucher has four ledger entries on purpose: a credit to "Sundry Debtors" (the customer paid us), three debits split into "Razorpay Bank" (the actual money we received), "MDR Expense" (Razorpay's cut), and "Input GST 18%" (the tax we can claim back on the MDR). ISDEEMEDPOSITIVE=Yes is Tally's accounting convention for debit; No is credit. Get this wrong and the trial balance refuses to balance with no useful error message — Tally just rejects the entire envelope.
## The split-settlement edge case (this is the one nobody warns you about)
Razorpay aggregates multiple payments into a single bank settlement (the "T+2 sweep"). When a chargeback or refund happens between capture and settlement, the settlement is split — you get one credit for the bulk, then a separate debit for the chargeback, possibly on a different day. Naive sync scripts that 1:1-match payment to bank credit will be off by the chargeback amount and stay off until someone manually reconciles.
/v1/settlements endpoint per payment ID and store the settlement_id in the Tally narration. When the bank credit lands two days later (matched separately by a different sub-flow), the reconciliation joins on settlement_id, not on payment_id or amount. This is the single change that took our auto-match rate from 89% to 99.6%.- 1. Schedule Trigger (cron
30 23 * * *Asia/Kolkata) - 2. HTTP — Razorpay Payments API (today's window)
- 3. Split In Batches (50 per batch, prevents Razorpay rate-limit hits)
- 4. HTTP — Razorpay /v1/settlements/{id} (per payment, settlement enrichment)
- 5. Code — split into 4 ledger lines (gross / MDR / GST / net)
- 6. IF — branch refunds and chargebacks to a different sub-flow
- 7. Set — format Tally date (DD-MM-YYYY → YYYYMMDD)
- 8. HTTP — POST Tally voucher XML to Server-9 listener
- 9. XML — parse Tally response (LINEERROR check)
- 10. IF — success vs error branch
- 11. Google Sheets append (success log, daily totals)
- 12. Google Sheets append (exception log, with raw payload)
- 13. Email Send — CFO morning summary at 06:00 (Wait node)
- 14. Error Workflow trigger — DM ops on any node failure
- Razorpay key_id / key_secret stored as n8n credential, never in workflow JSON
- Tally Server-9 HTTP listener confirmed running on port 9000 (test with curl)
- All 4 ledger names spelt EXACTLY as in Tally — case-sensitive, including "Razorpay Bank" not "Razorpay bank"
- MDR and GST percentages match the client's actual Razorpay tier (check the Razorpay dashboard, not the docs)
- GST 2.0 mapping (5%/18%/40%) verified against post-22-September rules
- Cron timezone set to Asia/Kolkata explicitly, not server-default UTC
- Exception sheet has columns: payment_id, order_id, gross, error_text, raw_payload, status
- CFO email template renders correctly with zero-payment days (test by clearing the day's data)
- Workflow exported to git as JSON the day of go-live
- Manual test run in Tally sandbox company before pointing at production
settlement_id in the Tally narration, then run a weekly sub-flow that re-matches on settlement_id once the bank credit lands. Do not try to force same-day reconciliation for chargebacks.
Symptom: "n8n times out on the Razorpay API for high-volume days." Cause: the default HTTP timeout is 5 min, but a busy day with 800+ payments hits Razorpay's pagination limits. Fix: switch to the Split In Batches node (size 50), then loop. We added an exponential backoff on rate-limit (429) errors — 3 retries, 2/4/8 second waits.
Symptom: "voucher posts twice on the same day." Cause: the workflow ran twice (manual trigger + scheduled), Tally happily accepts duplicates. Fix: add a unique reference in the voucher narration (we use payment_id), then a Tally TDL extension that rejects duplicates. The cleaner fix: use n8n's Execute Workflow Trigger with a deduplication key.
## Mini case study — 41 days at the Pune distributor
The first install ran 4 August 2025. After 41 days the numbers stabilised: 3,847 payments processed, 14 sent to the exception sheet, 12 of those resolved by the accountant within an hour, 2 escalated to Razorpay support (both turned out to be merchant-side disputes, not workflow bugs). The accountant's morning task — previously 4 hours of CSV wrangling once a week — is now 22 minutes of exception triage every weekday. Quarterly GST filing on 20 October took 90 minutes instead of the historical 6-hour ordeal.
For a similar nightly-close pattern with a Shopify storefront layered on top, see our 4-way Shopify + Tally + WhatsApp sync shipped in late 2025. For the architecture playbook used across these reconciliation projects, see the AI automation team page — same n8n stack, different end-points.
## When NOT to build this
Skip this flow if (a) your monthly Razorpay volume is under ~150 payments — the manual export-and-paste is genuinely faster, (b) your accountant is on Tally Prime Cloud (not Server-9) — the HTTP listener is not exposed there yet, you would need to fall back to the Excel-import path, or (c) your CFO actually wants the morning 4-hour reconciliation as a control activity. We turned down two clients in 2025 for reason (c). Automation does not fix a process that exists for governance reasons.
For the build pattern when Tally is not in the mix, see the Radiant Finance project — same n8n + Razorpay stack, but writing into a custom Postgres ledger instead.
## FAQ
### How long to build and deploy this n8n + Razorpay + Tally workflow?
For us, 7 working days end-to-end: 1 day on Razorpay credentials and ledger mapping, 2 days on the n8n flow, 2 days on Tally XML format debugging (Tally's error messages are notoriously sparse), 1 day on exception handling, 1 day on the CFO email and 41-day shadow run. For a self-build, budget 14-21 days including time spent learning Tally TDL syntax.
### Can this run on n8n Cloud instead of self-hosted?
Yes for the Razorpay parts, no for the Tally part — Tally Server-9's HTTP listener sits on a LAN IP that n8n Cloud cannot reach without exposing it via Cloudflare Tunnel or ngrok. We strongly recommend self-hosting n8n on the same LAN as Tally for security; the listener has no auth.
### What about GST 2.0 — did this break on 22 September 2025?
We rebuilt the slab mapping table on 21 September. The change was small in code (the mapping moved from a hard-coded constant to a Google Sheet lookup) but the test plan was 6 hours — we re-ran the previous month's data through both the old and new mapping to flag any discrepancies. There were exactly two — both were borderline 12-percent items that moved to 18 percent. The CFO approved both manually.
### How do I handle refunds and chargebacks?
Branch them at node 6 (the IF after settlement enrichment). Refunds get a Tally Payment voucher (opposite of Receipt), chargebacks get a Journal voucher with a "Disputed Receivable" ledger. We documented the chargeback flow as a separate 6-node sub-flow because the Razorpay payload structure is different.
### What is the Tally TDL response format?
Tally returns a small XML with RESPONSE and LINEERROR fields. Empty LINEERROR means success. Any text in there is a failure — usually a ledger name typo or a date format issue. We parse the response with the n8n XML node and route to the success/exception branch based on whether LINEERROR is empty.
### Why HTTP instead of the official Razorpay node?
n8n's official Razorpay node (as of v1.x in September 2025) covers payments and orders but not settlements — and settlement enrichment is the single most important step for accurate reconciliation. We use the HTTP node for the settlement endpoint and may use the Razorpay node for the payments fetch in newer flows. Both work.
### Can the same flow handle UPI Autopay or recurring payments?
Yes, with one tweak: the method field on Razorpay's payment object includes upi, card, netbanking, wallet, emi. UPI Autopay is method upi with recurring=true — you can branch in node 5 to post to a separate "Subscription Receivable" ledger.
Want this auto-reconciliation flow built for your finance team?
We ship the Razorpay + Tally daily close — n8n on your Hetzner box, Razorpay credentials, full Tally XML mapping including GST 2.0, CFO morning email — in 7 working days for ₹62,000. Suitable for any Razorpay-powered SMB doing 200+ payments a month with Tally Server-9 on the LAN.
Book a 20-min Call
