Every accountant I spoke to had the same complaint.
Open Tally Prime → export data → wait → open Excel → format it → wait some more.
Repeat this every single day for ledgers, invoices, balance sheets.
It was slow, manual, and error-prone. One wrong cell in Excel and the numbers don't match. One missed export and the report is already stale.
So I built TallySync - a real-time financial dashboard that talks directly to Tally Prime via its XML API. Create ledgers, push invoices, pull data, view analytics - all from a clean web UI, without touching Tally's menus at all.
🎥 Live Demo: sureshcodes.vercel.app/tallysync
📂 Source Code: github.com/Suresh4405/TallySync-Portal
What It Does
- Create ledgers (with GST, PAN, address, opening balance) and push them straight into Tally
- Create sales invoices and sync them as vouchers in Tally
- Pull ledgers from an existing Tally company into the dashboard database
- View analytics — revenue charts, sync status breakdowns, ledger summaries
- Admin panel to manage users and monitor sync logs
- JWT-based auth with protected routes
The Tech Stack
Frontend: React + Vite, Material UI, Recharts, Axios
Backend: Node.js + Express, Sequelize ORM, MySQL
Tally Integration: Tally XML API (ODBC port 9000)
How It Works
Step 1: Tally's XML API
Tally exposes a local HTTP server on port 9000 when ODBC is enabled. You can send XML envelopes to it and it either exports data or imports it directly into the active company.
To enable it: open Tally → F11 → F1 → Enable ODBC Server
Here's how the backend talks to Tally:
// services/TallyService.js
async sendRequestToTally(xmlData) {
const response = await axios.post(this.tallyHost, xmlData, {
headers: {
'Content-Type': 'application/xml',
'Accept': 'application/xml'
},
timeout: 30000
});
return response.data;
}
If ECONNREFUSED comes back, the app catches it and gives a clear message instead of a cryptic error.
Step 2: Creating a Ledger in Tally
When you submit a ledger form in the UI, the backend saves it to MySQL first, then fires an XML import request to Tally:
// services/TallyService.js
async createLedgerInTally(ledgerData, userId) {
const isConnected = await this.testTallyConnection();
if (!isConnected) {
throw new Error('Cannot connect to Tally. Please check Tally is running.');
}
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<ENVELOPE>
<HEADER>
<TALLYREQUEST>Import Data</TALLYREQUEST>
</HEADER>
<BODY>
<IMPORTDATA>
<REQUESTDESC>
<REPORTNAME>All Masters</REPORTNAME>
</REQUESTDESC>
<REQUESTDATA>
<TALLYMESSAGE xmlns:UDF="TallyUDF">
<LEDGER NAME="${ledgerData.ledger_name}" Action="Create">
<PARENT>${ledgerData.parent_group || 'Sundry Debtors'}</PARENT>
<OPENINGBALANCE>${ledgerData.opening_balance || 0}</OPENINGBALANCE>
<GSTREGISTRATIONTYPE>Regular</GSTREGISTRATIONTYPE>
<PARTYGSTIN>${ledgerData.gst_number || ''}</PARTYGSTIN>
</LEDGER>
</TALLYMESSAGE>
</REQUESTDATA>
</IMPORTDATA>
</BODY>
</ENVELOPE>`;
const response = await this.sendRequestToTally(xml);
// parse response, extract GUID, update DB record
}
If Tally accepts it, the tally_guid is extracted from the response and saved back to the DB so the record is fully traceable.
Step 3: Pulling Ledgers Back from Tally
The sync is bidirectional. The app can also fetch all ledgers from Tally and upsert them into MySQL:
async syncLedgersToDatabase(userId) {
const tallyLedgers = await this.getLedgersFromTally();
for (const tallyLedger of tallyLedgers) {
const [ledger, created] = await TallyLedger.findOrCreate({
where: { ledger_name: tallyLedger.name },
defaults: {
ledger_name: tallyLedger.name,
parent_group: tallyLedger.parent,
opening_balance: parseFloat(tallyLedger.opening_balance) || 0,
closing_balance: parseFloat(tallyLedger.closing_balance) || 0,
tally_guid: tallyLedger.guid,
synced_at: new Date()
}
});
if (!created) {
await ledger.update({ closing_balance: ..., synced_at: new Date() });
}
}
}
Step 4: The Dashboard & Analytics
The dashboard shows:
- Total ledgers, invoices, synced count, failed syncs
- Revenue trend (bar/line/area chart via Recharts)
- Sync status pie chart (pending / success / failed)
- Recent sync activity log
The analytics page lets you filter by date range and toggle between chart types.
The Database Schema
Three core tables:
tally_ledgers — stores ledger data with tally_guid, synced_at, GST, PAN, address, balances
tally_invoices — stores invoice/voucher data with sync_status enum (pending / success / failed)
sync_logs — audit trail of every push/pull operation with timestamps and error messages
Sequelize handles migrations with sync({ alter: false }) in production so the schema doesn't get nuked on redeploy.
What Broke First
XML parsing. Tally's response XML is deeply nested and the tag names change depending on the operation. I had to write a dedicated parser that extracts the GUID and checks for error strings inside the response body — because Tally returns HTTP 200 even when the import fails. The error is buried in the XML.
Connection refused with no context. The first time Tally wasn't running, the frontend just showed a generic 500. I added explicit ECONNREFUSED handling in the service layer so users get a helpful checklist instead.
Why I Stored Data in MySQL Too
I could have just been a passthrough — send to Tally and forget. But having a local copy means:
- You can query, filter, and show analytics without hitting Tally every time
- You have a sync log to audit what succeeded and what failed
- If Tally is offline, users can still view existing records
Setup
# Server
cd server
cp .env.example .env # set DB_HOST, TALLY_HOST, JWT_SECRET
npm install
node indexserver.js
# Client
cd client
cp .env.example .env # set VITE_API_URL
npm install
npm run dev
Make sure Tally is open with ODBC enabled on port 9000. The app checks the connection before every push operation.
🚀 Live Demo
https://sureshcodes.vercel.app/tallysync
📂 Source Code
github.com/Suresh4405/TallySync-Portal
👨💻 Portfolio
Drop a 🧡 if this was useful — especially if you've ever fought with Tally's XML API.

Top comments (1)
This solves a pain point every Tally user knows. Direct XML integration instead of endless exports can save a surprising amount of time and reduce reporting mistakes.