Configure your store to push product and page data in real-time
Emporiqa uses webhooks to receive product and page data from your e-commerce store. Instead of polling your store for updates, your store pushes data to us whenever changes occur.
Testing with Sandbox: We recommend starting with a free sandbox store to test your webhook integration. Sandbox stores support up to 100 products and 20 pages - perfect for development. Upgrade to production when you're ready to go live.
Platform Support: We have ready-to-use integrations for WooCommerce, Drupal Commerce, Sylius, Magento 2, PrestaShop, and Shopware. For other platforms, our webhook API works with any system that can send HTTP requests.
How it works: Your store sends HTTP POST requests to your unique webhook URL. We process the data asynchronously and return a 202 Accepted response immediately.
Benefits of webhooks:
Your unique webhook URL is available in your store dashboard. It follows this format:
POST https://emporiqa.com/webhooks/sync/{store_id}/
To find your webhook URL:
All webhook requests must include an HMAC-SHA256 signature for authentication. A connection secret is automatically generated when you create a store.
X-Webhook-Signature headerThe signature is an HMAC-SHA256 hash of the raw request body using your connection secret:
import hmac
import hashlib
signature = hmac.new(
webhook_secret.encode(),
request_body,
hashlib.sha256
).hexdigest()
headers = {
"X-Webhook-Signature": signature,
"Content-Type": "application/json"
}
Note: All webhook requests must include a valid HMAC-SHA256 signature. Requests without a signature or with an invalid signature will be rejected with 401 Unauthorized.
Emporiqa supports the following event types:
| Event | Description | Data Type |
|---|---|---|
product.created |
New product added | ProductEventData |
product.updated |
Product modified | ProductEventData |
product.deleted |
Product removed | DeleteEventData |
page.created |
New page added | PageEventData |
page.updated |
Page modified | PageEventData |
page.deleted |
Page removed | DeleteEventData |
sync.start |
Start full sync session | SyncStartEventData |
sync.complete |
Complete sync, remove missing items | SyncCompleteEventData |
order.completed |
Purchase completed (conversion tracking) | OrderCompletedEventData |
{
"type": "product.created",
"data": {
"identification_number": "PROD-123",
"sku": "WJ-001-M",
"channels": ["default"],
"names": {"default": {"en": "Winter Jacket"}},
...
}
}
Send multiple events in a single request for better performance:
{
"events": [
{
"type": "product.created",
"data": { ... }
},
{
"type": "product.updated",
"data": { ... }
},
{
"type": "page.created",
"data": { ... }
}
]
}
Tip: For initial data sync, send all products as a batch of product.created events.
| Field | Type | Required | Description |
|---|---|---|---|
identification_number |
string | Yes | Unique product identifier in your store |
sku |
string | Yes | Stock Keeping Unit |
channels |
array of strings | No | Sales channel identifiers. Your integration determines the channel key (e.g. slugified shop/channel name). Use ["b2b", "retail"] for multi-channel products. |
names |
{channel: {lang: value}} |
Recommended | Product name/title per channel and language. Example: {"default": {"en": "Winter Jacket", "de": "Winterjacke"}} |
descriptions |
{channel: {lang: value}} |
Recommended | Product description per channel and language (plain text or HTML) |
links |
{channel: {lang: value}} |
Recommended | Full URL to the product page per channel and language |
attributes |
{channel: {lang: {k: v}}} |
No | Translatable key-value pairs per channel and language. Example: {"default": {"en": {"Color": "Red"}, "de": {"Farbe": "Rot"}}} |
categories |
{channel: {lang: [values]}} |
Recommended | Translatable category strings per channel and language. Supports hierarchy: {"default": {"en": ["Electronics > Laptops", "Sale"], "de": ["Elektronik > Laptops", "Angebote"]}}. Parent categories are auto-generated. |
brands |
{channel: value} |
Recommended | Brand/manufacturer name per channel. Example: {"default": "OutdoorPro"} |
prices |
{channel: [PriceObject]} |
Recommended | Price objects per channel. See Price Object below. |
availability_statuses |
{channel: value} |
Recommended | Product availability per channel. One of: available, preorder, backorder, out_of_stock, discontinued |
stock_quantities |
{channel: int|null} |
No | Stock count per channel. null means stock is not tracked. Enables low-stock messaging in chat. |
images |
{channel: [urls]} |
No | Image URLs per channel. Example: {"default": ["https://example.com/img.jpg"]} |
parent_sku |
string|null | No | Parent product SKU (for variants) |
is_parent |
boolean | No | Is this a parent/grouped product |
variation_attributes |
{channel: {lang: [names]}} |
No | Translatable attribute names that define variants. Must match keys in attributes. Example: {"default": {"en": ["Color", "Size"], "de": ["Farbe", "Größe"]}} |
sync_session_id |
string|null | No | Session ID for full sync reconciliation |
{
"type": "product.created",
"data": {
"identification_number": "PROD-123",
"sku": "WJ-001-M-BLK",
"channels": ["default"],
"names": {
"default": {"en": "Winter Jacket - Waterproof"}
},
"descriptions": {
"default": {"en": "Stay warm with our premium waterproof winter jacket..."}
},
"links": {
"default": {"en": "https://example.com/products/winter-jacket"}
},
"categories": {
"default": {"en": ["Clothing > Jackets > Winter"]}
},
"brands": {"default": "OutdoorPro"},
"prices": {
"default": [
{
"currency": "EUR",
"current_price": 149.99,
"regular_price": 199.99,
"price_incl_tax": 149.99,
"price_excl_tax": 126.04
}
]
},
"availability_statuses": {"default": "available"},
"stock_quantities": {"default": 25},
"attributes": {
"default": {"en": {"Color": "Black", "Size": "Medium", "Material": "Polyester"}}
},
"images": {
"default": ["https://example.com/images/jacket-front.jpg", "https://example.com/images/jacket-back.jpg"]
}
}
}
Consolidated format: Each product event contains all translations and channels in one payload. The channel key is determined by your integration (e.g. slugified shop name, sales channel code). For multilingual stores, add more language keys: {"default": {"en": "...", "de": "...", "fr": "..."}}
Each entry in the prices array represents a price in a specific currency:
| Field | Type | Required | Description |
|---|---|---|---|
currency |
string | Yes | ISO 4217 currency code (e.g., EUR, USD, GBP) |
current_price |
float | Yes | Active selling price |
regular_price |
float | No | Original/list price (before discount). The salesperson shows a discount when this is higher than current_price. |
price_incl_tax |
float | No | Gross price including VAT/tax |
price_excl_tax |
float | No | Net price excluding VAT/tax |
tier_prices |
array | No | Volume discount tiers. Each entry: {"min_quantity": 5, "price": 35.99} |
Add one price entry per currency your store sells in:
"prices": {
"default": [
{"currency": "EUR", "current_price": 149.99, "regular_price": 199.99},
{"currency": "GBP", "current_price": 129.99},
{"currency": "USD", "current_price": 159.99}
]
}
Tip: When your widget uses ?currency=EUR, the salesperson selects the matching price entry. If no match is found, it falls back to the first entry.
Pages are used for policy documents, FAQ pages, shipping information, and other non-product content that customers might ask about.
| Field | Type | Required | Description |
|---|---|---|---|
identification_number |
string | Yes | Unique page identifier |
channels |
array of strings | No | Sales channel identifiers. Your integration determines the channel key (e.g. slugified shop/channel name). |
titles |
{channel: {lang: value}} |
Recommended | Page title per channel and language |
contents |
{channel: {lang: value}} |
Recommended | Full page content per channel and language (HTML supported) |
links |
{channel: {lang: value}} |
Recommended | Full URL to the page per channel and language |
sync_session_id |
string|null | No | Session ID for full sync reconciliation |
{
"type": "page.created",
"data": {
"identification_number": "PAGE-SHIPPING",
"channels": ["default"],
"titles": {
"default": {"en": "Shipping Policy"}
},
"contents": {
"default": {"en": "<h2>Shipping Information</h2><p>We offer free shipping on orders over $50...</p>"}
},
"links": {
"default": {"en": "https://example.com/pages/shipping-policy"}
}
}
}
Only identification_number is required. The entire entity is deleted across all languages and channels:
{
"type": "product.deleted",
"data": {
"identification_number": "PROD-123"
}
}
Use sync sessions to perform a full data reconciliation. This is useful for:
How it works: Start a sync session, send all your products/pages, then complete the session. Any items not sent during the session will be automatically marked as deleted.
Send sync.start event with a unique session ID
Send product.created/updated events in batches with the sync_session_id
Send sync.complete event - items not seen are marked deleted
{
"type": "sync.start",
"data": {
"session_id": "sync-2024-01-15-abc123",
"entity": "products"
}
}
Include the sync_session_id in each event to register it with the sync session:
{
"events": [
{
"type": "product.updated",
"data": {
"identification_number": "PROD-001",
"sku": "SKU-001",
"names": {"default": {"en": "Product One"}},
"sync_session_id": "sync-2024-01-15-abc123",
...
}
},
{
"type": "product.updated",
"data": {
"identification_number": "PROD-002",
"sku": "SKU-002",
"names": {"default": {"en": "Product Two"}},
"sync_session_id": "sync-2024-01-15-abc123",
...
}
}
]
}
Tip: Send products in batches of 50-100 for optimal performance. You can send as many batches as needed before completing the session.
After sending all products/pages, complete the session. Any items not seen during the session will be marked as deleted:
{
"type": "sync.complete",
"data": {
"session_id": "sync-2024-01-15-abc123",
"entity": "products"
}
}
Important: Sync sessions expire after 24 hours. Make sure to complete your sync within this time window.
Overlapping syncs: Only one sync session per entity (products or pages) can be active at a time. Starting a new sync while one is already in progress returns 409 Conflict. Wait for the current sync to complete before starting a new one. If a previous sync failed and sync.complete was never sent, the stale session is automatically replaced after 1 hour, so you can retry without issues.
| Field | Type | Required | Description |
|---|---|---|---|
session_id |
string | Yes | Unique identifier for this sync session |
entity |
string | Yes | Either 'products' or 'pages' |
On success, the webhook returns HTTP 202 Accepted:
{
"status": "accepted",
"queued": 5,
"errors": []
}
| Status | Scenario | Response |
|---|---|---|
| 404 | Store ID not found | {"error": "Store not found"} |
| 403 | Subscription not active | {"error": "Subscription not active"} |
| 401 | Missing/invalid signature | {"error": "Invalid webhook signature"} |
| 400 | Invalid JSON or payload | {"error": "Invalid JSON"} |
| 409 | Sync session already active | {"error": "Sync session already active", "active_session_id": "..."} |
| 429 | Rate limit exceeded | {"error": "Rate limit exceeded"} with Retry-After header |
| 500 | Server error | {"error": "Failed to queue events"} |
Rate Limit: The webhook endpoint rate limit depends on your plan: Sandbox: 60, Starter: 120, Growth: 360, Standard: 600, Enterprise: 1,200 requests per 60 seconds. Each request can contain multiple events in a batch, so use batching to stay within the limit during bulk imports. If exceeded, the response includes a Retry-After header with the number of seconds to wait.
import hmac
import hashlib
import json
import requests
WEBHOOK_URL = "https://emporiqa.com/webhooks/sync/YOUR_STORE_ID/"
WEBHOOK_SECRET = "your_webhook_secret"
def send_webhook(events):
payload = json.dumps({"events": events}).encode()
signature = hmac.new(
WEBHOOK_SECRET.encode(),
payload,
hashlib.sha256
).hexdigest()
headers = {
"Content-Type": "application/json",
"X-Webhook-Signature": signature,
}
response = requests.post(WEBHOOK_URL, data=payload, headers=headers)
return response.json()
# Example: Create a product
result = send_webhook([{
"type": "product.created",
"data": {
"identification_number": "PROD-123",
"sku": "SKU-001",
"channels": ["default"],
"names": {"default": {"en": "My Product"}},
"descriptions": {"default": {"en": "Product description here"}},
"links": {"default": {"en": "https://mystore.com/products/my-product"}},
"categories": {"default": {"en": ["Electronics"]}},
"brands": {"default": "MyBrand"},
"prices": {"default": [
{"currency": "EUR", "current_price": 79.99, "regular_price": 99.99}
]},
"availability_statuses": {"default": "available"}
}
}])
print(result)
const crypto = require('crypto');
const WEBHOOK_URL = 'https://emporiqa.com/webhooks/sync/YOUR_STORE_ID/';
const WEBHOOK_SECRET = 'your_webhook_secret';
async function sendWebhook(events) {
const payload = JSON.stringify({ events });
const signature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');
const headers = {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
};
const response = await fetch(WEBHOOK_URL, {
method: 'POST',
headers,
body: payload
});
return response.json();
}
// Example usage
sendWebhook([{
type: 'product.created',
data: {
identification_number: 'PROD-123',
sku: 'SKU-001',
channels: [''],
names: {'': {en: 'My Product'}},
descriptions: {'': {en: 'Product description here'}},
links: {'': {en: 'https://mystore.com/products/my-product'}},
categories: {'': {en: ['Electronics']}},
brands: {'': 'MyBrand'},
prices: {'': [
{ currency: 'EUR', current_price: 79.99, regular_price: 99.99 }
]},
availability_statuses: {'': 'available'}
}
}]).then(console.log);
<?php
$webhookUrl = 'https://emporiqa.com/webhooks/sync/YOUR_STORE_ID/';
$webhookSecret = 'your_webhook_secret';
function sendWebhook($events) {
global $webhookUrl, $webhookSecret;
$payload = json_encode(['events' => $events]);
$signature = hash_hmac('sha256', $payload, $webhookSecret);
$headers = [
'Content-Type: application/json',
'X-Webhook-Signature: ' . $signature,
];
$ch = curl_init($webhookUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
// Example usage
$result = sendWebhook([[
'type' => 'product.created',
'data' => [
'identification_number' => 'PROD-123',
'sku' => 'SKU-001',
'channels' => [''],
'names' => ['' => ['en' => 'My Product']],
'descriptions' => ['' => ['en' => 'Product description here']],
'links' => ['' => ['en' => 'https://mystore.com/products/my-product']],
'categories' => ['' => ['en' => ['Electronics']]],
'brands' => ['' => 'MyBrand'],
'prices' => ['' => [
['currency' => 'EUR', 'current_price' => 79.99, 'regular_price' => 99.99]
]],
'availability_statuses' => ['' => 'available']
]
]]);
print_r($result);
?>
Ensure you're using the correct connection secret and computing the signature on the raw JSON body (not form-encoded).
Check that your JSON is valid and all required fields are present.
Data processing is asynchronous. Products may take a few seconds to become searchable.
Send an order.completed event when a customer completes a purchase. Emporiqa uses this to attribute sales to chat sessions and display revenue data on your conversion tracking dashboard.
order.completed event to the Emporiqa webhook endpoint| Field | Type | Required | Description |
|---|---|---|---|
order_id |
string | Yes | Your store's order ID or number |
total |
decimal | Yes | Order total amount (e.g., 149.99) |
currency |
string | No | 3-letter ISO 4217 currency code (e.g., 'USD', 'EUR') |
emporiqa_session_id |
string | No | The Emporiqa chat session ID. If provided, the purchase is linked directly to that session. If omitted, the conversion is recorded without session attribution. |
items |
array | No | Line items in the order. Each item can include product_id (string), quantity (integer), and price (decimal). Some integrations also include variation_id. |
{
"type": "order.completed",
"data": {
"order_id": "ORD-2026-4521",
"total": 149.99,
"currency": "EUR",
"emporiqa_session_id": "abc123-def456-ghi789",
"items": [
{"product_id": "product-42", "quantity": 2, "price": 49.99},
{"product_id": "product-87", "quantity": 1, "price": 50.01}
]
}
}
The chat widget stores the session ID in the browser. Your store can retrieve it via the EmporiqaCartHandler or by reading the widget's session storage. Our platform plugins (WooCommerce, Drupal, Sylius) handle this automatically.
Deduplication: If you send the same order_id twice, the second event is ignored. This makes it safe to retry on failure.
Note: This event uses the same webhook URL and HMAC signature as product/page events. No separate configuration is needed.
Test your webhook integration without processing any data. Add ?dry_run=true to the webhook URL. The server runs the full validation pipeline (signature, rate limit, schema) but does not queue or store anything.
POST https://emporiqa.com/webhooks/sync/YOUR_STORE_ID/?dry_run=true
Content-Type: application/json
X-Webhook-Signature: <HMAC-SHA256 signature>
{
"type": "product.created",
"data": { ... your normal product payload ... }
}
{
"status": "dry_run",
"signature": "valid",
"events_validated": 1,
"events": [
{
"type": "product.created",
"valid": true,
"identification_number": "product-123",
"sku": "JACKET-TRAIL",
"languages_detected": ["de", "en"],
"channels_detected": ["default"],
"fields": {
"names": true,
"descriptions": true,
"links": true,
"categories": true,
"attributes": true,
"prices": true,
"variation_attributes": true
},
"is_parent": true,
"parent_sku": null,
"warnings": []
}
]
}
Safe to use in production: Dry run requests never create, update, or delete any data. The only side effect is rate limit consumption.
Emporiqa supports optional order tracking. Unlike webhooks (where your store pushes data to us), order tracking works in the opposite direction: when a customer asks about their order status in the chat, Emporiqa sends a request to your store's order tracking API endpoint.
Emporiqa sends a POST request to your configured URL:
POST https://yourstore.com/api/orders/track
Content-Type: application/json
X-Emporiqa-Signature: <HMAC-SHA256 signature>
{
"order_identifier": "12345",
"timestamp": 1707400000,
"user_id": "customer-456",
"verification_fields": {
"email": "[email protected]",
"phone_last_4": "1234"
}
}
Request fields:
order_identifier (string, always present) — The order number or tracking ID provided by the customertimestamp (integer, always present) — Unix timestamp of the requestuser_id (string, optional) — The verified customer ID, extracted from the signed token passed when embedding the widget. This ID is cryptographically verified by Emporiqa before being forwarded, so you can trust it to restrict results to that customer's orders.verification_fields (object, optional) — Customer-provided verification data collected by the chat assistant before looking up the order. Only present if you have configured verification fields in Store Settings > Integration. The keys match the field names you defined, and the values are what the customer provided. Use these to verify the customer is authorized to view the order (e.g., check that the email matches the order record).
Requests are signed with HMAC-SHA256 using the same connection secret configured in your store settings. The signature is included in the X-Emporiqa-Signature header. Verify this signature on your side to ensure the request is from Emporiqa.
Your endpoint should return one of the following:
Content-Type: application/jsonContent-Type: text/html (HTML tags will be stripped)Note: If order tracking is not configured, customers asking about orders will be handled by the Customer Support agent instead. There is no need to sync order data via webhooks. The order tracking feature queries your store's API in real-time.
Our team is here to help with your integration. Whether you need assistance with webhooks, widget embedding, or troubleshooting, just reach out.
Contact Support