All work

case study 02 · 2025 to 2026 · sole author of both integrations

Two ERPs that answer 200 when they fail

Merchants keep their books in Tally or BUSY on a desktop. The platform needs items, customers, stock, prices and balances to flow in, and orders, delivery notes and invoices to flow out. Both ERPs report failure as success.

Tally add-on
Sole author6,000+ lines of TDL
releases
Versionedwith in-app auto-update
BUSY plugin
Sole authorreverse-engineered
stack
TDLNode.js, BullMQ, XML
TallyHTTP/1.1 200 OK
<RESPONSE>
  <CREATED>0</CREATED>
  <ERRORS>1</ERRORS>
  <LINEERROR>...</LINEERROR>
</RESPONSE>
BUSYHTTP/1.1 200 OK
Result: F
Description: ...

<z:row Col1="..." Col2="..." />
Both say 200. The truth is in the body for Tally and in a header for BUSY, so the integration never trusts the status code.

the claim

Tally answers HTTP 200 for every request and reports the outcome inside the XML, as STATUS, LINEERROR, ERRORS or EXCEPTIONS. An import result can even report STATUS 1 while hiding failures. BUSY exposes one GET endpoint driven by numeric service codes in headers, returns ADO rowsets with columns as attributes, and also answers 200 whatever happens. The BUSY code that existed before me had never actually run.

what I built

  • A Tally add-on in TDL, over 6,000 lines across 19 modules: two-way sync, extra fields on the stock item screen, a Send Invoice button on the voucher form, an auth and health check, and an in-app updater that downloads and hot-reloads a new build.
  • The full BUSY client (service codes 1 to 9): catalogue, stock, price and outstanding sync, orders as sale orders, and shipments as delivery challans.
  • Response handling that reads the body and headers for the real outcome, never the status code, with credentials redacted from every log line.

three more lies

Batch Post. Tally's documented Start Batch Post is a no-op for an HTTP JSON collection, so whole catalogues went out in one POST and the API rejected them with a 413. I wrote a two-pass pager in TDL that posts 500 rows at a time and stamps the returned ids back onto each master.

The timeout. A 26 MB Day Book export blocked Node's event loop for minutes, long enough that the request's own abort timer never fired. A size guard now runs before parsing, and big exports fall back to reading ids first, then exporting in 500-master windows. A 28.9 MB export that used to fail now completes in bounded batches.

The migration. Moving clients from the TDL add-on to the desktop connector would have re-imported every catalogue as duplicates, because the two generations mint different sync ids from the same Tally master. Reconciliation now lets the legacy id win wherever one exists. No client got a duplicate catalogue.

what I built next

Sync alone meant sellers still kept two sets of books. So I rebuilt purchases and sales on a books layer shaped like Tally's own model: groups, ledgers and vouchers, with stock lines and bill-wise allocations.

Where a store runs Tally or BUSY, the ERP owns the invoice and the platform keeps a replica, synced both ways. Where a store runs no ERP, the platform's own books do the job. When to invoice is set per store, because one client invoices on acceptance and another on dispatch.

what changed

Item and customer sync work on companies of any size, in both directions. The longer story, with the XML, is in Building a two-way Tally ERP sync that survives retries.

next case · 03 When Postgres quietly picks the slow plan