Every developer who has integrated with an on-premise accounting system knows the failure mode, even if they've never named it. You push a record to the cloud API, it returns 200, and the data never reaches the desktop. No error. No log line. Nothing — until a customer notices a missing invoice a week later. This is silent failure, and it is the defining problem of desktop-to-cloud integration.
Tally is the canonical example in the Indian market, but the shape is universal: QuickBooks Desktop, Tally, Busy, any system that runs on a machine in an office rather than a server you control. The cloud API is the easy part. The hard part is that the API is blind to the desktop's state.
Why the failure is silent
A desktop-to-cloud integration has three moving parts: your application, a cloud bridge, and a small connector running next to the desktop software. When you push a voucher, the bridge accepts it and forwards it to the connector, which talks to the local software.
The trouble is that accepting a push and delivering it are different events, often seconds or minutes apart. So the API returns success the moment the bridge accepts the work — long before anyone knows whether the desktop was even reachable. If the accounting software was closed, or the connector was offline, or the wrong company file was open, the push simply evaporates downstream. The caller already got its 200 and moved on.
Most integration tools stop here. They give you a fire-and-forget push and a dashboard a human can squint at. What they don't give you is a signal your code can act on before it sends. So your only real strategy is "push again and hope."
The fix: expose the desktop's state as data
The cure for a blind push is a preflight check — a health endpoint that reports the live state of the desktop as structured, enumerable values you can branch on. Not a colour on a screen: a field in a JSON body.
The mistake teams make is modelling that state as a boolean (online: true/false). "Offline" is not one condition — it is several, and the right message to your user depends on which one. You need at least two small state machines.
State machine 1: is the accounting software actually running?
The connector can be perfectly online while the accounting software itself is closed. So "connector reachable" and "software running" are distinct facts. A useful probe returns four states, not two:
| State | Meaning | What to tell the user |
|---|---|---|
running | The software is open on the paired machine. | — (safe to sync) |
not_running | The connector is online, but the software is closed. | "Please open Tally to sync." |
reconnecting | A transient link loss, recovering. | "Reconnecting… try again shortly." |
unreachable | Connector offline, machine asleep, or bridge unavailable. | "Your Connector is offline." |
The distinction between not_running and unreachable is the whole point. Both are "can't sync right now," but one is fixed by opening an application and the other by turning a machine on. Collapse them into false and you've thrown away the only information that lets your UI say something useful.
State machine 2: is the right company loaded?
Even with the software running, a multi-company accounting package might have no company open, or the wrong one. That's a second four-state probe:
| State | Meaning |
|---|---|
company_linked | The correct company is loaded and bound — safe to sync. |
no_company_loaded | The software is running but no company is open. |
company_unavailable | The company is known but not currently reachable. |
company_not_linked | The company isn't bound to this connector. |
Now "the sync failed" resolves into precise, actionable causes: the app is closed (not_running), the machine is off (unreachable), no file is open (no_company_loaded), or the wrong file is open (company_not_linked). Each maps to a different sentence you can show a non-technical user.
The preflight pattern
With those states exposed, blind push becomes check → decide → push:
# 1. Ask before you send
GET /v1/companies/{id}/health
Authorization: Bearer KEY_ID:SECRET
# → { "tally_status": "not_running",
# "company_status": "no_company_loaded",
# "connector_online": true, ... }
# 2. Decide in your code
if health.tally_running and health.company_linked:
POST /v1/invoices # it will land
else:
show health.tally_status / health.company_status to the user
retry when they've fixed it
Two design details make this work in practice:
- The health endpoint should never error. An offline connector is an answer (
unreachable), not an exception. Return200with a usable state every time, so callers don't have to wrap the check in a try/catch that itself becomes a failure path. - It should be cheap and unmetered. If you want developers to call it before every push, you cannot bill them per check. A metered health probe just trains people to skip it — which reintroduces the silent failure you were trying to kill.
Health, timeouts, and outcomes are three different questions
A common confusion is treating the preflight check as redundant with runtime errors. They answer different questions, and you want all three:
- Health — "can I sync right now?" Answered before you send, by the four-state probe.
- Timeout — "did this specific call reach a connector?" Answered during, by a distinct signal (e.g. a
504 connector_timeout) rather than a generic 500, so retries are easy to reason about. - Outcome — "did the desktop accept what I sent?" Answered after, by a status endpoint that surfaces the raw rejection (a bad ledger, a duplicate) so you can fix the cause.
Preflight with health and the timeout becomes the rare exception instead of your primary — and only — failure signal.
Takeaways
- Silent failure comes from an API that can't see the desktop. The cure is to make the desktop's state a first-class part of the API.
- Model that state as small state machines, not booleans — "offline" has several causes and each needs a different response.
- Expose a preflight health check that always returns
200and is cheap enough to call before every write. - Keep health, timeout and outcome as three separate signals.
At Bizmitra we built exactly this for Tally: a live GET /v1/companies/{id}/health that returns both four-state machines above, so an integration can check whether Tally is running and the right company is loaded before it pushes a voucher. If you're evaluating any Tally integration, "can your API tell me whether Tally is running right now?" is the first question to ask — most can't. You can read how ours works on the Tally connector status API page.
Comments 0
Leave a Comment