DEV Community

Suresh
Suresh

Posted on

I Built a Real-Time Financial Dashboard That Talks Directly to Tally Prime's XML API

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 → F11F1Enable 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;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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() });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: The Dashboard & Analytics

Dashboard

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
Enter fullscreen mode Exit fullscreen mode

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

sureshcodes.vercel.app


Drop a 🧡 if this was useful — especially if you've ever fought with Tally's XML API.

Top comments (1)

Collapse
 
bhavin-allinonetools profile image
Bhavin Sheth

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.