Webhook Setup Guide
Configure your store to push product and page data in real-time
Overview
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 the webhook integration: Install free with $25 of signup credit. Sync your real catalog (up to 30,000 products) and validate webhook delivery in production mode. The credit covers about 100 conversations of testing.
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:
- Real-time updates - changes reflect immediately
- Efficient - only changed data is sent
- Store-controlled - you decide when to sync
- Batch support - send multiple events in one request
Getting Your Webhook URL
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:
- Log in to your Emporiqa dashboard
- Go to your store's Settings page
- Navigate to the Integration tab
- Copy the Webhook URL shown there
Authentication
All webhook requests must include an HMAC-SHA256 signature for authentication. A connection secret is automatically generated when you create a store.
Setting Up Signature Verification
- Copy the connection secret from your store settings
- Store this secret securely in your application
- Include the signature in the
X-Webhook-Signatureheader
Generating the Signature
The 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.
Event Types
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 |
Payload Format
Single Event
{
"type": "product.created",
"data": {
"identification_number": "PROD-123",
"sku": "WJ-001-M",
"channels": ["default"],
"names": {"default": {"en": "Winter Jacket"}},
...
}
}
Batch Events
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.
Product Events
ProductEventData Fields
| 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 |
Example Product Event
{
"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": "..."}}
Price Object Fields
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} |
Multi-Currency Example
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.
Page Events
Pages are used for policy documents, FAQ pages, shipping information, and other non-product content that customers might ask about.
PageEventData Fields
| 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 |
Example Page Event
{
"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"}
}
}
}
Delete Events
Only identification_number is required. The entire entity is deleted across all languages and channels:
{
"type": "product.deleted",
"data": {
"identification_number": "PROD-123"
}
}
Full Sync (Reconciliation)
Use sync sessions to perform a full data reconciliation. This is useful for:
- Initial data sync
- Periodic full sync to ensure data consistency
- Recovering from missed webhook events
- Removing products/pages that no longer exist in your store
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.
Sync Session Flow
Start Session
Send sync.start event with a unique session ID
Send Data
Send product.created/updated events in batches with the sync_session_id
Complete Session
Send sync.complete event - items not seen are marked deleted
Step 1: Start Sync Session
{
"type": "sync.start",
"data": {
"session_id": "sync-2024-01-15-abc123",
"entity": "products"
}
}
Step 2: Send Products/Pages with Session ID
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.
Step 3: Complete Sync 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.
SyncStartEventData / SyncCompleteEventData Fields
| Field | Type | Required | Description |
|---|---|---|---|
session_id |
string | Yes | Unique identifier for this sync session |
entity |
string | Yes | Either 'products' or 'pages' |
Response Codes
Success Response
On success, the webhook returns HTTP 202 Accepted:
{
"status": "accepted",
"queued": 5,
"errors": []
}
Error Responses
| 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: Pay-as-you-go: 3,000 requests per 60 seconds. Enterprise: 10,000 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.
Code Examples
Python
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)
JavaScript (Node.js)
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
<?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);
?>
Troubleshooting
401 Unauthorized - Invalid signature
Ensure you're using the correct connection secret and computing the signature on the raw JSON body (not form-encoded).
- Check that the secret matches exactly (including whitespace)
- Verify you're signing the raw body, not a re-serialized version
- Ensure the Content-Type header is application/json
400 Bad Request - Invalid payload
Check that your JSON is valid and all required fields are present.
- Validate your JSON structure
- Ensure all required fields are included
- Check that field types match (strings vs numbers)
Products not appearing in search
Data processing is asynchronous. Products may take a few seconds to become searchable.
- Wait 30-60 seconds after sending data
- Check that the language codes in your product data are correct
- Verify the subscription limits haven't been exceeded
Order Completed Webhook (Conversion Tracking)
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.
How It Works
- Customer interacts with the chat widget (the widget stores a session ID in the browser)
- Customer completes a purchase on your store
- Your store sends an
order.completedevent to the Emporiqa webhook endpoint - Emporiqa links the purchase to the chat session and records the conversion
- Revenue and conversion data appear on your dashboard
OrderCompletedEventData Fields
| 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. |
Example
{
"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}
]
}
}
Getting the Session ID
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.
Dry Run (Test Connection)
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.
Request
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 ... }
}
Response
{
"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": []
}
]
}
What It Checks
- Signature: HMAC-SHA256 verification (same as production)
- Rate limit: Counts against rate limits (same as production)
- Schema validation: Pydantic schema for each event type
- Field presence: Which translatable fields are populated
- Warnings: Data quality hints (missing descriptions, variation_attributes not matching attribute keys, empty prices)
Safe to use in production: Dry run requests never create, update, or delete any data. The only side effect is rate limit consumption.
Order Tracking API (Optional)
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.
How It Works
- Configure your Order Tracking API URL in Store Settings > Integration
- Customer asks "Where is my order #12345?" in the chat widget
- Emporiqa sends a POST request to your endpoint with the order identifier
- Your endpoint returns the order status (JSON, HTML, or plain text)
- The salesperson formats the response and presents it to the customer
Request Format
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).
Authentication
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.
Expected Response
Your endpoint should return one of the following:
- JSON (recommended): Return order data as JSON with
Content-Type: application/json - HTML: Return formatted HTML with
Content-Type: text/html(HTML tags will be stripped) - Plain text: Return plain text with order details
- 404: Return HTTP 404 if the order is not found
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.
Need Help with Integration?
Our team is here to help with your integration. Whether you need assistance with webhooks, widget embedding, or troubleshooting, just reach out.
Contact Support