Integrate a customer relationship management system

Ask about this Page
Copy for LLM
View as Markdown

Synchronize Customer and Order data between commercetools and an external CRM, migrate existing records, and handle consent, deletion, and erasure.

Learn more about integrations in the self-paced Integration patterns module.

A customer relationship management system (CRM) stores customer profiles, segments, and interaction history so that marketing, sales, and support teams can act on them. This guide covers the flows that connect one to commercetools: an outbound export of Customer and Order changes, an inbound flow that applies CRM-mastered changes to Customers, and a one-time migration of existing records.

A CRM integration is asynchronous. Nothing in it runs on the checkout path, so a slow or unavailable CRM must never delay a registration, a Cart operation, or Order creation.

This guide does not cover authentication or identity providers. They decide how a shopper signs in rather than where their profile is mastered.

This guide also excludes B2B organization modeling. Account hierarchies belong to Model Business Units, while account, credit, and payment-term data mastered in an ERP belongs to Integrate ERP. For transactional email, see Integrate email. Exporting commerce data to a warehouse is also separate from CRM synchronization.
Before you start, you need a commercetools Project, a CRM with a documented API and test credentials, and an agreed owner for each data domain described in the next section. For the wider planning framework, see Integration planning and patterns.

Define CRM integration requirements

The Connector choice and the sync design are both downstream of these decisions. Record them before you configure or build anything.

Assign a source of truth per data domain

The system of record for customer data varies more across organizations than any other data type. Assign exactly one owner per domain and make the other side read-only for it. A field with two writers produces conflicts, update loops, and lost changes that are difficult to diagnose after the fact. For the wider decision, see Plan integrations.
Data domainTypical owner when CRM is presentDirection
Account credentials and sign-incommercetoolsNot synchronized
Customer profile and contact detailsCRM, or commercetools when no CRM masters themVaries
Addressescommercetools when they drive shipping and taxcommercetools to CRM
Marketing consent and communication preferencesCRMCRM to commercetools
Segments, lifecycle stage, and loyalty tierCRMCRM to commercetools
Customer Groups used for pricingcommercetoolsNot synchronized, or derived from CRM segments
Order capture and contentscommercetoolscommercetools to CRM
Order fulfillment lifecycleOMS or ERPOMS or ERP to commercetools
B2B accounts and hierarchiesERP, or commercetoolsVaries
Even when the CRM masters the profile, a Customer must exist in commercetools. It carries customer permissions, ownership of Carts and Orders, and eligibility for personalized promotions.
For the systems that own adjacent domains, see Integrate an order management system and Integrate ERP.

Keep credentials out of the synchronization entirely. Passwords are stored as hashes and cannot be copied between systems, which makes sign-in a migration problem rather than a sync problem.

A default suits most implementations: the CRM masters the profile, synchronization runs one way, migration runs as a separate job, and deletion propagates. Depart from it only for a stated reason. Bi-directional synchronization is the exception, not the starting point, and it requires field-level ownership on top of everything else in this guide.

Scope the sync flows

Answer and record the following questions because each one changes the design:

  • Which direction, and which resources? Customers only, or Customers and Orders. A commercetools Customer usually maps to a CRM contact or person, and an Order to a CRM deal, opportunity, or sales record.
  • Which changes trigger an outbound sync? Creation only, or every profile change, or specific changes such as an email address update. This decides whether you register a Change Subscription or a set of Message Subscriptions.
  • How does the CRM deliver changes inbound? A webhook it calls, or an endpoint you poll. A webhook maps to a service application. Polling maps to a job application.
  • Migration, ongoing sync, or both? Most implementations need both built and deployed separately.
  • What latency and volume must the integration support? Near-real-time delivery or an overnight batch, and the record counts that determine pagination and rate-limit handling.
  • How do the two record models map? Localized names, the commercetools address array against a flat contact schema, Customer Groups, and Custom Fields rarely map one to one.
  • What must happen on deletion? Whether an erasure request deletes or anonymizes the CRM record, and which other systems hold the same personal data.
  • Which consent flags apply? Marketing consent, double opt-in, and channel preferences, and which system records them.

Record special requirements

The list above covers the common shape. Capture additional constraints as separate requirements. Each one can move the decision in the next section from configuring a Connector toward forking or building one:

  • Multi-brand or multi-market contact separation. Customers can be assigned to Stores, and view_customers and manage_customers both have Store-scoped variants.
  • Segments or loyalty tiers that must drive pricing, which makes them Customer Group input rather than profile data.
  • B2B contacts that belong to a Business Unit as well as to a CRM account.
  • Data residency and compliance constraints that restrict the deployment Region.
  • An existing CRM contract or tenant that constrains the integration shape.
  • A customer data platform already sitting between commercetools and the CRM. Feeding one from commercetools is an export pipeline rather than a CRM sync, and is covered by Integrate an analytics destination.

Choose an integration path

Work through these options in order and stop at the first one that meets your requirements. Each later option is more to build and more to maintain.

Check for an existing CRM Connector

Check the current Connector inventory instead of relying on a remembered list, because published Connectors and their versions change. Use an API Client with the view_connectors scope to call the Search Connectors endpoint and filter by integration type:
Search for CRM Connectorshttp
GET https://connect.{region}.commercetools.com/connectors/search?integrationTypes=crm&integrationTypes=email&private=false
Query both crm and email because the IntegrationType enum separates customer relationship management from email and marketing, and marketing automation and customer data platforms are commonly listed under the latter. Set private to false so that the response covers all available Connectors rather than only those already assigned to your Project. For the host to use in each Region, see Hosts and authorization.

Clarify which system the business actually means before you evaluate candidates. Marketing automation, campaign, and personalization platforms are a different category from a system-of-record sales CRM, and the available Connectors differ between them.

Compare certification and access, source availability, installation requirements, and Region support using the canonical Connector representation. Record the Connector key and version you evaluated.
A listing in the technology partner directory is not automatically an installable Connector. For installable Public Connectors, use Connect in the Merchant Center. Confirm the deployment model of each candidate before you plan a Connect deployment. The vendor documentation and repository are authoritative for how an integration installs and what it covers.

Compare each candidate against your requirements rather than against its headline. Check the direction it supports, which resources it synchronizes, its field-mapping configuration, its consent and deletion handling, and support for your Region and volume.

Prove that a gap is not configuration

When a Connector matches your CRM but appears to be missing a behavior, confirm that configuration cannot close the gap before you consider forking it. Field mapping, Message selection, and consent handling are settings in most CRM Connectors rather than code.

Fork or build

If a Connector matches your CRM and has a genuine gap that configuration cannot close, and its source is available, fork it. Add only the difference and deploy the result as an Organization Connector.

If no Connector matches your CRM, build one. The Application templates overview documents the payment, product export, tax, and email templates, and none of them models customer synchronization. Scaffold the applications your direction requires with the Connect CLI instead:
Scaffold the applications a CRM integration needsbash
commercetools connect init my-crm-connector
commercetools connect application add --type event
Add a service or job application for the inbound flow, and a job application for migration. The scaffold supplies the application structure. Define the lifecycle scripts and Subscription registration, and implement the provider-specific envelope handling, CRM API calls, and record mapping.

Whichever option you choose, the design in the following sections is the same. Only the question of who implements it changes.

Look up the linked Customer

Correlate with a Query Predicate on the linking field. Pass the CRM identifier as an input variable rather than concatenating it into the predicate string, so that an identifier containing a quote or a backslash cannot alter the predicate:
Find the Customer linked to a CRM recordhttp
GET /{projectKey}/customers?where=externalId = :crmRecordId&var.crmRecordId=crm-84213&limit=2
Request a limit of two so that a duplicate is visible in the response rather than hidden behind the first result.
Customer Search indexes externalId as a keyword field and is the better fit for high-volume correlation, subject to two conditions. It is ID-first and returns Customer IDs only, so the handler still needs a Get Customer by ID call to read the version an update requires. It is also deactivated by default and deactivates again after 30 days without a search call, so an integration that searches only during a monthly batch can start returning a SearchNotReadyError without anything in the integration having changed. Call Check if Customer Search index exists before a batch run rather than assuming the index is there.

Design the sync flows

A CRM integration is a set of one-way flows that fail independently. This guide uses the standard Connect application types: an event application for the export, a service or job application for the inbound flow, and a job application for migration.

Export Customer changes to the CRM

The export reacts to a change in commercetools and pushes it to the CRM. Use an event application, which receives Messages through a Subscription and a Connect-provisioned message broker.

Subscribe to the smallest set of Messages that meets your requirements

A Change Subscription on the customer resource delivers ResourceCreated, ResourceUpdated, and ResourceDeleted for every change, which is the simplest way to keep a full profile in sync. Message Subscriptions target specific changes such as CustomerCreated or CustomerEmailChanged when only some changes should reach the CRM. Select the types you need from Customer Messages, and add OrderCreated from Cart and Order Messages when Orders map to CRM records. Acknowledge anything else without acting on it.
Exclude CustomerEmailTokenCreated and CustomerPasswordTokenCreated from any Message Subscription you register. Both carry the token itself in value when the token's validity is 60 minutes or less, which moves an account-recovery credential into a system that was never scoped to hold one. Decide per field what the CRM receives instead of forwarding a resource unchanged.

Retrieve the Customer instead of reading the Message payload

Retrieve the Customer by resource.id. Subscriptions deliver at least once with no ordering guarantee, so a payload can describe an older state than the Customer currently has, and an out-of-order delivery would otherwise overwrite a newer profile with an older one. Retrieving the resource makes the export converge on the current state. When strict ordering matters, the same page documents how to order deliveries using resource.id with sequenceNumber for Messages, or with version and oldVersion otherwise.

Decode the transport envelope before you read anything from it. Connect provisions either Google Cloud Pub/Sub or Amazon SNS as the destination, and each provider uses a different transport envelope. Decode it according to the provider's documented message format. The decoded body is either PlatformFormat or CloudEventsFormat depending on the Subscription configuration. Read the Message type and the resource identifier from whichever combination you receive, and validate the type before acting so that platform test notifications are acknowledged rather than processed.

Do not re-export your own write-back

Writing the link back is itself a Customer change. It emits CustomerExternalIdSet, and a Change Subscription on the customer resource delivers the corresponding ResourceUpdated notification to the same handler that caused it. Every new Customer is then exported twice, and export volume climbs with no business cause.

This happens in a one-way design, so guard against it even when nothing writes Customers inbound. Which guard is available depends on the Subscription you registered. A Change Subscription delivery carries no Message type, so the only guard is comparing the retrieved Customer against what the CRM already holds and exiting when the link is set and nothing else has changed. Type-based filtering, where the handler ignores the Message types its own writes produce, requires Message Subscriptions.

If an inbound flow also writes Customers, the same guard prevents the larger failure: an inbound write raises a change that the export pushes back to the CRM, which the CRM then reports as a change, without a natural stopping point.

Acknowledge deliberately

Respond with a status that the message queue accepts as an acknowledgment. Other responses are retried according to the retention and backoff rules in Event application behavior.
The Subscription that feeds the queue has its own, shorter delivery retry window, which depends on the SubscriptionHealthStatus. Negative acknowledgment is therefore a short-term recovery path rather than a substitute for reconciliation. Set up an automatic alert on the health status, and size your recovery plan against the documented Subscription delivery window rather than the message queue retention period.
Map each outcome to a status code on purpose. Integrate an order management system gives the full outcome-to-status mapping, and it applies here unchanged: acknowledge a Message you synchronized, a type you do not handle, an envelope that can never be decoded, and a record that permanently cannot be synchronized, and withhold acknowledgment only for a transient CRM or network failure.
Returning 200 for every error is the opposite failure: a CRM outage is acknowledged, the Messages are discarded, and the profiles silently never arrive.

Apply inbound CRM updates

Inbound updates arrive from the CRM. Use a service application as an authenticated webhook when the CRM can push changes, or a job application when you must poll for deltas.
Do not implement inbound updates as an API Extension. An API Extension runs synchronously inside a commercetools API request, so CRM latency or an outage would delay or block the operations the shopper is waiting on. In webhook mode no API Extension is registered, the default two-second API Extension response limit does not apply, and the five-minute Connect service timeout applies instead.

Authenticate every request and validate the payload

The endpoint is reachable from the internet and writes personal data to your Project. Verify a webhook signature, a shared secret, or a fully validated token including signature, issuer, audience, and expiry, on every processing route. Leave only a liveness route unauthenticated. Keep the secret in secured configuration.

Poll the CRM for inbound changes

When the CRM cannot send webhooks, use a job application to request records changed after a durable cursor. Store the cursor outside the application process, such as in a Custom Object, because a later run can start on another application instance.

Read pages in a stable order. If multiple records can have the same modification time, include the CRM record identifier in the cursor as a tie-breaker. Apply each record with the same creation policy, correlation, and version-conflict handling as the webhook flow. Save the next cursor only after every record in the page succeeds. If a transient failure interrupts the page, leave the previous cursor in place so the next run safely reprocesses it.

Take a durable lock before reading the cursor so that two scheduled runs cannot process the same window. Give the lock a time to live longer than the maximum run time so a failed run does not block every later run. Stop with enough time to save progress before the documented Job application timeout.
Treat an invalid record according to a recorded failure policy. Either stop the page without advancing the cursor, or record the item for correction and replay before continuing. Retry CRM 429 and 5xx responses with exponential backoff. Test that the cursor advances after a complete page, remains unchanged after an interrupted page, and that two overlapping runs cannot process the same window.

Govern who can edit CRM-owned fields

A Custom Field cannot be marked read-only. A FieldDefinition sets the data type, label, whether a value is required, and an input hint, and none of those restrict writes. Ownership is a governance decision that you enforce outside the data model.
Enforce it in three places. Keep CRM-mastered values out of the update actions your storefront and backend send. Grant manage_customers only to the API Clients that need it, and use the Store-scoped variant where it applies. Restrict Customer editing in the Merchant Center to the teams responsible for it. Merchant Center permissions do not constrain API Clients, so the first two places carry the enforcement.

Migrate existing customer records

A one-time backfill and an ongoing delta sync need different pagination, throughput, and error handling. Build them as separate applications and deploy the migration job independently.

Plan for passwords before you plan the records

Password hashes cannot be copied into commercetools, so migrating profiles does not migrate sign-in. Import customers sets out the two options, a gradual migration that rehashes each password on first sign-in and a forced password reset after the migration, and what each one requires. Choose between them before you scope the record mapping, because the gradual path adds a Custom Type to every Customer you migrate.

Checkpoint, serialize, and respect rate limits

Follow the documented Job application behavior and resource recommendations. The documented runtime and resource limits make checkpointing mandatory rather than optional: a full backfill of a large customer base might not finish in one run, so it has to survive being stopped and resumed. Checkpoint your cursor after each page, stop with a margin before the timeout, and orchestrate heavy processing outside the container rather than performing it inside the job.

Guard against overlapping scheduled runs with a durable lock whose time to live is longer than the maximum run time. A crashed run then eventually releases the lock.

Email is the mandatory unique identifier of a Customer, and creating two Customers with the same email address at the same time can return a LockedField error. Partition the work so that a single email address is only ever processed by one worker, or serialize creation, rather than fanning out a migration and retrying the failures. Uniqueness depends on scope: a global Customer is unique across the Project, while Store-specific Customers can reuse an email address in another Store. In a multi-Store design the link field, not the email address, is the only safe correlation key.
CRMs rate-limit aggressively. Give every outbound call a timeout, retry 429 and 5xx responses with exponential backoff, and use the CRM batch endpoints for bulk work. Cleanse and validate records on the way through, because a migration is the cheapest point at which to reject data that would fail later.

Every unit of work must be idempotent, because a resumed run reprocesses the page it was working on. Share the mapping and the upsert with the ongoing sync rather than writing a second implementation that can drift from the first.

Propagate deletion to the CRM

Deleting a Customer produces the CustomerDeleted Message, and a Change Subscription on the customer resource delivers a ResourceDeleted notification. Handle it in the export and delete or anonymize the counterpart CRM record. A deletion that stops at the commercetools boundary leaves personal data in a downstream system that no longer has a reason to hold it.
Deleting a Customer does not delete their Carts. Carts are preserved when a Customer is deleted, so include them when you scope what an erasure request has to reach.

Erase personal data on request

A standard DELETE request leaves personal data that is part of Messages and in internal logs. Setting the dataErasure query parameter to true removes that data as well.
Delete a Customer and erase the related personal datahttp
DELETE /{projectKey}/customers/{id}?version=3&dataErasure=true
Erasure spans more than the Customer. GDPR compliance lists every resource that can hold personal data, the endpoints that accept the parameter, and the retrievals and deletions each one needs.
Drive the CRM side from the erasure itself rather than from a separate workflow. A ResourceDeleted delivery carries a dataErasure field recording whether the DELETE request set the parameter to true, so one handler can distinguish an ordinary deletion from an erasure request and apply the stricter treatment to the CRM record. This document describes platform behavior and is not legal advice.

Configure and deploy the Connector

Grant only the permissions the enabled flows use

Declare the scopes in connect.yaml and let Connect provision a least-privilege API Client rather than supplying credentials by hand. An outbound export needs view_customers, view_orders when it syncs Orders, and manage_subscriptions so that its postDeploy script can register the Subscriptions. An inbound flow needs manage_customers, and manage_types when it creates the Custom Type for CRM-mastered fields. A polling app also needs manage_key_value_documents when it stores its cursor or lock in Custom Objects. Grant each scope only to the application that uses it.
Subscription access comes from manage_subscriptions, which covers both reading and writing. Declare only scopes that appear in the scopes reference, because a token request naming a scope the API Client does not hold returns a 400 invalid_scope error at authentication rather than a permission error at the failing call.

Separate configuration from secrets

The CRM API token or OAuth client secret belongs in securedConfiguration. Base URLs, Region, sandbox toggles, page sizes, and which Messages to synchronize belong in standardConfiguration. Place connect.yaml in the root of the Connect application and use only documented envelope keys, as described in Connect development.
Mount each application's router at the endpoint base path declared for it in connect.yaml. A mismatch means platform traffic reaches an application that returns 404, which reads as a delivery failure rather than a routing mistake.

Register the Subscriptions and Types idempotently

Register the Subscriptions and any Custom Type the integration relies on in the postDeploy script, and remove them in preUndeploy. Declare both in the scripts block of connect.yaml, as shown in Automation scripts. Create each resource when it is absent and update it in place rather than deleting and recreating it. Confirm the resources exist rather than inferring success from the Deployment status, using Deployment logs.

Verify the round trip

Prove the flows against a test Project and a CRM sandbox before the first production run. The mapping is a pure transformation from a resource to a CRM record, so test it with fixtures and no credentials, and assert consent handling and the exclusion of token-bearing fields there.

Then prove the outcomes that only a deployed integration produces:

  • A new Customer produces a counterpart CRM record, and the link is written back. A record that appears with no link indicates that the write-back step is missing, and the next change creates a duplicate.
  • Updating one field produces exactly one write per direction, and no second record. Watch the write count settle rather than checking only that the value arrived.
  • Redelivering the same Message changes nothing. Post the encoded envelope to the application a second time and confirm the CRM record is untouched.
  • Creating a Customer produces one export, not two. A second export of the same Customer means the write-back is re-entering the handler.
  • A CRM-originated change reaches commercetools and does not bounce back to the CRM.
  • A CRM record with no linked Customer follows the selected creation policy: it either creates one linked Customer or produces a permanent replay record. A second delivery must not create another Customer.
  • A polling run advances its cursor only after a complete page. Interrupt a page, run the job again, and confirm that it reprocesses the page without duplicating changes. Start two runs together and confirm that only the lock holder processes the window.
  • Deleting a Customer deletes or anonymizes the CRM record, and no personal data remains in the logs of either system.
  • The migration job resumes from its checkpoint after an interrupted run instead of reloading everything.

Some expected behavior looks like failure while testing. A CRM sandbox can cap records, expire data, or accept a write and return success without persisting it, so verify the contract against the sandbox and verify real persistence against a controlled full account. Clean up test records afterward so they do not reach CRM reporting or campaigns.

Troubleshoot observable symptoms

SymptomLikely causeResolution
Duplicate contacts in the CRMThe handler creates on every Message, or the link was never written backLook the record up by the link before creating it, and write the identifier back in the same run
Duplicate Customers in commercetoolsThe inbound flow creates without first checking the link, or concurrent deliveries both observe zero matchesApply the selected zero-match policy once, serialize creation by the CRM identifier, and reject multiple matches
Every new Customer is exported twiceThe write-back emits a Customer change that the export Subscription redeliversExit early when the link is already set, or use Message Subscriptions and ignore the types your own writes produce
Each inbound write triggers an outbound writeA bi-directional design with no self-change filterMove to one-way synchronization, or assert the filter with a test
The same Message is redelivered indefinitelyThe response is outside the acknowledgment setReturn 102, 200, 201, 202, or 204 for handled and irrelevant Messages
Customers stop reaching the CRM with no errorsEvery failure is acknowledged with 200, so transient failures are discardedReturn a non-acknowledgment status for transient failures only
Notifications stop arriving after a long CRM outageSubscription delivery is retried for up to 48 hours on a TemporaryError, and for a shorter window on a ConfigurationError, after which notifications can be droppedAlert on the Subscription health status, and reconcile the gap from commercetools rather than waiting for redelivery
The handler crashes on the incoming bodyThe transport envelope is not decoded, or the code assumes the wrong destination typeDecode the provider's transport envelope, then validate the Message type
A migration stops landing recordsCRM rate limiting rather than a logic faultAdd exponential backoff on 429, use batch endpoints, and confirm the job resumed from its checkpoint
A migration worker fails with LockedFieldTwo workers created Customers with the same email address at the same timePartition or serialize by email address
The polling job skips CRM changes after a failureThe cursor advanced before every record in the page succeededSave the cursor only after the page completes, then replay the interrupted page
Two polling runs apply the same window concurrentlyScheduled runs overlapped without a durable lockAcquire a lock with a time to live before reading the cursor
Correlation fails with SearchNotReadyErrorCustomer Search is deactivated by default, and deactivates after 30 days without a search callReactivate it, and check the index before a batch run
Platform traffic returns 404The router is not mounted at the endpoint declared in connect.yamlAlign the route with the declared base path
No Subscription exists after deploymentThe postDeploy script is not declared in the scripts blockDeclare it, redeploy, and confirm the Subscription exists
Storefront edits overwrite CRM-mastered fieldsCustom Fields carry no read-only enforcementRemove those fields from your write paths and narrow the API Client scopes