Blog
Accounting integrations need explicit sync state
An accounting integration gets hard to support once the app can't say whether a record is waiting, synced, rejected or changed afterwards. I'm making sync state visible so retries and reconciliation have something to go on, and staff don't have to compare two systems by hand.

Your application and the accounting system are going to disagree sometimes. A request times out, a tax code is invalid, someone edits a record after it's been exported, or somebody changes the accounting copy directly. If all you've stored locally is an external ID, the app can't tell you which version moved, when it moved or whether a retry is safe. A synced checkbox doesn't help much either, because it has nowhere to put waiting work, a rejection, a conflict or an attempt that timed out.
I'd keep the state on its own sync record instead of loading it onto the invoice itself, since one invoice might go to more than one integration or have several attempts. A practical set of states might be waiting, processing, synced, rejected and review required, each with a written rule for how a record gets in and what can happen next. "Processing" needs a start time and a lease or timeout, otherwise a crashed worker leaves the record stuck forever.
When a create succeeds, keep the external ID, but also keep the local version you sent, whatever version or modification token came back and the time it was acknowledged. That's what lets you work out whether the local side changed, the remote side changed or both did (which needs a conflict rule). If you build a content hash from the exported fields rather than the whole database row, an unrelated local note won't trigger an accounting update.
A timeout doesn't mean the remote operation failed. The accounting system may have accepted the request and the response just never came back, so blindly retrying a create can give you two invoices, contacts or payments. Use the provider's idempotency key if it has one, generated from a stable internal operation ID and reused for every retry of that operation. If it doesn't, search for a unique external reference before creating anything. Keep retryable failures like connection loss, rate limiting and temporary service errors apart from rejections such as an invalid account code or a closed accounting period, because repeating those only adds noise to the queue.
Staff need to see all this on the business record itself, since nobody's going to dig through a developer log to answer a support question. Show the destination, state, last successful sync, the pending operation and a short error that names the field or rule needing attention. A retry button should retry the current failed operation after checking the correction was made, and say whether it'll create, update, void or import before anyone confirms. A queue grouped by state and age helps someone spot a stalled integration before several cases arrive.
Before releasing a change, I'd test a successful create and update, a duplicate event, a timeout after remote acceptance, a permanent validation error, a rate limit, a local edit after sync, a remote edit after sync and an unknown remote record. For each one, check the visible state and the next action available. That's what explicit sync state is for: when the app and the accounting system disagree, the app can say which side moved, what failed and whether a retry is safe, so staff aren't comparing the two systems by hand. The last check is to ask support to use that view to explain what happened in each case, and if they can't, the state model needs more work before release.
On X
Got a take on this?
Share it on X. I read the replies.