building crm connect: event queues, retries and never failing silently.

How we built a Shopify-to-Salesforce connector that queues every event, retries on its own, writes CRM IDs back, and always says why something hasn't synced.

  • BUILD LOG
  • 6 min read
  • by lead.data

CRM Connect moves orders, customers, fulfilments, returns and delivery updates from Shopify into Salesforce, through the MuleSoft layer a merchant already runs. It is launching oct 2026. This is a build log: the decisions that shaped it, and the one rule we kept coming back to.

The rule: an integration is allowed to be slow, and it is allowed to retry, but it is never allowed to fail silently. Every CRM integration we have been asked to rescue failed the same way. Something stopped syncing, nobody knew, and the CRM team found out weeks later when the numbers did not add up.

where it started

The starting point was a familiar one for multi-brand merchants: several brand-specific connectors, each with its own codebase and its own hard-coded business rules. Each did roughly the same job slightly differently. A fix in one did not reach the others.

So the first design decision was the biggest one: one multi-tenant app, with the rules that used to live in code turned into per-store settings. Which events sync, how payment gateways and tax sources map, how tender is reported, which CRM connection an order goes to by shipping country, which store location maps to which CRM store code. All of it is configuration. One install serves every brand, and each store keeps its own setup.

capture, transform, queue, deliver

The pipeline has four stages, and each one is separate on purpose.

  1. Capture. Shopify webhooks arrive for orders, customers, fulfilments, refunds and returns. Each is verified for authenticity, then deduplicated, because webhooks are delivered at least once and the same event can arrive more than once.
  2. Transform. The event is mapped into the payload contract the merchant’s MuleSoft layer already expects. Nothing in Salesforce has to change.
  3. Queue. The transformed payload becomes a job: a record with a status, an attempt count and a correlation ID.
  4. Deliver. A dedicated worker picks up jobs and calls the MuleSoft endpoint. It runs separately from the web interface, alongside a scheduler, so a busy admin never slows delivery and a slow CRM never slows the admin.

The webhook handler does as little as possible. It verifies, deduplicates, records and returns. Everything that can fail slowly happens in the worker.

// Webhook handler: acknowledge fast, do the work later.
export async function onWebhook(req) {
  const event = await verifyAndParse(req);          // reject anything unsigned
  if (await alreadySeen(event.webhookId)) return ok();
  await enqueue({
    tenant: event.shopDomain,
    topic: event.topic,
    payload: event.body,
    correlationId: newCorrelationId(),
  });
  return ok();
}

retries, with a ceiling

When a delivery fails, the job is retried automatically with backoff. The wait grows with each attempt, so a CRM that is briefly down is not hammered, and a short outage resolves itself without anyone touching it.

Retries have a ceiling. After the last attempt, the job is marked failed and shows up on the dashboard with the full request, the full response and the error. From there, the merchant’s own team can re-run it with one click once the cause is fixed. They do not need a developer, and they do not need to ask us.

waiting is not failing

The decision we are most pleased with is small. Some events are not ready to send. An order awaiting payment, for example, should not go to the CRM as a confirmed sale. Cross-border orders can need an identifier from the cross-border provider before they are complete.

The easy implementation drops these, or fails them, or retries them until they give up. All three look the same to an operator: something is missing and nobody knows why.

Instead, these jobs sit in a waiting state with a stated reason. The dashboard says “waiting: awaiting payment”, not “failed” and not nothing. When the condition clears, the job moves on. An operator looking at a single order can see exactly where it is and why.

traceability, both ways

Every successful sync writes the Salesforce IDs it created back to the store as metafields. A record in Shopify links directly to its record in the CRM. When someone asks “did this order reach Salesforce?”, the answer is on the order.

The order history view shows every sync for a single order, with its attempts, its correlation ID and the CRM IDs it produced. The activity log shows each request and response, with customer data masked by default. Support can see what was sent and what came back without seeing personal data they do not need.

security decisions

Because one install serves several brands, tenant isolation had to be structural, not careful. The active tenant is always taken from the verified session, never from a value the caller supplies. Credentials for each store’s MuleSoft connection are encrypted at rest in a secret store and never shown in the interface. Traffic to the CRM and the database is encrypted in transit. Erasure and export requests remove or return real customer data, and uninstalling cleans up.

modules, not forks

Not every brand needs every feature. Courier tracking for regional couriers, fulfilment routing, re-delivery handling with customer emails, cross-border order gating, Shopify Returns sync and storefront forms for newsletter, contact and track-my-order are all modules, switched on per store. Adding a capability for one brand does not mean a new codebase for that brand. That was the whole point.

what should anyone building a crm connector get right?

  • Acknowledge webhooks fast and do the work in a queue.
  • Deduplicate on the way in, and make every write idempotent anyway.
  • Give every job an ID a human can search for.
  • Retry with backoff and a ceiling, then surface the failure with its full context.
  • Make “not ready yet” a visible state with a reason.
  • Put the retry button in the merchant’s hands.

None of this is novel. It is just rarely all there at once. If you run Salesforce alongside Shopify Plus and want to talk through a rollout, scope it with an engineer.