Webhooks notify your app in real-time when events happen in a merchant's store. Instead of polling the API, you receive HTTPS POST requests with event data.
Your webhook URL is registered when you create your app. It must:
POST requests with Content-Type: application/json| Event | Trigger | Scope Required |
|---|---|---|
order.created | New order placed | read_orders |
order.paid | Payment confirmed | read_orders |
order.fulfilled | Order shipped/fulfilled | read_orders |
order.cancelled | Order cancelled | read_orders |
order.refunded | Refund issued | read_orders |
product.created | New product added | read_products |
product.updated | Product details changed | read_products |
product.deleted | Product removed | read_products |
customer.created | New customer registered | read_customers |
customer.updated | Customer profile changed | read_customers |
inventory.low_stock | Stock below threshold | read_inventory |
inventory.out_of_stock | Stock reaches zero | read_inventory |
cart.abandoned | Cart inactive for 30+ minutes | read_orders |
subscription.created | New subscription started | read_orders |
subscription.cancelled | Subscription cancelled | read_orders |
collection.created | New collection added | read_products |
collection.updated | Collection changed | read_products |
app.installed | Your app was installed | None (always sent) |
app.uninstalled | Your app was removed | None (always sent) |
read_orders but not read_customers, you will receive order events but not customer events. Every webhook POST has the same envelope format:
{
"id": "evt_a1b2c3d4",
"event": "order.created",
"created_at": "2026-04-21T14:30:00Z",
"store_id": "merchant_store_uuid",
"app_id": "your_app_uuid",
"payload": {
// Event-specific data (full resource object)
}
}| Header | Description |
|---|---|
Content-Type | application/json |
X-Mercentia-Signature | HMAC-SHA256 signature for verification |
X-Mercentia-Timestamp | Unix timestamp (ms) when the event was sent |
X-Mercentia-Event | Event type (e.g., order.created) |
X-Mercentia-Delivery-Id | Unique delivery ID for idempotency |
User-Agent | Mercentia-Webhooks/1.0 |
{
"id": "evt_x9y8z7",
"event": "order.created",
"created_at": "2026-04-21T14:30:00Z",
"store_id": "store_uuid",
"app_id": "app_uuid",
"payload": {
"id": "order_uuid",
"orderNumber": "SF-1042",
"status": "pending",
"total": 70.37,
"currency": "USD",
"customerEmail": "[email protected]",
"items": [
{
"productId": "prod_uuid",
"name": "Premium Cotton T-Shirt",
"quantity": 2,
"price": 29.99
}
],
"shippingAddress": {
"line1": "123 Main St",
"city": "London",
"country": "GB",
"postcode": "SW1A 1AA"
},
"createdAt": "2026-04-21T14:30:00Z"
}
}Always verify webhook signatures to ensure requests genuinely come from Mercentia.
import crypto from 'crypto';
function verifyWebhook(req) {
const signature = req.headers['x-mercentia-signature'];
const timestamp = req.headers['x-mercentia-timestamp'];
const body = JSON.stringify(req.body);
// 1. Reject if timestamp is older than 5 minutes
if (Date.now() - parseInt(timestamp) > 300000) {
throw new Error('Webhook timestamp too old');
}
// 2. Compute expected signature
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(`${timestamp}.${body}`)
.digest('hex');
// 3. Constant-time comparison to prevent timing attacks
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)) {
throw new Error('Invalid webhook signature');
}
return true;
}import hmac, hashlib, time, json
def verify_webhook(headers, body):
signature = headers.get('X-Mercentia-Signature')
timestamp = headers.get('X-Mercentia-Timestamp')
# Reject old timestamps (5 minute window)
if time.time() * 1000 - int(timestamp) > 300000:
raise ValueError('Webhook timestamp too old')
expected = hmac.new(
WEBHOOK_SECRET.encode(),
f'{timestamp}.{json.dumps(body)}'.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
raise ValueError('Invalid webhook signature')If your endpoint fails to respond with a 2xx status within 5 seconds, Mercentia retries with exponential backoff:
| Attempt | Delay | Total Elapsed |
|---|---|---|
| 1st retry | 30 seconds | ~30s |
| 2nd retry | 2 minutes | ~2.5m |
| 3rd retry | 10 minutes | ~12.5m |
| 4th retry | 30 minutes | ~42.5m |
| 5th retry | 1 hour | ~1h 42m |
| 6th retry (final) | 4 hours | ~5h 42m |
After 6 failed attempts, the event is logged to the dead letter queue. Persistent failures (50+ in a row) will trigger a warning email and may result in your webhook being paused.
X-Mercentia-Delivery-Id to deduplicate; the same event may be delivered more than once