Catalog Webhook Integration Guide for Merchants
This document explains how an external merchant system integrates with Merchant Services through catalog webhooks. It is written for merchant engineering teams that own ERP, PIM, commerce platform, warehouse, marketplace, or custom product data systems.
The scope is catalog only:
- Send product create, update, upsert, price, inventory, and delete events into Merchant Services.
- Understand every product and variant field that can be posted.
- Understand the product, cart, checkout, and order data that Merchant Services can send back to the merchant through outbound webhooks.
- Implement a secure, reliable outbound webhook receiver.
- Prepare network, security, retry, monitoring, and go-live controls.
1. Integration Model
Merchant Services expects the merchant's source system to remain the system of record for product data. Merchant Services stores a synchronized catalog copy so the merchant can use catalog APIs, cart creation, checkout preparation, order events, and connected commerce flows.
Use webhook integration when the merchant wants event-driven synchronization. Use REST batch import when the merchant wants scheduled full syncs. Many production deployments use both: a nightly REST reconciliation job plus near-real-time webhooks for product changes.
2. Required Values
Before development starts, Thyris or your integration owner will provide the required connection values. Do not infer these values from another environment.
| Value | Description |
|---|---|
webhookBaseUrl | Public webhook ingress host shared by Thyris or your integration owner, for example https://webhook.thyris.ai or a customer-specific custom domain. |
apiKey | Catalog webhook API key shared with you for the target environment. |
merchantId | Merchant account identifier shared for support and reconciliation. |
storeId | Store UUID shared by Thyris or your integration owner. Every product webhook must include the correct store. |
sku policy | Merchant-owned SKU format and uniqueness rules agreed during integration. |
externalId policy | Source-system product ID format agreed during integration. Strongly recommended. |
| outbound destination URL | Merchant-owned public HTTPS endpoint that you provide to receive catalog events. |
| outbound secret | Shared secret agreed for outbound delivery. Merchant Services sends it as x-webhook-secret. |
| staging hosts | Staging webhook ingress and merchant receiver URLs shared for testing. |
| production contacts | Technical owner, escalation contact, and incident channel. |
3. Inbound Catalog Webhook
Inbound catalog webhooks are HTTP POST requests from the merchant system to Merchant Services.
Endpoint
POST /webhooks/catalog
Authorization: Bearer tr_live_your_key_here
Content-Type: application/json
Webhook URL:
{webhookBaseUrl}/webhooks/catalog
webhookBaseUrl may be a Thyris-hosted domain or a customer-specific custom domain. Always use the exact domain, protocol, and path shared by Thyris or your integration owner.
The same endpoint accepts single-product and batch events.
4. Inbound Authentication
Use a developer API key.
Authorization: Bearer tr_live_your_key_here
Alternative header:
X-Catalog-API-Key: tr_live_your_key_here
X-API-Key is also accepted for shared tooling, but Authorization: Bearer ... is the recommended production header and X-Catalog-API-Key is the recommended catalog-specific alternative.
Security requirements:
- Keep the key server-side only.
- Do not call the webhook endpoint from browser JavaScript or mobile apps.
- Use a store-scoped key when one integration syncs one store.
- Use a merchant-scoped key only when one backend service syncs multiple stores.
- Rotate the key before production launch.
- Rotate the key after vendor, employee, or credential exposure changes.
- Store keys in a secret manager or encrypted runtime configuration.
- Never place keys in product
metadata, logs, screenshots, Git repositories, or merchant/customer-visible fields.
5. Supported Inbound Events
| Event | Meaning | Product body |
|---|---|---|
product.created | Create a new product. | product |
product.updated | Update an existing product or create if not found by identifiers. | product |
product.upserted | Create or update a product. Recommended default. | product |
product.inventory.updated | Update stock status or stock quantity. | product |
product.price.updated | Update price and currency. | product |
product.deleted | Delete a product by storeId plus sku and/or externalId. | product |
catalog.batch.upserted | Create or update multiple products. | products |
catalog.batch.deleted | Delete multiple products. | products |
Recommended event strategy:
- Use
product.upsertedfor most create/update flows. - Use
product.inventory.updatedwhen only stock fields change. - Use
product.price.updatedwhen only price fields change. - Use
status: "archived"instead of hard delete when historical visibility matters. - Use
product.deletedonly when the merchant wants the product removed from active catalog storage.
6. Payload Envelope
Single product:
{
"event": "product.upserted",
"direction": "inbound",
"source": "merchant-pim",
"product": {
"storeId": "STORE_UUID"
}
}
Batch:
{
"event": "catalog.batch.upserted",
"direction": "inbound",
"source": "merchant-pim",
"products": [
{
"storeId": "STORE_UUID"
}
]
}
Envelope fields:
| Field | Type | Required | Description |
|---|---|---|---|
event | string | Yes | One of the supported catalog events. |
direction | string | No | Use inbound. Defaults to inbound when omitted. |
source | string | No | Source system name such as erp, pim, shop, or warehouse. Max 120 chars. |
product | object | Required for single events | One product payload. |
products | array | Required for batch events | Product payload array. Max 250 products. |
Send either product or products. Do not send both unless explicitly testing mixed fallback behavior.
7. Product Fields the Merchant Can Send
This is the full supported product payload for inbound catalog webhooks.
| Field | Type | Required | Validation | Description |
|---|---|---|---|---|
storeId | UUID string | Yes | Must be accessible by API key. | Target store. |
sku | string or null | Recommended | Max 120 chars. | Merchant SKU. Used for idempotent upsert in the store. |
externalId | string or null | Recommended | Max 255 chars. | Product ID in the merchant's ERP/PIM/platform. Used for idempotent upsert. |
name | string | Yes for upsert/update/create | 1-255 chars. | Product display name. |
description | string or null | No | Text. | Product description used for search and product detail rendering. |
price | number | Yes for upsert/update/create | Must be >= 0. | Current sell price. |
currency | string | No | Exactly 3 letters. Default USD. | ISO-style currency code such as USD, EUR, TRY, SAR. |
status | string | No | draft, active, or archived. Default active. | Product publication state. |
inStock | boolean | No | true or false. Default true. | Whether the product can currently be sold. |
inventoryQuantity | integer | No | Must be >= 0. Default 0. | Available inventory quantity. |
imageUrl | string, empty string, or null | No | Valid URL when present. | Backward-compatible primary product image URL. |
imageUrls | string[] or null | No | Maximum 20 valid URLs. | Ordered gallery; first URL is synchronized to imageUrl. |
productUrl | string, empty string, or null | No | Valid URL when present. | Public storefront product URL. |
metadata | object or null | No | JSON object. | Internal integration metadata and custom data provided by Thyris or your integration owner. Avoid PII and secrets. |
otherDetails | object or null | No | JSON object. | Searchable product attributes and integration-provided custom catalog data such as brand, category, color, tags, or campaign fields. |
variants | array | No | Array of variant objects. Default []. | Size/color/package variants stored under the product. |
Identity rules:
storeId + skushould uniquely identify one product.storeId + externalIdshould uniquely identify one product.- Send at least one of
skuorexternalIdfor every product. - If both identifiers are sent, they must refer to the same product.
- Use the same identifiers on every retry.
- Do not reuse an old SKU or external ID for a different product unless the old product has been intentionally retired and reconciled.
Status rules:
active: product can appear in search, carts, and customer-facing flows.draft: product exists but should not be treated as generally available.archived: product should be hidden from active selling flows while preserving historical context.
8. Variant Fields the Merchant Can Send
Variants are stored inside the parent product payload.
| Field | Type | Required | Validation | Description |
|---|---|---|---|---|
sku | string or null | Recommended | Max 120 chars. | Variant SKU. |
externalId | string or null | Recommended | Max 255 chars. | Source-system variant ID. |
name | string | Yes | 1-255 chars. | Variant display name. |
description | string or null | No | Text. | Variant description. |
price | number | Yes | Must be >= 0. | Variant sell price. |
currency | string | No | Exactly 3 letters. Default USD. | Variant currency. |
status | string | No | draft, active, or archived. Default active. | Variant status. |
inStock | boolean | No | Boolean. Default true. | Variant stock state. |
inventoryQuantity | integer | No | Must be >= 0. Default 0. | Variant stock quantity. |
imageUrl | string, empty string, or null | No | Valid URL when present. | Variant image URL. |
productUrl | string, empty string, or null | No | Valid URL when present. | Variant-specific product URL. |
otherDetails | object or null | No | JSON object. | Variant attributes such as color, size, material. |
Variant guidance:
- Use variants when a customer can choose size, color, package, configuration, or region.
- Keep parent product fields usable even when variants exist.
- Put shared product attributes on the parent and option-specific attributes on each variant.
- Do not send thousands of variants in one product. Split very large catalogs into separate products or discuss a custom model.
9. Recommended otherDetails
otherDetails is the best place for merchant-specific searchable product attributes and integration-provided custom catalog data. If Thyris or your integration owner shares additional campaign, segment, marketplace, loyalty, installment, or channel fields, place them here unless they are mapped to a dedicated top-level field.
Recommended fields:
{
"brand": "Example Brand",
"category": "Apparel",
"subcategory": "Hoodies",
"tags": ["hoodie", "black", "bestseller"],
"color": "black",
"material": "cotton",
"gender": "unisex",
"ageGroup": "adult",
"sizeSystem": "US",
"season": "winter",
"collection": "2026-core",
"barcode": "0123456789012",
"countryOfOrigin": "TR",
"campaignCode": "SUMMER-26",
"segment": "premium",
"installmentEligible": true,
"customData": {
"providedByIntegration": true,
"displayPriority": 10
},
"fulfillment": {
"warehouse": "Example City-main",
"shipsInDays": 2
}
}
Avoid putting credentials, private cost, private margin, raw customer data, or internal notes in otherDetails.
10. Recommended metadata
Use metadata for integration, operational information, and custom technical data provided by Thyris or your integration owner that helps reconciliation.
{
"sourceEventId": "evt_20260611_0001",
"sourceUpdatedAt": "2026-06-11T20:00:00Z",
"sourceSystem": "merchant-pim",
"syncJobId": "job_123",
"schemaVersion": "2026-06",
"integrationId": "integration_001",
"integrationProvidedCustomData": {
"routingGroup": "catalog-primary"
}
}
Do not use metadata for secrets, API keys, access tokens, customer PII, payment data, or browser-only state.
11. Complete Product Upsert Example
{
"event": "product.upserted",
"direction": "inbound",
"source": "merchant-pim",
"product": {
"storeId": "STORE_UUID",
"sku": "HOODIE-BLK",
"externalId": "pim_product_123",
"name": "Black Hoodie",
"description": "A heavyweight black hoodie with brushed interior.",
"price": 49.9,
"currency": "USD",
"status": "active",
"inStock": true,
"inventoryQuantity": 12,
"imageUrl": "https://cdn.merchant.example/products/hoodie-black.png",
"imageUrls": [
"https://cdn.merchant.example/products/hoodie-black.png",
"https://cdn.merchant.example/products/hoodie-black-back.png"
],
"productUrl": "https://store.merchant.example/products/black-hoodie",
"metadata": {
"sourceEventId": "evt_20260611_0001",
"sourceUpdatedAt": "2026-06-11T20:00:00Z",
"schemaVersion": "2026-06"
},
"otherDetails": {
"brand": "Example Brand",
"category": "Apparel",
"subcategory": "Hoodies",
"tags": ["hoodie", "black"],
"material": "cotton",
"gender": "unisex"
},
"variants": [
{
"sku": "HOODIE-BLK-M",
"externalId": "pim_variant_123_m",
"name": "Black Hoodie / Medium",
"price": 49.9,
"currency": "USD",
"status": "active",
"inStock": true,
"inventoryQuantity": 8,
"imageUrl": "https://cdn.merchant.example/products/hoodie-black-m.png",
"productUrl": "https://store.merchant.example/products/black-hoodie?variant=medium",
"otherDetails": {
"color": "black",
"size": "M"
}
}
]
}
}
12. Price Update Example
{
"event": "product.price.updated",
"source": "merchant-pricing",
"product": {
"storeId": "STORE_UUID",
"sku": "HOODIE-BLK",
"externalId": "pim_product_123",
"name": "Black Hoodie",
"price": 44.9,
"currency": "USD",
"status": "active"
}
}
Current webhook processing stores the product state from the payload. For partial operational events, still send enough product fields to keep the catalog record complete and valid.
13. Inventory Update Example
{
"event": "product.inventory.updated",
"source": "warehouse",
"product": {
"storeId": "STORE_UUID",
"sku": "HOODIE-BLK",
"externalId": "pim_product_123",
"name": "Black Hoodie",
"price": 44.9,
"currency": "USD",
"status": "active",
"inStock": true,
"inventoryQuantity": 24
}
}
14. Batch Upsert Example
{
"event": "catalog.batch.upserted",
"source": "merchant-pim",
"products": [
{
"storeId": "STORE_UUID",
"sku": "SKU-001",
"externalId": "pim_001",
"name": "Product 1",
"price": 19.9,
"currency": "USD",
"status": "active",
"inStock": true,
"inventoryQuantity": 20
},
{
"storeId": "STORE_UUID",
"sku": "SKU-002",
"externalId": "pim_002",
"name": "Product 2",
"price": 29.9,
"currency": "USD",
"status": "active",
"inStock": false,
"inventoryQuantity": 0
}
]
}
Batch requirements:
- Maximum
productsarray size is 250. - All products must belong to stores accessible by the API key.
- Use stable identifiers for every product.
- Retry the full batch only if the sender can tolerate idempotent reprocessing.
- For very large catalogs, send pages sequentially and keep a sync checkpoint in the merchant system.
15. Delete or Archive
Hard delete by SKU or external ID:
{
"event": "product.deleted",
"source": "merchant-erp",
"product": {
"storeId": "STORE_UUID",
"sku": "HOODIE-BLK",
"externalId": "pim_product_123"
}
}
Archive instead of delete:
{
"event": "product.upserted",
"source": "merchant-erp",
"product": {
"storeId": "STORE_UUID",
"sku": "HOODIE-BLK",
"externalId": "pim_product_123",
"name": "Black Hoodie",
"price": 44.9,
"currency": "USD",
"status": "archived",
"inStock": false,
"inventoryQuantity": 0
}
}
Use archive when:
- The product has been sold before.
- Orders, carts, reports, or support tools may reference the product.
- The merchant wants a reversible operation.
Use hard delete when:
- The product was created by mistake.
- The product must be removed from catalog storage.
- Historical references are not required.
16. Inbound Responses
Created:
{
"success": true,
"mode": "created",
"count": 1,
"results": [
{
"success": true,
"mode": "created",
"data": {
"id": "PRODUCT_UUID"
}
}
]
}
Updated:
{
"success": true,
"mode": "updated",
"count": 1,
"results": [
{
"success": true,
"mode": "updated",
"data": {
"id": "PRODUCT_UUID"
}
}
]
}
Batch:
{
"success": true,
"mode": "batch",
"count": 2,
"results": [
{
"success": true,
"mode": "created",
"data": {
"id": "PRODUCT_UUID_1"
}
},
{
"success": true,
"mode": "updated",
"data": {
"id": "PRODUCT_UUID_2"
}
}
]
}
mode values can include created, updated, deleted, ignored, or batch.
17. Product Data Merchant Can Receive
When Merchant Services sends outbound product webhooks, product objects may include the following fields.
Outbound delivery is also triggered by catalog MCP writes. catalog_upsert_product, catalog_delete_product, catalog_create_cart, catalog_clear_cart, and catalog_complete_order emit their documented product/cart/checkout/order events with direction: "outbound" and source: "mcp" when the destination subscribes to those events.
| Field | Type | Description |
|---|---|---|
id | string | Thyris product UUID. |
catalogId | string | 10-digit catalog ID used by cart and agent tools. |
merchantId | string | Merchant account UUID. |
storeId | string | Store UUID. |
sku | string or null | Merchant SKU. |
externalId | string or null | Merchant source-system product ID. |
name | string | Product name. |
description | string or null | Product description. |
price | string or number | Product price. Some responses serialize decimals as strings. |
currency | string | Currency code. |
status | string | draft, active, or archived. |
inStock | boolean | Stock availability. |
inventoryQuantity | integer | Available quantity. |
imageUrl | string or null | Primary product image URL. |
imageUrls | string[] or null | Ordered product gallery, maximum 20 URLs. |
productUrl | string or null | Product page URL. |
metadata | object | Integration metadata. |
otherDetails | object | Flexible business attributes. |
variants | array | Variant objects. |
source | string | Last write source such as api, webhook, thyris, or another integration source. |
createdAt | string | ISO timestamp. |
updatedAt | string | ISO timestamp. |
Example outbound product event:
{
"event": "product.updated",
"direction": "outbound",
"source": "thyris",
"product": {
"id": "PRODUCT_UUID",
"catalogId": "1234567890",
"merchantId": "MERCHANT_UUID",
"storeId": "STORE_UUID",
"sku": "HOODIE-BLK",
"externalId": "pim_product_123",
"name": "Black Hoodie",
"description": "A heavyweight black hoodie.",
"price": "44.90",
"currency": "USD",
"status": "active",
"inStock": true,
"inventoryQuantity": 24,
"imageUrl": "https://cdn.merchant.example/products/hoodie-black.png",
"imageUrls": [
"https://cdn.merchant.example/products/hoodie-black.png",
"https://cdn.merchant.example/products/hoodie-black-back.png"
],
"productUrl": "https://store.merchant.example/products/black-hoodie",
"metadata": {
"sourceEventId": "evt_20260611_0001"
},
"otherDetails": {
"brand": "Example Brand",
"category": "Apparel"
},
"variants": [],
"source": "thyris",
"createdAt": "2026-06-11T19:45:00Z",
"updatedAt": "2026-06-11T20:00:00Z"
}
}
18. Other Catalog Data Merchant Can Receive
Outbound catalog webhooks can also notify the merchant about cart, checkout, and order activity.
Cart Fields
| Field | Type | Description |
|---|---|---|
id | string | Thyris cart UUID. |
scope | object | Owning { merchantId, storeId } pair. |
checkout.id | string | Checkout ID generated from cart; checkout is omitted for carts without one. |
status | string | Cart status such as open, checkout_created, or completed state. |
items | array | Cart line items. |
totalQuantity | integer | Sum of item quantities. |
totalAmount | string | Total amount. |
currency | string | Cart currency. |
metadata | object | Integration metadata. |
otherDetails | object | Integration or channel context. |
createdAt | string | ISO timestamp. |
updatedAt | string | ISO timestamp. |
Cart item fields can include id, productId, catalogId, quantity, unitPrice, totalPrice, currency, productSnapshot, metadata, and otherDetails.
Order Fields
| Field | Type | Description |
|---|---|---|
id | string | Thyris order UUID. |
orderId | string | Public order identifier. |
checkout.id | string | Checkout ID used to create the order. |
payment.id | string | Payment reference supplied by the caller or generated by Catalog. |
scope | object | Owning merchant/store pair. |
customer | object | Customer information supplied during order completion. |
shippingAddress | object | Shipping address supplied during order completion. |
billingAddress | object | Billing address when present. |
payment | object | Safe ID/provider/status data. |
status | string | Order status such as completed or cancelled. |
items | array | Immutable order item snapshots. |
totalQuantity | integer | Sum of item quantities. |
totalAmount | string | Order total amount. |
currency | string | Order currency. |
metadata | object | Integration metadata. |
otherDetails | object | Integration-specific data. |
createdAt | string | ISO timestamp. |
completedAt | string or null | Completion timestamp. |
Example outbound order event:
{
"event": "order.completed",
"direction": "outbound",
"source": "thyris",
"order": {
"id": "ORDER_UUID",
"orderId": "ord_1234567890abcdef12345678",
"scope": { "merchantId": "MERCHANT_UUID", "storeId": "STORE_UUID" },
"cart": { "id": "CART_UUID" },
"checkout": { "id": "chk_1234567890abcdef12345678" },
"payment": { "id": "pay_1234567890abcdef12345678", "status": "paid" },
"status": "completed",
"totalQuantity": 2,
"totalAmount": "99.80",
"currency": "USD",
"items": []
}
}
19. Outbound Catalog Events
Merchant Services can send these catalog events to the merchant's receiver.
| Event | Trigger |
|---|---|
product.created | Product created through API, webhook, or another approved catalog integration. |
product.updated | Product updated through API, webhook, or another approved catalog integration. |
product.upserted | Generic upsert event when included in the agreed outbound event set. |
product.deleted | Product deleted. |
product.inventory.updated | Stock status or quantity changed. |
product.price.updated | Price changed. |
catalog.batch.upserted | Batch upsert processed. |
catalog.batch.deleted | Batch delete processed. |
cart.created | Cart created. |
cart.updated | Cart changed. |
cart.cleared | Cart cleared. |
checkout.created | Checkout ID created for a cart. |
order.created | Order created. |
order.updated | Order changed. |
order.completed | Order completed. |
order.cancelled | Order cancelled. |
Outbound delivery rules:
- Merchant Services sends outbound webhooks as HTTP
POST. Content-Typeisapplication/json.- If an outbound secret is agreed for the integration, Merchant Services sends
x-webhook-secret. - The merchant endpoint must respond within 10 seconds.
- Any
2xxstatus is treated as success. - Non-
2xx, timeout, DNS, TLS, invalid URL, and network errors are logged as failed delivery.
20. Implementing the Merchant Receiver
The receiver is a merchant-owned HTTPS endpoint that accepts outbound catalog events.
Minimum behavior:
- Accept
POSTonly. - Enforce HTTPS.
- Validate
Content-Type: application/json. - Validate
x-webhook-secretif an outbound secret was agreed. - Parse JSON.
- Validate
event. - Extract stable identifiers.
- Deduplicate.
- Persist or enqueue the payload.
- Return
2xxquickly.
Recommended receiver path:
POST https://integrations.merchant.example/webhooks/merchant-services/catalog
Do not block the HTTP response on slow ERP, OMS, PIM, or warehouse writes. Store the payload and process it asynchronously.
Receiver Pseudocode
on POST /webhooks/merchant-services/catalog:
reject if method is not POST
reject if content type is not JSON
reject if x-webhook-secret is missing or wrong
parse JSON
validate event
derive idempotency key:
event + product.id/catalogId/sku/externalId
or event + cart.id/cart.checkout.id
or event + order.orderId/id
if idempotency key has already been accepted:
return 200
save raw payload and headers
enqueue downstream processing job
return 202
Node.js Receiver Example
import express from "express"
const app = express()
app.use(express.json({ limit: "1mb" }))
const WEBHOOK_SECRET = process.env.MERCHANT_SERVICES_WEBHOOK_SECRET
app.post("/webhooks/merchant-services/catalog", async (req, res) => {
if (WEBHOOK_SECRET && req.header("x-webhook-secret") !== WEBHOOK_SECRET) {
return res.status(401).json({ error: "invalid webhook secret" })
}
const event = String(req.body?.event || "")
if (!event) {
return res.status(400).json({ error: "event is required" })
}
const product = req.body?.product
const cart = req.body?.cart
const order = req.body?.order
const stableId =
product?.id ||
product?.catalogId ||
product?.sku ||
product?.externalId ||
cart?.id ||
cart?.checkout?.id ||
order?.orderId ||
order?.id
if (!stableId) {
return res.status(400).json({ error: "stable object identifier is required" })
}
const idempotencyKey = `${event}:${stableId}`
await saveCatalogWebhookEvent({
idempotencyKey,
event,
payload: req.body,
headers: {
"x-webhook-secret-present": Boolean(req.header("x-webhook-secret"))
},
receivedAt: new Date().toISOString()
})
return res.status(202).json({ received: true })
})
app.listen(3000)
Replace saveCatalogWebhookEvent with the merchant's database, queue, or event bus implementation.
21. Network and Connectivity Requirements
Inbound to Merchant Services:
- Merchant source system must reach the
webhookBaseUrlprovided by Thyris or your integration owner. - TLS 1.2 or newer is recommended.
- Payloads must be JSON over HTTPS.
- Requests should come from stable backend infrastructure where possible.
- Merchant firewalls should allow outbound HTTPS to the webhook ingress host.
- Do not send webhooks through a user's browser.
Outbound to merchant:
- Merchant destination URL must be public HTTPS.
- Hostname must resolve in public DNS.
- URL must not resolve to private, loopback, multicast, or internal network IPs.
- Endpoint must accept HTTP
POST. - Endpoint must respond within 10 seconds.
- Endpoint should support normal internet egress from Merchant Services delivery infrastructure.
- If IP allowlisting is required, coordinate the current egress ranges with Thyris or your integration owner.
- Use a stable hostname rather than a raw IP.
- Use a valid certificate from a public certificate authority.
Unsupported outbound destinations:
http://URLs.localhost.- RFC1918 private IP addresses.
- VPN-only hosts.
- Internal Kubernetes service names.
- Self-signed TLS certificates.
- URLs requiring interactive login.
22. Security Controls
Required controls:
- Store API keys and webhook secrets in a secret manager.
- Validate outbound
x-webhook-secret. - Log request IDs and event IDs, not secrets.
- Redact
Authorization,x-api-key, andx-webhook-secretin logs. - Use least-privilege store-scoped keys when possible.
- Separate staging and production credentials.
- Rotate credentials on a schedule.
- Disable credentials immediately after suspected exposure.
Recommended controls:
- Maintain an allowlist of expected event names.
- Maintain an allowlist of expected
storeIdvalues. - Reject payloads larger than the agreed limit.
- Rate limit the merchant receiver endpoint.
- Persist raw event payloads in an access-controlled store for audit and replay.
- Monitor repeated failures, unexpected event types, and spikes.
- Keep a schema version in
metadata.schemaVersion. - Use source event IDs in
metadata.sourceEventId.
23. Idempotency
Inbound sender:
- Use stable
storeId + skuorstoreId + externalId. - Include
metadata.sourceEventIdwhen the source system has an event ID. - Retry using the same payload and identifiers.
- Treat repeated
product.upsertedcalls as safe. - Treat delete of an already deleted product as non-fatal.
Outbound receiver:
- Deduplicate by event plus stable object identifier.
- Product key preference:
product.id,product.catalogId,product.sku,product.externalId. - Cart key preference:
cart.id,cart.checkout.id. - Order key preference:
order.orderId,order.id. - Store idempotency keys long enough to cover retry windows and replay operations.
24. Retry and Backoff
Merchant inbound sender should retry:
408 Request Timeout409 Conflictwhen the conflict is expected to clear425 Too Early429 Too Many Requests5xxserver errors- Network timeouts
Merchant inbound sender should not retry unchanged payloads for:
400 Bad Request401 Unauthorized403 Forbidden- Schema validation errors
- Store scope errors
Suggested backoff:
Merchant outbound receiver should:
- Return
202after accepting and queueing the event. - Return
200for duplicate but already accepted events. - Return
401for invalid secrets. - Avoid
500for downstream system downtime when the event can be queued.
25. Testing Checklist
Inbound tests:
- Send one valid
product.upserted. - Send the same
product.upsertedagain and verify no duplicate product is created. - Send
product.price.updated. - Send
product.inventory.updated. - Send
catalog.batch.upsertedwith at least two products. - Send
product.deletedor archive payload. - Send invalid API key and verify
401. - Send missing
storeIdand verify validation failure. - Send invalid
currencyand verify validation failure. - Send unsupported
statusand verify validation failure.
Outbound receiver tests:
- Provide a staging HTTPS destination to Thyris or your integration owner.
- Validate the agreed
x-webhook-secret. - Trigger
product.createdorproduct.updated. - Verify receiver validates secret.
- Verify receiver logs payload and returns
2xx. - Trigger duplicate payload and verify idempotency.
- Trigger cart/order events if those flows are in scope.
- Force receiver timeout in staging and verify failure is logged.
- Restore receiver and confirm successful delivery.
Operational tests:
- Rotate API key in staging.
- Rotate outbound secret in staging.
- Confirm monitoring alerts for failed inbound sends.
- Confirm monitoring alerts for failed outbound receives.
- Confirm replay process for queued receiver jobs.
26. Go-Live Checklist
Credentials:
- Production API key received from Thyris or your integration owner.
- Production key scope confirmed.
- Production key is not reused from staging.
- Outbound secret agreed and tested.
- Key and secret are stored securely.
Product data:
storeIdmapping is final.- SKU policy is final.
- External ID policy is final.
- Required fields are always present.
- Product status mapping is final.
- Delete vs archive policy is signed off.
- Image URLs are public and durable.
- Product URLs are public and durable.
metadataandotherDetailsdo not contain secrets or unnecessary PII.
Network:
- Merchant source system can reach webhook ingress.
- Merchant receiver has public HTTPS.
- DNS and TLS are valid.
- Firewall rules are configured.
- Receiver responds within 10 seconds.
Operations:
- Retry policy implemented.
- Idempotency implemented.
- Raw event logging implemented.
- Alerting configured.
- Replay process documented.
- Support contacts exchanged.
- Staging UAT complete.
- Production smoke test complete.
27. Common Errors
| Error | Likely cause | Fix |
|---|---|---|
401 Unauthorized | Missing or invalid API key. | Check environment, key value, and key scope. |
403 Forbidden | API key cannot access the target store. | Use correct storeId or key scope. |
400 Invalid JSON payload | Body is not valid JSON. | Validate JSON before sending. |
400 Either product or products is required | Payload has no product object. | Add product or products. |
400 storeId is required | Product missing store ID. | Include assigned storeId. |
400 name is required | Upsert payload missing product name. | Include name. |
400 price must be greater than or equal to 0 | Negative price. | Send zero or positive value. |
400 currency must be a 3-letter code | Currency is not exactly 3 letters. | Send USD, EUR, TRY, SAR, etc. |
400 status must be draft, active, or archived | Unsupported status. | Map source statuses to allowed values. |
409 SKU and externalId match different catalog products | Identifiers point to different existing products. | Fix source mapping and reconcile product identities. |
| Outbound URL rejected | URL is not public HTTPS or resolves privately. | Use a public HTTPS endpoint. |
| Outbound timeout | Receiver did not respond within 10 seconds. | Queue work and respond quickly. |
28. Merchant Ownership
Merchant owns:
- Product source data accuracy.
- Product identifier stability.
- Product status mapping.
- Inbound webhook sender.
- Outbound webhook receiver.
- Receiver idempotency.
- Receiver retry/replay process.
- Source system reconciliation.
- Secure storage of API keys and webhook secrets.
Merchant Services owns:
- Public webhook ingress.
- API key validation.
- Store scope enforcement.
- Catalog persistence.
- Outbound webhook dispatch.
- Delivery logging.
- Outbound webhook delivery setup.
- Catalog API product availability.