Tutorial

How to Integrate a Custom CRM With Accounting Software

A practical guide to linking a custom CRM with accounting software so every record has one owner, one match key, and no duplicate rows.

To integrate a custom CRM with accounting software without duplicating data, give each record one owning system and let the other system hold a reference to it, matched on a stable internal identifier rather than on a name or an email address. Each sync run then updates the same record instead of creating a second one, and every duplicate you find in production traces back to a place where that rule was broken.

Why does duplicate data appear when a custom CRM and accounting software sync?

Duplicates appear because both systems create the same record independently: a salesperson adds a contact in the CRM, an invoice clerk adds a customer in accounting, and the sync later matches them by name or email, which are not unique.

Four mechanisms cause it. The first is independent creation, where two people add the same company in the same week. The second is an unstable match key, since names change when a company rebrands, emails change when staff leave, and phone numbers get reformatted. The third is a re-import from a spreadsheet with no way to tell which rows already exist, and the fourth is two-way writes, where each system is allowed to create records the other one owns.

A few terms make the rest of this easier. A system of record is the one application that holds the authoritative version of a piece of data. An API, short for application programming interface, is a documented way for one program to read and write data in another. A webhook is a message one system pushes to another the moment something changes, and a sync is any process that keeps two stores of data in agreement.

Which system should be the source of truth for each record?

Assign each record type to one owner: the CRM owns leads, contacts, and deal history, while accounting owns invoices, payments, tax details, and the chart of accounts. The other system stores a copy for display and never writes back.

The split rarely follows the boundary between the two products. Sales wants to see whether a customer pays on time; finance wants to see who negotiated the contract. Those are read needs, not write needs, and treating them that way removes most of the conflict. Where a single field is genuinely co-owned, such as a billing address, pick one writer and make the other side read only. In accounting, the chart of accounts is the list of categories that transactions are filed under, and it should change in one place only.

Record typeOwnerSync directionMatch key
Contact and companyCustom CRMCRM to accountingcrm_contact_id
Lead and opportunityCustom CRMnonenot applicable
Customer accountAccountingaccounting to CRM, reference onlyaccounting_customer_id
InvoiceAccountingaccounting to CRMaccounting_invoice_id
Payment and credit noteAccountingaccounting to CRMaccounting_invoice_id
Accepted quoteCustom CRMCRM to accounting on acceptancecrm_deal_id

The link between the two sides is a foreign key, which is an identifier one record stores so it can point at a related record in another system. In practice you keep a small link table: one row per matched pair, holding the CRM identifier, the accounting identifier, the record type, the last sync time, and a status. That table is the only place the integration decides whether a record already exists, which makes it the part worth backing up and watching.

How do you build the sync without creating copies?

Build one mapping layer that stores the identifier pairs, then run every sync through it. Create only when no link exists, update when one does, and make each operation idempotent so a repeated message cannot insert a second row.

Idempotent means running the same operation twice leaves the data in the same state as running it once. Without that property, a webhook that fires twice, which happens whenever a receiving endpoint times out and the sender retries, produces a duplicate. The build order that works:

  1. Inventory the fields in both systems and mark each one with an owner, a direction, and a transformation rule. Currency, date format, and tax rounding are the usual trouble spots.
  2. Create the link table and fill it with a one-time matching pass over existing records, reviewed by a person before it goes live.
  3. Write one function per record type that looks up the link and then creates or updates. Nothing else is allowed to write.
  4. Choose how changes are detected, which is the trade-off in the table below.
  5. Add a daily reconciliation job, meaning a comparison of counts and totals on both sides to confirm the two systems agree about the same set of records.
TriggerTypical latencyFitsMain risk
Webhooksecondsinvoices, payments, status changesmissed events when the endpoint is down
Scheduled polling2 to 15 minutescontacts, price lists, reference datawasted calls, out of order updates
Nightly batchone dayhistorical loads, reporting tablesstale figures during working hours

Whichever you choose, keep a dead letter queue, which is a holding area for messages that failed after several retries so they can be inspected and replayed instead of lost. Log the identifier pair on every write. When someone reports a duplicate six months from now, that log is the fastest way to find which step produced it.

What does the integration cost to build and run?

Expect roughly four to eight weeks of build time for one backend developer, with most of that effort going into field mapping and reconciliation rather than into API calls. Ongoing running costs are typically modest unless volumes are high.

A reasonable estimate for a small company is 60% to 80% of the project spent on mapping, matching rules, and cleanup, with the remainder on the traffic between systems. Even with good design, roughly 1% to 2% of sync events fail on the first attempt and need a retry, mostly from timeouts and validation errors on the receiving side. The cost of doing nothing is easier to measure: a finance team of three typically spends five to eight hours a week reconciling accounts by hand when duplicates stay in place. Hosting and monitoring for the integration itself usually runs a few hundred dollars a month at small volumes.

Cleaning up the records that already exist is a separate job and often the larger one. Most teams find that 2% to 5% of their customer records are duplicates before the first cleanup pass. Match on more than one field, merge carefully, and keep an audit trail of what was merged, because finance will ask which invoice belonged to which customer before the merge.

What should you test before switching the sync on?

Test against a copy of production data and watch duplicates, not only error logs. Seed the same customer in both systems deliberately and confirm the sync links them instead of creating a second record.

A practical pre-launch check:

  • Send the same webhook payload twice and confirm the second one changes nothing.
  • Rename a customer in the owning system and confirm the other side updates rather than creating a new record.
  • Delete a record on the owning side and write down what the other side should do about it.
  • Replay a week of historical events and compare final totals on both sides.
  • Fail the receiving endpoint on purpose and confirm the retries and the dead letter queue behave as expected.

If you would rather not maintain the mapping layer in house, custom CRM development covers this as one scoped piece of work, usually alongside the CRM itself. Either way the operating rule stays the same: one owner per record, one link table, and no writes from the side that does not own the data.

Frequently asked questions

Which system should own customer data, the CRM or the accounting software?

The CRM owns the contact and the relationship history, while the accounting system owns the customer account, invoices, and payments. Both sides display the other's data by reference, so the underlying values are edited in one place only.

How do you match existing records when you first connect a CRM and accounting package?

Run a one-time matching pass that scores candidate pairs on several fields, such as company name, tax number, billing address, and domain, then have someone review the matches before anything is linked or merged. Roughly 2% to 5% of records in a typical customer list turn out to be duplicates at this stage, so the review step is what stops bad merges.

Can a custom CRM and accounting software sync in both directions?

Yes, but not for the same record or the same field. Each record type has one owner that accepts writes, and the other system receives updates for display, which prevents the two sides from overwriting each other's values.

How long does it take to integrate a CRM with accounting software?

A first version typically takes four to eight weeks with one backend developer and a part-time finance stakeholder, and the field mapping and matching rules take longer than the API work. Allow another few weeks for cleaning up historical duplicates if the data has never been reviewed.

Back to blog