Every mid-sized business has a website form, a WhatsApp bot, an ads platform, and a spreadsheet all trying to feed the same CRM — usually with different field names for the same data. We fix that layer, so your reporting stops lying to your board.
| Field | Owner | Readers | Conflict rule |
|---|---|---|---|
| Website form | CRM, Mailchimp, WhatsApp | Website wins — validated at capture | |
| phone (E.164) | WhatsApp opt-in | CRM, calling app | WhatsApp wins — verified via OTP |
| lifecycle_stage | CRM (Sales) | Website (personalisation) | CRM wins always |
| last_touch_source | Ads / analytics | CRM report | Analytics wins — CRM read-only |
| deal_amount | CRM (Sales) | Finance | CRM wins — Finance reads |
| support_tier | Billing system | CRM, support desk | Billing wins — nightly sync |
New lead, form submit, WhatsApp opt-in — written to an `outbox` table in Postgres. Never straight to the CRM.
Reads outbox, calls CRM API, marks delivered on 2xx. On failure — retry with exponential backoff, up to 24h.
After 24h of failure it lands in a Slack channel a human can triage. Zero silent drops.
Because they're modelled as 'push all leads' instead of 'reconcile two systems'. The moment field mappings drift, duplicates appear, custom fields get renamed, or a webhook silently fails — the sync degrades. We build for reconciliation, not fire-and-forget.
Bi-directional sync is easy on the whiteboard, painful in production. These are the six decisions you make before the first record moves.
Tell us your goals — we'll recommend the right technology honestly, even if it isn't this one.