PrestaShop Webhook Integration
Overview
Section titled “Overview”This guide explains how to configure and use PrestaShop webhooks with OpenLinker. PrestaShop webhooks allow real-time synchronization of products, inventory, and orders.
Prerequisites
Section titled “Prerequisites”- PrestaShop Installation: PrestaShop 1.7+ with webhook module installed
- OpenLinker Connection: Active PrestaShop connection configured in OpenLinker
- Webhook Secret: Shared secret configured in both PrestaShop and OpenLinker
Configuration
Section titled “Configuration”1. Create Connection in OpenLinker
Section titled “1. Create Connection in OpenLinker”POST /connections{ "name": "My PrestaShop Store", "platformType": "prestashop", "adapterKey": "prestashop.webservice.v1", "status": "active", "credentials": { "apiUrl": "https://your-store.com/api", "apiKey": "your-api-key" }}Save the connectionId from the response.
2. Configure Webhook Secret
Section titled “2. Configure Webhook Secret”Set the webhook secret as an environment variable:
# Connection-specific (recommended)export OPENLINKER_WEBHOOK_SECRET__PRESTASHOP__<CONNECTION_ID>=your-secret-key
# Or provider-level (fallback)export OPENLINKER_WEBHOOK_SECRET__PRESTASHOP=your-secret-keyNote: Replace <CONNECTION_ID> with your actual connection ID (uppercase, no dashes).
3. Install PrestaShop Webhook Module
Section titled “3. Install PrestaShop Webhook Module”Install the OpenLinker module in your PrestaShop instance:
For detailed installation instructions, see: PrestaShop Module README
Quick Start:
- Upload module ZIP to PrestaShop (or use bind-mount for development)
- Install module via PrestaShop backoffice: Modules → Module Manager
- Configure module settings (see module README for details)
4. Configure PrestaShop Webhook Module
Section titled “4. Configure PrestaShop Webhook Module”In your PrestaShop webhook module configuration:
- Webhook URL:
https://your-openlinker-instance.com/webhooks/prestashop/<CONNECTION_ID> - Secret Key: Same secret as configured in OpenLinker
- Events: Select events to subscribe to (see Supported Events below)
Supported Events
Section titled “Supported Events”Product Events
Section titled “Product Events”product.saved
Section titled “product.saved”Triggered when a product is created or updated.
Payload:
{ "schemaVersion": 1, "eventId": "prestashop-product-12345", "eventType": "product.saved", "occurredAt": "2025-01-01T12:00:00.000Z", "object": { "type": "product", "externalId": "12345" }, "payload": { "name": "Product Name", "reference": "PROD-001", "price": 29.99 }}Result: Triggers master.product.syncByExternalId job to fetch full product data via PrestaShop WebService API.
product.deleted
Section titled “product.deleted”Triggered when a product is deleted (PrestaShop’s actionProductDelete hook).
On a multistore install the hook fires only once the product is gone from every
shop, so a delete in one shop does not report a deletion the catalogue has not
made yet.
Payload:
{ "schemaVersion": 1, "eventId": "prestashop-product-12345", "eventType": "product.deleted", "occurredAt": "2025-01-01T12:00:00.000Z", "object": { "type": "product", "externalId": "12345" }}Result: Triggers the same master.product.syncByExternalId job. The webhook
is a trigger, not the deletion itself - the shop answering “no such product” is
the authoritative signal, which stales the product’s variants, emits
master.product.stale and pauses the offers on every marketplace.
Without this event a deletion reached OpenLinker only through the hourly
deletion-audit pass (master.product.reconcile), which walks the catalogue a
page at a time, so on a large shop a deleted product kept selling for as long as
the walk took to reach it. The audit pass is unchanged and stays the backstop
for a lost or undelivered webhook.
Inventory Events
Section titled “Inventory Events”stock.changed
Section titled “stock.changed”Triggered when product stock level changes.
Payload:
{ "schemaVersion": 1, "eventId": "prestashop-stock-12345", "eventType": "stock.changed", "occurredAt": "2025-01-01T12:00:00.000Z", "object": { "type": "stock", "externalId": "12345" }, "payload": { "quantity": 100 }}Result: Triggers prestashop.inventory.syncByExternalId job to sync inventory levels.
Order Events
Section titled “Order Events”order.created
Section titled “order.created”Triggered when a new order is created.
Payload:
{ "schemaVersion": 1, "eventId": "prestashop-order-67890", "eventType": "order.created", "occurredAt": "2025-01-01T12:00:00.000Z", "object": { "type": "order", "externalId": "67890" }, "payload": { "total": 99.99, "currency": "EUR" }}Result: Triggers prestashop.order.syncByExternalId job to fetch full order data.
order.status_changed
Section titled “order.status_changed”Triggered when order status changes.
Payload:
{ "schemaVersion": 1, "eventId": "prestashop-order-status-67890", "eventType": "order.status_changed", "occurredAt": "2025-01-01T12:00:00.000Z", "object": { "type": "order", "externalId": "67890" }, "payload": { "status": "shipped" }}Result: Triggers prestashop.order.syncByExternalId job to sync order status.
Webhook Signature Generation
Section titled “Webhook Signature Generation”PrestaShop webhook module must generate signatures using the same scheme as OpenLinker:
const crypto = require('crypto');
function generateSignature(secret, timestamp, rawBody) { const signedPayload = timestamp + '.' + rawBody; return crypto .createHmac('sha256', secret) .update(signedPayload) .digest('hex');}
// Usageconst timestamp = Date.now().toString();const rawBody = JSON.stringify(payload);const signature = generateSignature(secret, timestamp, rawBody);
// Headersheaders['X-OpenLinker-Timestamp'] = timestamp;headers['X-OpenLinker-Signature'] = `sha256=${signature}`;Testing
Section titled “Testing”Manual Test
Section titled “Manual Test”-
Generate signature:
Terminal window node scripts/generate-webhook-signature.js your-secret-key -
Send test webhook:
Terminal window curl -X POST http://localhost:3000/webhooks/prestashop/<CONNECTION_ID> \-H "Content-Type: application/json" \-H "X-OpenLinker-Timestamp: <TIMESTAMP>" \-H "X-OpenLinker-Signature: sha256=<SIGNATURE>" \-d '{"schemaVersion": 1,"eventId": "test-event-123","eventType": "product.saved","occurredAt": "2025-01-01T12:00:00.000Z","object": {"type": "product","externalId": "12345"}}' -
Verify — in Postgres, not Redis. Since #2280 the delivery row and the
sync_jobswork row commit in one transaction at ingress, so the pair is the authoritative record (the inbound Redis stream was retired by #2300):SELECT status, "dlqReason", "downstreamJobType", "downstreamJobId"FROM webhook_deliveries WHERE "eventId" = '<eventId>';SELECT "jobType", status FROM sync_jobsWHERE "idempotencyKey" = 'prestashop:<connectionId>:<eventId>';
Event Deduplication
Section titled “Event Deduplication”Why Multiple Hook Fires Occur
Section titled “Why Multiple Hook Fires Occur”PrestaShop’s actionProductSave hook (and other hooks) can fire multiple times during a single product save operation. This is expected PrestaShop behavior, not a bug.
Common scenarios:
- Product saved for multiple languages (2 languages = 2+ hook fires)
- Product saved for multiple shops (multi-shop setup)
- Product saved in multiple phases (main record, attributes, categories, stock, images, SEO)
- Hook called from multiple places in PrestaShop core code
Example: Saving a product with 2 languages can trigger the hook 6 times within the same second, all for the same product ID.
How Deduplication Works
Section titled “How Deduplication Works”The PrestaShop webhook module coalesces repeat hook fires. Coalescing is keyed on queue state, not on a clock (#2603):
-
Subject-derived dedup key: every outbox row carries a
dedup_keyover Provider + Connection ID + Event Type + Object Type + External ID. No timestamp goes into it. -
Database-level coalescing:
INSERT IGNOREplus a unique constraint ondedup_keydrops a repeat fire while the first row is still queued. -
The key is released when the row leaves the queue: the repository sets
dedup_keytoNULLwhen a row is claimed for delivery, when it is delivered, and when it fails for good. MySQL treatsNULLas distinct in a unique index, so history never blocks a new event.
Result: even if a hook fires 6 times in one second, only 1 event is created and sent to OpenLinker. A later change to the same object always gets its own row, whatever the cron cadence.
Event IDs are unique per row and must stay that way. OpenLinker keys its own durable replay protection on (provider, connection_id, event_id), so reusing an id across two rows would make the second real delivery look like a replay and be discarded. The id is generated once at enqueue time and stored on the row, which is what makes a retry of the same delivery recognisable as a replay.
The key covers the subject, not the payload. Two order.status_changed fires while a row is queued deliver only the first newStatusId. That is correct under “the webhook is a trigger, the pull is authoritative” - the sync job re-reads current shop state.
Verification
Section titled “Verification”To verify deduplication is working:
-- At most one queued row per subject (should return 0 rows)SELECT dedup_key, COUNT(*) as countFROM ps_openlinker_webhook_outboxWHERE dedup_key IS NOT NULLGROUP BY dedup_keyHAVING count > 1;
-- Check events for a specific product (one row per save operation)SELECT id, event_type, external_id, status, dedup_key, created_atFROM ps_openlinker_webhook_outboxWHERE external_id = '23' AND event_type = 'product.saved'ORDER BY created_at DESC;Expected: one event per product save operation, even if the hook fired multiple times. Rows that have already been delivered or have failed for good show dedup_key = NULL.
Troubleshooting
Section titled “Troubleshooting”401 Unauthorized
Section titled “401 Unauthorized”Possible causes:
- Invalid signature (check secret key matches)
- Timestamp out of window (check system clock sync)
- Signature computed on wrong body (must use exact raw bytes)
Solution: Verify signature generation matches OpenLinker’s scheme exactly.
404 Not Found
Section titled “404 Not Found”Possible causes:
- Connection ID doesn’t exist
- Connection is disabled
- Provider mismatch (URL says
prestashopbut connection isallegro)
Solution: Check connection exists and is active with correct platformType.
Events Not Processing
Section titled “Events Not Processing”Possible causes:
- Handler not running (check application logs)
- Consumer group not created (check logs for “Created consumer group”)
- Redis connection issues
Solution: Check application logs and Redis connectivity.
Duplicate Events in Database
Section titled “Duplicate Events in Database”Two rows for the same object are normal when the change really happened twice. Investigate only if a single save produced several rows.
Possible causes:
- The
dedup_keycolumn or its unique index is missing (the 1.3.0 upgrade did not run) INSERT IGNOREis no longer used inOutboxRepository::enqueueEvent()
Solution:
- Verify the schema:
SHOW CREATE TABLE ps_openlinker_webhook_outbox;- bothevent_idanddedup_keymust carry a unique key - Do not make event IDs deterministic. They are unique per row on purpose; a reused id is discarded by OpenLinker as a replay
- Verify
INSERT IGNOREis being used inOutboxRepository::enqueueEvent()
Best Practices
Section titled “Best Practices”-
Use Connection-Specific Secrets: Prefer connection-specific secrets over provider-level for better security isolation.
-
Monitor Webhook Delivery: Set up alerts for signature verification failures and duplicate events.
-
Unique Event IDs, Idempotent Handling: give every delivery its own event ID and keep it stable across retries of that delivery. OpenLinker dedups on
(provider, connection_id, event_id), so a deterministic id shared by two real changes silently discards the second. Coalesce repeat fires at the source instead, on a key you release once the event is sent. -
Minimal Payloads: Keep webhook payloads minimal. Full data is fetched via adapter APIs during sync jobs.
-
Error Handling: Implement retry logic in PrestaShop webhook module for transient failures (5xx responses).
Related Documentation
Section titled “Related Documentation”- Webhook Overview - OpenLinker webhook ingestion system
- PrestaShop Module README - Module installation and configuration guide
- Architecture Overview - System architecture