Export commerce data from commercetools to a data warehouse, customer data platform, or product analytics tool, and keep that copy correct as the data changes.
This guide covers the export pipeline that moves commerce data out of commercetools so that another system can report on it. It starts with deciding which kind of analytics you need, moves through choosing a destination and an integration path, and ends with the two applications that keep the destination data synchronized with your Project.
Data flows in one direction. commercetools is the source of truth, and the destination holds a copy shaped for querying. commercetools never reads back from the destination. This design runs the pipeline asynchronously outside the checkout path, so an outage produces stale reports rather than a broken checkout.
Before you start, you need the following:
- a commercetools Project with the data you want to export, and an API Client that can read it
- an account and ingestion credentials for the destination
- somewhere to run the integration, such as Connect, a cloud function, or your own infrastructure
- an agreed answer to each question in Identify requirements that shape the pipeline
Define analytics export requirements
The destination, the integration path, and the pipeline design depend on these decisions. Record them before you configure or build anything.
Confirm you need a commerce data export
Three unrelated things are called analytics, and they route to different products. Confirm which one your requirement is before you read further, because only the first is an export pipeline.
| What you want | Where it comes from | This guide |
|---|---|---|
| Transactional and state truth for reporting, such as Orders, Customers, Payments, Inventory, and catalog | An export pipeline you own, built on Subscriptions and API queries. | Covered here |
| API latency, error rates, request volume, and server-side request logs | Platform Insights, which forwards telemetry to an application performance monitoring provider. See Set up Platform Insights. | Not covered |
| A governance record of who changed what and when | Change History, a compliance log on a separate host. | Not covered |
The Change History boundary and the client-side tracking boundary are worth stating plainly, because both mistakes are expensive to reverse.
Retry-After header. The documentation also advises you to avoid making API calls in response to an event stream or message subscription. Audit Log Basic records only changes that originate in the Merchant Center, so changes made through the HTTP API and the Import API are absent unless you have the Audit Log Premium Add-On. A pipeline built on Change History is both throttled and incomplete.Client-side behavioral tracking is a storefront concern. Page views, clicks, and consent-managed pixels belong in your storefront with a tag manager, not in this pipeline. An export can feed the server-side ingestion endpoint of the same vendor, but it cannot produce browser events.
Identify requirements that shape the pipeline
Answer and record the following, because each answer changes the design.
| Question | Design impact |
|---|---|
| Which destination, and which category? | Decides the ingestion mechanism and whether an existing integration might cover you. See Choose the destination category. |
| Which data domains and which fields? | Decides the Messages you subscribe to, the resources the backfill queries, and the read scopes you grant. |
| Does the destination need near-real-time data, periodic loads, or both? | Decides which applications exist. Most pipelines need both. |
| Is a one-time historical load required? | A historical load is a separate run from the ongoing delta, even though it shares the code path. |
| What is the destination schema grain? | One row per change in an append-only table, or one current-state row per resource. This decides the transform and the merge key. |
| What volume and change rate must it handle? | Sets page sizes, backoff, run duration, and whether the Subscription budget constrains the design. |
| Which exported fields are personal data? | Decides field minimization, and whether erasure has to propagate to the destination. |
Capture anything that does not fit these rows as its own requirement rather than forcing it into one. Each of the following can move the decision in the next section:
- consolidating several Projects or Regions into one destination
- data residency rules that restrict where the destination and the runtime may sit
- an existing extract, transform, and load tool or customer data platform that can already read the commercetools API
- a retention or erasure policy that the destination must honor
- near-real-time personalization built on the same feed, which raises the latency requirement
Unless a requirement says otherwise, the design that follows is the default: a streaming application for freshness, a scheduled job for history and gap repair, read-only scopes, destination credentials in secured configuration, and deduplication on the destination side.
Choose the destination category
The category decides the mechanism. Vendor documentation is authoritative for ingestion endpoints, field names, and limits.
| Category | Examples | Mechanism | What flows |
|---|---|---|---|
| Data warehouse | BigQuery, Snowflake, Redshift, Databricks | Stream and batch. This is the default fit. | All transactional and state data, modeled downstream |
| Customer data platform | Segment, mParticle, RudderStack, Tealium | Stream. Check first whether the platform has its own commercetools source. | A curated set of Customer and Order events keyed to a stable identifier |
| Product analytics | Amplitude, Mixpanel, GA4, Snowplow | Stream a small set of conversion events into the server-side ingestion API. | Conversion and behavioral events, not Orders as rows |
| Business intelligence | Looker, Tableau, Power BI | None. These read the warehouse. | Nothing directly from commercetools |
Two of these rows carry a decision that is easy to get wrong.
Business intelligence tools do not integrate with commercetools directly. Point the pipeline at a warehouse and let the reporting tool read modeled tables from there. Building an extract for a reporting tool alone produces a pipeline with no history and no place to model.
Product analytics tools that are designed for browser instrumentation, such as GA4 and Mixpanel, expose a narrower server-side ingestion API than their browser SDK. Confirm what each one accepts before you treat it as a substitute for a warehouse. If the requirement is complete history and modeling, choose a warehouse and treat the analytics tool as an additional destination rather than the only one.
Choose an integration path
Work through these options in order and stop at the first one that meets your requirements.
Check for an existing analytics Connector
view_connectors scope to call the Search Connectors endpoint and filter on the analytics integration type:GET https://connect.{region}.commercetools.com/connectors/search?integrationTypes=analytics&private=false
private=false so the response covers all available Connectors rather than only the ones already assigned to your Project. For the host to use in each Region, see Hosts and authorization. Search for your destination by name as well, because a Connector that writes to a warehouse may be registered under a different IntegrationType.Record the Connector key and version you evaluated, or record that none exists. Plan for a custom build until the search identifies an option that meets your requirements, and record that assumption in the effort estimate.
Two adjacent options exist and neither is a Connect deployment:
- A customer data platform may ship its own commercetools source, configured in that vendor's console. If it covers your domains, use it. Nothing in this guide's build and deploy sections applies.
- An extract, transform, and load tool that already reads the commercetools API may be the cheapest path if your organization runs one. It is a valid alternative, and it is not built or deployed through Connect.
Prove that a gap is not configuration
When a Connector matches your destination but appears to be missing a behavior, confirm that configuration cannot close the gap before you consider forking it. Which Messages flow, how fields map to destination columns, and which table or dataset receives them are usually configuration.
Fork or build
If a Connector matches your destination 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.
product-ingestion.commercetools connect init my-analytics-connector --template product-ingestion
- The destination calls themselves. The
saveProductsandremoveProductsfunctions in both applications log a count and return. No destination client exists. - A scheduled backfill. The template has no
jobapplication, nolastModifiedAtwindow, and no checkpoint. The full export is aserviceapplication that runs on demand. - Your own resources and Messages. The template exports Products from a single Store and subscribes to Product, Product Selection, and Store changes. An analytics export usually needs Orders, Customers, and Payments instead.
- Authentication on the full export endpoint. It accepts unauthenticated requests that trigger a complete export.
- Idempotent Subscription registration. The
postDeployscript deletes the Subscription and creates a new one, so every change emitted between the two calls is lost. - Least-privilege permissions. The template expects a pre-created API Client supplied through secured configuration rather than declaring
inheritAs.apiClient.scopes. - Bounded memory use. The full export collects the entire result set in memory before writing anything.
- A non-destructive rebuild. The full export removes the existing records before it writes the new ones, which leaves the destination empty for the length of the run.
- Meaningful tests. The suite asserts only that a response object exists.
Whichever option you choose, the pipeline design in the next section is the same.
Design the export pipeline
An analytics export is made of two applications that write to the same destination. The streaming application keeps the destination fresh. The scheduled job loads history and repairs whatever the stream missed. Both produce the same rows, so both must be safe to run more than once.
Stream changes as they happen
event application, which receives Messages through a Subscription and a Connect-provisioned message broker.- Retrieve the Order instead of reading the Message payload, which covers at-least-once delivery, the absence of an ordering guarantee, and the omitted payload that arrives as PayloadNotIncluded when a Message exceeds the queue size limit or its format is not accepted by the messaging service.
- Acknowledge deliberately, which covers the status codes the message queue treats as an acknowledgment, the seven-day retention of unacknowledged messages, and the outcome-to-status-code mapping.
Three decisions are specific to an export.
| Message | Emitted when |
|---|---|
| OrderCreated | The initial Order is created directly from a Cart. |
| OrderCreatedFromRecurringOrder | A Recurring Order produces an Order on its schedule. |
| OrderImported | An Order is imported. |
50 Subscriptions. This is a soft limit that can be raised after a performance impact review, but a design with one Subscription per Message type reaches it sooner than a design that fans out inside the handler.sequenceNumber alongside version and resourceVersion. A change notification carries version, and on an update also oldVersion, but no sequenceNumber.Backfill and repair on a schedule
The scheduled job loads history and repairs what the stream missed. Query resources to load data that predates the Subscription because Message history has limited retention and is not enabled by default.
job application and follow the documented Job application behavior and resource recommendations. The application request times out after 30 minutes, and the CPU and memory ceilings are low enough that the job should stream pages to the destination rather than assemble a dataset in memory. For a large historical load, chunk the run and let successive runs resume from the stored position.Window on lastModifiedAt and page with an identifier cursor
:windowEnd for the whole run, and query the half-open window between the last checkpoint and that value. Then page through the window on the resource identifier:GET /{projectKey}/orders?withTotal=false&sort=id asc&limit=100&where=lastModifiedAt >= :cursor and lastModifiedAt < :windowEnd
GET /{projectKey}/orders?withTotal=false&sort=id asc&limit=100&where=lastModifiedAt >= :cursor and lastModifiedAt < :windowEnd and id > :lastId
var. prefix, such as var.cursor=2026-09-01T00:00:00.000Z&var.windowEnd=2026-09-02T00:00:00.000Z, rather than interpolating them into the predicate string. Repeat while the number of returned results equals the requested limit. The maximum limit is 500. The examples show the predicates unencoded for readability. URL-encode them in the request.id > :lastId, so that run never exports it. Holding :windowEnd fixed puts the change outside the current window instead, where the next run collects it.10 000, so a window with more results than that leaves the export incomplete, and a resource modified mid-run shifts every later page. The identifier cursor is stable and resumable. For the full pattern, see Iterate over all elements.Checkpoint the window
Keep two values, because they do different jobs.
:windowEnd and the identifier of the last row written in a Custom Object or in the destination itself, and save them after each page. A run that crashes or runs out of time then resumes inside the same window instead of restarting it.:windowEnd only once the window is exhausted. Advancing it to the lastModifiedAt of the last row written moves it to a value chosen by identifier order rather than by time, which can step over changes the window never reached.Start the next window slightly before the stored cursor rather than exactly on it. Deduplication on the destination absorbs the repeated rows, and the overlap covers resources whose modification time falls near a boundary.
The Connect documentation does not guarantee that scheduled runs cannot overlap, so take a durable lock with a time to live longer than the maximum run time. A crashed run then eventually releases the lock.
Do not poll the Messages API for history
Map resources onto the destination schema
The transform is where the value of the pipeline sits. Keep it a pure function with no network calls, so you can test it without a deployment.
Decide the grain first, because it determines everything else:
- One row per change. The destination holds an append-only record of what happened, and modeling happens downstream. This suits a warehouse and preserves history.
- One current-state row per resource. The destination holds the latest version of each resource, merged on its identifier. This suits a dashboard that only reports on what is true now, and it discards history.
Three shapes rarely map onto a flat table without a decision:
- Localized strings. A LocalizedString holds one value per locale. Flatten the locales you report on into columns, or keep the whole object in a semi-structured column.
- Money. CentPrecisionMoney carries
centAmount,currencyCode, andfractionDigits. Carry all three. AcentAmountalone is not comparable across currencies, and dividing it by 100 is wrong for currencies with a different exponent. - Nested collections. Line Items, addresses, and Custom Fields are arrays. Either explode them into a child table keyed to the parent, or keep them as a semi-structured column and model them later.
resource.id, the version or sequence value, and lastModifiedAt as columns on every row. Without them the destination cannot deduplicate, resolve a late arrival, or tell you when the copy was last correct.Deduplicate on the destination side
id, a version, and a lastModifiedAt value. That gives you one merge key for both:- Append-only tables. Make the primary key
idplusversion. A repeated write is then a no-op rather than a second row. For a raw event table fed only by the stream,resource.idplussequenceNumberfrom the notification works as well, andsequenceNumberhas the advantage of being sequential per resource. - Current-state tables. Merge on
idand keep the row with the highestversion. Aversionvalue is not sequential, but it is comparable for the same resource, so a late arrival that carries a lowerversionis discarded instead of overwriting newer data.
Use the same key in both applications. If the stream and the backfill key their rows differently, every backfill run duplicates rows the stream already wrote.
Propagate deletion and minimize personal data
Orders and Customers carry personal data, and an analytics destination is a common place for it to outlive its purpose. Treat the destination as a data processor and design for erasure before the first load:
- Export only the fields the reports need. A pipeline that copies whole Customer profiles by default takes on a data-protection obligation that the reports never required.
- Subscribe to deletion. CustomerDeleted is emitted when a Customer is deleted. Handle it by deleting or anonymizing the matching rows in the destination. A pipeline that only ever writes leaves orphaned personal data behind after an erasure request is satisfied in commercetools.
- Account for deletion in the backfill too. A query that windows on
lastModifiedAtreturns resources that still exist. A resource deleted since the last run does not appear in any result, so the backfill alone never removes anything. Deletion has to come from the stream, or from a periodic reconciliation of identifiers. - Keep personal data and credentials out of logs. Log identifiers, decisions, and counts. Log a correlation key on every line, such as
resource.idwithsequenceNumber, so a run can be traced without recording its contents.
Configure and deploy the Connector
Configure the applications, permissions, and Subscription as one contract.
Grant only the scopes the flows use
inheritAs.apiClient.scopes so Connect generates a scoped API Client at installation, instead of asking an installer to supply credentials. The API Client that creates the Deployment needs manage_connectors_deployments, and it also needs manage_api_clients because the Connector generates its credentials automatically. See Hosts and authorization and Modify a Connector.manage_subscriptions for the registration script. Grant nothing more. See the canonical API scopes reference for the full list.inheritAs:
apiClient:
scopes:
- manage_subscriptions
- view_orders
- view_customers
configuration:
standardConfiguration:
- key: DESTINATION_TARGET
description: Destination dataset, table, or source identifier
securedConfiguration:
- key: DESTINATION_CREDENTIALS
description: Destination ingestion credential
manage_subscriptions grants permission to view, create, update, and delete Subscriptions, so the registration script needs no separate read scope. Confirm every scope you declare against the API scopes reference: requesting a scope the API Client does not have returns a 400 invalid_scope error at runtime.Separate configuration from secrets
connect.yaml configuration rules. Destination credentials belong in securedConfiguration. Never place them in code or logs.event application and the backfill as a job application with a schedule:deployAs:
- name: analytics-streamer
applicationType: event
endpoint: /analyticsStreamer
scripts:
postDeploy: npm ci --omit=dev && npm run connector:post-deploy
preUndeploy: npm ci --omit=dev && npm run connector:pre-undeploy
- name: analytics-backfill
applicationType: job
endpoint: /analyticsBackfill
properties:
schedule: "17 2 * * *"
Run the one-time historical load through the same job before you let the schedule take over, so the code path you depend on for recovery is the one you exercise routinely.
Register the Subscription idempotently
postDeploy and remove it in preUndeploy. Declare both in the scripts block of connect.yaml, because Connect runs only the scripts declared there. A script file that exists in the repository but is not declared never runs.Read the Subscription first and create it only when it is absent, updating it in place otherwise. A registration routine that deletes the Subscription and recreates it drops every notification emitted during the gap, and those changes reach the destination only if a later backfill happens to cover them.
CONNECT_SUBSCRIPTION_DESTINATION to identify SNS or GoogleCloudPubSub, then use CONNECT_AWS_TOPIC_ARN or CONNECT_GCP_TOPIC_NAME for the active deployment Region. See Automation scripts for the Event variables.Creating, updating, or deleting a Subscription can take up to one minute to take effect. commercetools also sends a test notification to the Destination when a Subscription is created, so the handler must acknowledge a notification that carries no resource change.
Verify the export
Add automated tests before you deploy. Mock both the commercetools API and the destination, and assert on what the code decided to write:
- The transform is a pure function that handles localized strings, money, and nested collections for a representative resource.
- Each subscribed Message type routes to the intended handler, and unhandled types are acknowledged without a destination call. A handler fed by a Change Subscription instead routes on the resource type, because the notification carries no Message type.
- The streaming application retrieves the resource by identifier and never reads the Message payload, including when the payload is omitted.
- Every row carries the deduplication key, and a redelivered notification produces one row rather than two.
- A transient destination failure returns a status outside the acknowledgment set, and a terminal failure is acknowledged and recorded for replay.
- The backfill pages with an identifier cursor, advances the checkpoint after each page, and resumes from the checkpoint on the next run.
- A backfill run over a window that the stream already covered adds no rows.
- A deletion removes or anonymizes the matching rows in the destination.
postDeploycreates one Subscription, a repeated deployment does not duplicate it, andpreUndeployremoves it.
After deployment, trace a real change end to end:
- Confirm that one Subscription exists with the expected key, destination, and Message types.
- Make a representative change, such as placing an Order or editing a Customer, and confirm that the row appears in the destination with the mapped fields correct.
- Confirm that the row carries the deduplication key.
- Redeliver the same notification and confirm that the destination still holds one row. To do this locally without a broker, post the encoded envelope to the application directly. See Test an event application locally.
- Run the backfill over a known window, then run it again over the same window, and confirm that the second run adds no rows and that the checkpoint advanced.
- Confirm that the backfill reached the end of the window rather than stopping at the offset ceiling.
- Delete a test Customer and confirm that the destination rows are removed or anonymized.
- Make the destination fail temporarily and confirm that the work is retried rather than lost.
Some correct behavior looks like failure while you test. Confirm the deployment environment and the elapsed time before you treat any of these as a defect:
- Subscription delivery has no guaranteed time frame, and delays of several minutes happen. A row that has not appeared yet is not evidence of a dropped notification.
- A default
sandboxdeployment scales to zero and needs time to boot again, which can exceed the event acknowledgment deadline. Early notifications are redelivered until the application is warm. - Duplicate rows during a deliberate redelivery test are the expected input to deduplication. Judge the result after the merge, not before it.
In production, monitor Subscription health, acknowledgment failures, destination ingestion errors, backfill duration against the job timeout, checkpoint lag, and the row count each run writes. Define how an operator replays a window after a fix, and record it alongside the Connector.
Troubleshoot observable symptoms
Match the symptom you observe to its likely cause and resolution:
| Symptom | Likely causes | Checks and resolution |
|---|---|---|
| No rows arrive at all | The Subscription is missing. The scripts block was never declared in connect.yaml. Or postDeploy built a destination for the wrong broker and Subscription creation failed. | Query Subscriptions by key and inspect the health status. Compare the destination with CONNECT_SUBSCRIPTION_DESTINATION. Review Deployment logs. |
| Duplicate rows after a redelivery or a backfill run | The rows carry no deduplication key, or the stream and the backfill key them differently. | Assert the key on both paths and merge on it in the destination. |
| Most Orders arrive, but subscription and migrated Orders never do | The Subscription covers Order Created only. | Add Order Created From Recurring Order and Order Imported to the Subscription and the handler. |
| Rows are missing fields, and only the largest resources are affected | The handler transformed the Message payload, which is omitted above the queue size limit. | Retrieve the resource by resource.id and transform from current state. |
| Logs show success, but the destination has no rows | The handler acknowledged before the destination call, or swallowed the error and returned a success status. | Check destination-side ingestion, not only application logs. Let transient failures redeliver. |
| The same notification retries for hours and then disappears | The handler returns a status outside the acknowledgment set for a notification that can never succeed. | Acknowledge permanently unprocessable notifications, record them for replay, and alert instead of looping. |
| A backfill stops after a fixed number of rows | The job pages by offset and reached the maximum offset. | Page with an identifier cursor inside the lastModifiedAt window. |
| A backfill reloads everything on every run | The checkpoint is never saved, or it is saved only at the end of a run that times out. | Save the checkpoint after each page. |
| Two backfill runs process the same rows at once | Scheduled runs overlapped. | Take a durable lock with a time to live longer than the 30-minute job timeout. |
| A query against the Messages API returns nothing | Querying Messages is off by default, and retention is limited when it is on. | Window on the resources' lastModifiedAt instead. |
| Subscription registration fails on a new Deployment | The Project reached its Subscription limit. | Consolidate Message Subscriptions into a Change Subscription per resource, then route on the resource type because the notification carries no Message type. Or request a limit increase. |
| Change History queries return Too Many Requests, and API-origin changes are absent | The pipeline reads Change History. | Rebuild the feed on Subscriptions and API queries. Change History is a governance log. |
| Deleted Customers remain in the destination | Only the backfill runs, or the stream does not handle deletion. | Subscribe to Customer Deleted and delete or anonymize the matching rows. |
| Deployment succeeds but requests fail at runtime | The API Client scopes do not cover the exported domains, or a non-existent scope was declared. | Compare the exported domains against the declared scopes, and confirm each one exists in the API scopes reference. |
| Reports disagree with commercetools on totals | Money was flattened to centAmount without currencyCode and fractionDigits. | Carry all three fields and convert in the destination. |
| The destination is empty during a rebuild | The full load clears the target before it writes. | Build into a new target and swap it in when the load completes. |