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
- You register an HTTPS endpoint in your SynthesQ tenant settings.
- SynthesQ POSTs a JSON payload to your endpoint within seconds of the event.
- Your endpoint returns
2xxto acknowledge receipt. Any other status triggers a retry.
Event payload structure
Every webhook payload shares the same envelope:
{
"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
| Event | Fires when |
|---|---|
order.confirmed | Order moves from draft → confirmed |
order.shipped | Order marked as shipped with tracking info |
order.closed | Order fully closed |
lead.converted | CRM lead converted to a customer/opportunity |
invoice.issued | Invoice sent to customer |
invoice.paid | Payment recorded against an invoice |
product.low_stock | Variant stock falls below reorder threshold |
Verifying signatures
SynthesQ signs every outbound payload with HMAC-SHA256. Your endpoint should verify this before processing:
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:
| Attempt | Delay |
|---|---|
| 1st retry | 30 s |
| 2nd retry | 5 min |
| 3rd retry | 30 min |
| 4th retry | 2 h |
| 5th retry | 6 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
// 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:
ngrok http 3000
# Gives you: https://abc123.ngrok.io
# Register https://abc123.ngrok.io/webhooks/synthesq in tenant settingsNext steps
- Authentication - generate tokens with appropriate abilities
- Error Handling - understand
4xx/5xxresponse shapes - API Reference - full event schema documentation