28 November 2024 / Integrations

Accounting integrations need explicit sync state

An integration becomes difficult to support when the application cannot say whether a record is waiting, synced, rejected or changed afterwards. I am making sync state visible so retries and reconciliation have evidence and staff do not have to compare two systems by hand.

Intersecting steel beams and bracing viewed from beneath a bridge.
Photo: Sebastian Schuster (opens in a new tab)

An application and an accounting system will disagree sometimes. A network request times out, a tax code is invalid, a record is edited after export or somebody changes the accounting copy directly. If the local application stores only an external identifier, it cannot explain which version moved, when it moved or whether a retry is safe. The application needs to record that disagreement in a form staff can see and act on. A synced checkbox cannot describe waiting work, rejection, conflict or a timed-out attempt. Choose names that describe operational conditions rather than implementation details. A practical set may include waiting, processing, synced, rejected and review required. The exact list depends on the integration, but each state needs a written entry rule and allowed next steps. "Waiting" means there is local work that has not been accepted by the accounting system. "Processing" should have a start time and a lease or timeout, otherwise a crashed worker can leave the record stuck forever. "Rejected" means the remote system returned a response that requires a correction. "Review required" can cover ambiguity such as a remote change that conflicts with a local edit. Store the state on a sync record rather than overloading the business record. One invoice may participate in more than one integration or have several sync attempts. The sync record can hold the destination, local object, operation, current state and relevant version information.

Staff-facing labels can be friendlier, but they should map cleanly to stable internal states. A successful create usually returns an external identifier. Keep it, but also retain the local version that was sent, the remote version or modification token returned, and the time of acknowledgement. This lets the application distinguish several cases. The local record may be unchanged since the last sync. It may have changed and need an update. The remote record may have changed since the application last saw it. Both may have changed, which needs a conflict rule. A content hash can help identify whether the fields relevant to the integration changed. Build it from a canonical representation of exported fields, not the whole database row. An unrelated local note should not force an accounting update. Keep the mapping between local and remote values too. Tax codes, account codes, currencies and contact identifiers often have different keys in each system. A rejected mapping should be visible as data that needs correction, not flattened into a generic API error. A timeout does not mean the remote operation failed. The accounting system may have accepted a request while the response disappeared. Retrying a create without an idempotency strategy can produce duplicate invoices, contacts or payments.

Use the provider's idempotency key where it supports one. Generate the key from a stable internal operation identifier and reuse it for retries of that operation. If the provider has no idempotency support, search or reconcile using a unique external reference before creating another record. Separate retryable failures from permanent rejection. Connection loss, rate limiting and temporary service errors may be retried with backoff. Invalid account codes, closed accounting periods and missing required fields need correction. Repeating those requests only creates noise and can hide the actual queue. Limit attempts and record the next retry time. When automated retries stop, move the item to a visible state with the last useful error and an assigned route for review. One-way export can still receive remote changes. An accountant may correct a code, void a document or merge a contact. The local application must decide whether those changes are authoritative, imported, warned about or left alone. Write field ownership down. The application might own order details while the accounting system owns settlement and reconciliation fields. Where both sides can edit the same value, define conflict handling rather than relying on the most recent write. Use remote webhooks when available, but assume they may be delayed, duplicated or missed. Verify signatures, process each event idempotently and supplement them with a scheduled reconciliation job. Polling should use modification markers or bounded date windows where possible rather than downloading the full ledger every time.

When a conflict occurs, preserve both observed values and the versions involved. A message saying "sync failed" gives staff no basis for choosing the correct record. Users need sync information near the business record, not only in a developer log. Show the destination, state, last successful sync, pending operation and a short actionable error where one exists. Expose the external reference as a safe link when permissions allow. If the record is waiting, show when processing is due. If it was rejected, name the field or rule that needs attention. If review is required, show which side changed and what actions are available. A retry button should not blindly repeat every operation. It should retry the current failed operation after checking that required corrections have been made. Display whether the action creates, updates, voids or imports a record before staff confirm it. Bulk views matter too. A single badge on an invoice helps one support case; a queue grouped by state and age helps staff see a stalled integration before several cases arrive. Per-record status will not catch everything. A record may never enter the queue because an earlier rule failed, or a webhook may refer to an object the application does not recognise.

Run reconciliation from both directions. Compare local records expected to sync against acknowledged remote identifiers. Check remote changes against known local mappings. Count waiting, processing, rejected and review-required items by age. Flag processing leases that have expired. Financial totals can be a useful secondary check, but matching totals do not prove that individual records are correct. Compare counts and identifiers as well, within a clearly defined period and status scope. Timezones and inclusive date boundaries must match on both sides. Before releasing a change, test successful create and update, duplicate event, timeout after remote acceptance, permanent validation error, rate limit, local edit after sync, remote edit after sync and an unknown remote record. For each case, verify the visible state and the available next action. Ask support to use that view to explain what happened and whether a retry is safe; add any missing detail before release.

Continue the thinking.

Comments are public and hosted in an open-source GitHub Discussions repository.

Loading comments connects your browser to GitHub. A GitHub account is required to post.

All blogs