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 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:

  • 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:

  1. Log in to your Emporiqa dashboard
  2. Go to your store's Settings page
  3. Navigate to the Integration tab
  4. 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

  1. Copy the connection secret from your store settings
  2. Store this secret securely in your application
  3. Include the signature in the X-Webhook-Signature header

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

1

Start Session

Send sync.start event with a unique session ID

2

Send Data

Send product.created/updated events in batches with the sync_session_id

3

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: 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.

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

  1. Customer interacts with the chat widget (the widget stores a session ID in the browser)
  2. Customer completes a purchase on your store
  3. Your store sends an order.completed event to the Emporiqa webhook endpoint
  4. Emporiqa links the purchase to the chat session and records the conversion
  5. 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

  1. Configure your Order Tracking API URL in Store Settings > Integration
  2. Customer asks "Where is my order #12345?" in the chat widget
  3. Emporiqa sends a POST request to your endpoint with the order identifier
  4. Your endpoint returns the order status (JSON, HTML, or plain text)
  5. 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 customer
  • timestamp (integer, always present) — Unix timestamp of the request
  • user_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