← All Documentation

Webhooks

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.

Setup

Your webhook URL is registered when you create your app. It must:

  • Use HTTPS (HTTP will be rejected)
  • Respond within 5 seconds with a 2xx status code
  • Accept POST requests with Content-Type: application/json
  • Be publicly accessible (no localhost, unless using ngrok for development)

Event Types

EventTriggerScope Required
order.createdNew order placedread_orders
order.paidPayment confirmedread_orders
order.fulfilledOrder shipped/fulfilledread_orders
order.cancelledOrder cancelledread_orders
order.refundedRefund issuedread_orders
product.createdNew product addedread_products
product.updatedProduct details changedread_products
product.deletedProduct removedread_products
customer.createdNew customer registeredread_customers
customer.updatedCustomer profile changedread_customers
inventory.low_stockStock below thresholdread_inventory
inventory.out_of_stockStock reaches zeroread_inventory
cart.abandonedCart inactive for 30+ minutesread_orders
subscription.createdNew subscription startedread_orders
subscription.cancelledSubscription cancelledread_orders
collection.createdNew collection addedread_products
collection.updatedCollection changedread_products
app.installedYour app was installedNone (always sent)
app.uninstalledYour app was removedNone (always sent)
You only receive events for scopes your app has been granted. If a merchant grants read_orders but not read_customers, you will receive order events but not customer events.

Payload Format

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)
  }
}

Headers

HeaderDescription
Content-Typeapplication/json
X-Mercentia-SignatureHMAC-SHA256 signature for verification
X-Mercentia-TimestampUnix timestamp (ms) when the event was sent
X-Mercentia-EventEvent type (e.g., order.created)
X-Mercentia-Delivery-IdUnique delivery ID for idempotency
User-AgentMercentia-Webhooks/1.0

Example: order.created

{
  "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": "customer@example.com",
    "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"
  }
}

Signature Verification

Always verify webhook signatures to ensure requests genuinely come from Mercentia.

Node.js
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;
}
Python
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')

Retry Policy

If your endpoint fails to respond with a 2xx status within 5 seconds, Mercentia retries with exponential backoff:

AttemptDelayTotal Elapsed
1st retry30 seconds~30s
2nd retry2 minutes~2.5m
3rd retry10 minutes~12.5m
4th retry30 minutes~42.5m
5th retry1 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.

Best Practices

  • Respond fast — return 200 immediately, then process asynchronously
  • Be idempotent — use the X-Mercentia-Delivery-Id to deduplicate; the same event may be delivered more than once
  • Verify signatures — always validate before processing
  • Handle unknown events — return 200 for event types you don't recognise; new events may be added
  • Use a queue — push webhook payloads into a job queue (Redis, SQS, etc.) and process them asynchronously
  • Monitor failures — track your webhook success rate and set up alerts for failures
  • Log everything — store the delivery ID, timestamp, event type, and processing result for debugging