Skip to content

SynthesQ emits domain events whenever significant state changes occur - an order is confirmed, a lead is converted, stock drops below threshold. By subscribing to these events via webhooks, you can keep your external systems (CRM, ERP, fulfilment, analytics) in sync without polling.

How webhooks work

  1. You register an HTTPS endpoint in your SynthesQ tenant settings.
  2. SynthesQ POSTs a JSON payload to your endpoint within seconds of the event.
  3. Your endpoint returns 2xx to acknowledge receipt. Any other status triggers a retry.

Event payload structure

Every webhook payload shares the same envelope:

json
{
  "id": "evt_01JNABCDE12345",
  "event": "order.confirmed",
  "tenant_id": "ten_01J9RPMNVK4W",
  "occurred_at": "2026-02-10T14:32:00Z",
  "data": {
    "order_id": "ord_01JNAB99ZZ",
    "order_number": "SO-2026-0042",
    "customer_id": "cust_01J9RPMNVK4W",
    "total": 259.98,
    "currency": "USD",
    "status": "confirmed"
  }
}

The event field uses dot notation: {resource}.{action}. The data object shape varies per event type (see the API reference for the full schema).

Available event types

EventFires when
order.confirmedOrder moves from draft → confirmed
order.shippedOrder marked as shipped with tracking info
order.closedOrder fully closed
lead.convertedCRM lead converted to a customer/opportunity
invoice.issuedInvoice sent to customer
invoice.paidPayment recorded against an invoice
product.low_stockVariant stock falls below reorder threshold

Verifying signatures

SynthesQ signs every outbound payload with HMAC-SHA256. Your endpoint should verify this before processing:

ts
import { createHmac } from 'crypto'

function verifyWebhook(
  rawBody: string,
  signature: string,
  secret: string,
): boolean {
  const expected = createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex')
  // Use timing-safe comparison to prevent timing attacks
  return expected === signature
}

The signature is sent in the X-SynthesQ-Signature header. Your webhook secret is shown once at registration time - store it in an environment variable, never in source code.

Handling retries

SynthesQ retries failed deliveries with exponential backoff:

AttemptDelay
1st retry30 s
2nd retry5 min
3rd retry30 min
4th retry2 h
5th retry6 h

After 5 failed attempts the event is marked failed and no further retries are made. You can replay individual failed events from the tenant dashboard.

Idempotency

Use the id field to deduplicate events - your endpoint may receive the same event more than once if a retry fires after a slow 2xx response.

Example: sync orders to a fulfilment service

ts
// Express.js example
app.post('/webhooks/synthesq', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-synthesq-signature'] as string
  if (!verifyWebhook(req.body.toString(), sig, process.env.SYNTHESQ_WEBHOOK_SECRET!)) {
    return res.status(401).send('Invalid signature')
  }

  const event = JSON.parse(req.body.toString())

  if (event.event === 'order.confirmed') {
    // Fire-and-forget; acknowledge first to avoid timeout retries
    res.status(200).send('ok')
    sendToFulfilment(event.data)
    return
  }

  res.status(200).send('ok')
})

Acknowledge (200) immediately, then process asynchronously. If your handler throws after a 200, SynthesQ won't retry - so push the event onto a queue for reliable processing.

Testing locally

Use a tunnelling tool (e.g. ngrok) to expose a local endpoint during development:

bash
ngrok http 3000
# Gives you: https://abc123.ngrok.io
# Register https://abc123.ngrok.io/webhooks/synthesq in tenant settings

Next steps

Documentation for SynthesQ CRM/ERP Platform