Integration & ingestion
Commerce integration is built from source-agnostic event ingestion and CMS → Shopify bidirectional sync. Every event is logged with its raw payload, so any event can be replayed.
Event ingestion
backend adapter (verify signature, build the envelope)
→ direct:commerce-ingest
├─ logEvent (record the raw payload, status=received)
├─ by topic: a dedicated route (order-paid, …)
└─ otherwise: normalizeEvent → /content/commerce/entities/{source}/{collection}/{id}.json
→ markEvent (processed / error)
- all-topics ingestion: every webhook is accepted (no allow-list); topics without a dedicated workflow are normalized into entities
- multi-backend: if an adapter sends the same envelope (
event_source/event_topic/event_id/received_at, body = raw payload) todirect:commerce-ingest, nothing downstream needs to change - config:
/etc/commerce/config/ingest.yml
Replay
- automatic: the
commerce-replaytimer (every 5 min by default) re-runserrorevents up toreplay.maxAttempts(withbackoffMinutes);processedevents are pruned byretentionDays - manual:
POST …/endpoints/events.groovywith a single event ({source, eventId}) or a filter ({status, topic, source, sinceDays}); returns{matched, replayed} - mechanism: the envelope is re-dispatched with
replay=true— the webhook idempotency guard is bypassed while per-workflow guards still prevent duplicates
Read: GET …/endpoints/events.groovy?status=error&limit=100. UI: Commerce Operations → Events.
Bidirectional sync (CMS → Shopify)
GET …/endpoints/sync.groovy # capability / status
POST …/endpoints/sync.groovy # perform an action
| Action | Purpose | Shopify mutation |
|---|---|---|
inventory |
set a location's available quantity | inventorySetQuantities |
price |
set a variant price | productVariantsBulkUpdate |
publish |
publish/unpublish (ACTIVE/DRAFT) | productUpdate |
metafields |
upsert metafields | metafieldsSet |
{ "action": "inventory", "inventoryItemId": 123, "locationId": 456, "quantity": 10 }
{ "action": "price", "productId": 1, "variantId": 2, "price": "19.99", "dryRun": true }
- gated by
adminApi.enabled(shopify.yml); when disabled it returns409and never calls Shopify dryRun: truevalidates and echoes the target and values without calling Shopify- every attempt (including dry runs and failures) is audited to
/content/commerce/sync/{yyyy}/{MM}/sync_{ts}.json - implemented in
commerce.ShopifyWrite(oncommerce.ShopifyAdmin); reconciliation's auto-heal uses the same write primitives
UI: Commerce Operations → Sync (a dry-run toggle and a recent-writes table).
Commerce i18n
Commerce messages live in /etc/i18n/commerce.<locale>.json and each app's <appId>.<locale>.json, merged per locale. Keys are app.<appId>.* (shared ones are common.*); the format is ICU. Keep en and ja in sync, and write one sentence per key rather than concatenating fragments.
Currency and locale follow the user's effective settings (Preferences → Localization), with store.json's currency as the fallback. For platform-wide i18n, see "Localization & i18n".