Webhooks
How to subscribe to AISAR events, verify delivery signatures, and handle retries.
Webhooks let you receive AISAR events (a new message, a deal change, a delivery status update, and so on) in real time: the platform sends a POST request to your URL for every event, instead of you polling the API.
Subscribing to events
A subscription is created as a webhook-type integration:
curl -X POST https://api.aisar.app/v1/integrations \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "webhook",
"name": "My integration",
"status": "active",
"settings": {
"url": "https://my-app.example.com/aisar/webhook",
"selectedEventKeys": ["message.created", "deal.updated"],
"signatureHeader": "X-AISAR-Signature",
"timeoutSeconds": 30,
"retryEnabled": true,
"maxAttempts": 6
},
"secrets": {
"signingSecret": "YOUR_SIGNING_SECRET"
}
}'| `settings.*` field | Description |
|---|---|
url | Where to deliver events (http/https, checked by an SSRF guard) |
selectedEventKeys | The list of event keys the integration subscribes to (e.g. message.created) |
channelFilter | "all" (default) or an array of channel IDs — only deliver events for those channels |
signatureHeader | Header name carrying the HMAC signature, defaults to X-AISAR-Signature |
authType | none (default), bearer or basic — adds an Authorization header in addition to the signature |
timeoutSeconds | Delivery request timeout, 5–60 seconds, defaults to 30 |
retryEnabled | Whether retries on failure are enabled, defaults to true |
maxAttempts | Maximum delivery attempts, 1–10, defaults to 6 |
The signing secret is passed separately, in secrets.signingSecret, and is never returned back in API responses. The full event catalogue is in the webhooks reference (messages, conversations, contacts, deals, funnels, broadcasts, channels, templates, automations, and more).
Delivery format
Your url receives a POST request with a JSON body and a set of headers:
POST {webhook_url}
Content-Type: application/json
X-AISAR-Event: message.created
X-AISAR-Delivery-Id: evt_01JXXXXXXXXXXXXXXXXXXXXX_45
X-AISAR-Signature: sha256=...
User-Agent: AISAR-Webhook/1.0Every event shares a common envelope:
{
"event": "message.created",
"event_id": "evt_01JXXXXXXXXXXXXXXXXXXXXX",
"timestamp": "2026-03-22T12:00:00.000000Z",
"company_id": 45,
"data": {
"message": {
"id": 123,
"conversation_id": 45,
"channel_id": 2,
"direction": "inbound",
"type": "text",
"content": { "text": "Hello, I need help" },
"created_at": "2026-03-22T12:00:00.000000Z"
},
"sender": {
"type": "contact",
"contact_id": 10,
"name": "John Doe",
"phone": "+77001234567"
},
"conversation": { "id": 45, "status": "active", "is_group": false },
"channel": { "id": 2, "type": "whatsapp", "name": "Main WhatsApp" }
}
}| Envelope field | Description |
|---|---|
event | The event type key, e.g. message.created |
event_id | Unique event ID (ULID), matches the X-AISAR-Delivery-Id header |
timestamp | ISO 8601 time the event was created |
company_id | The company ID the event originated from |
data | The event payload, its shape depends on event (see the specific event's reference) |
Verifying the signature
If secrets.signingSecret is configured on the integration, every delivery is signed with HMAC-SHA256 over the exact request body (the raw JSON bytes, before any parsing) using that secret. The signature is sent in the header (X-AISAR-Signature by default) as sha256={hex}.
Always verify the signature against the raw request body before parsing it, and use a timing-safe comparison (hash_equals in PHP, crypto.timingSafeEqual in Node.js).
<?php
function verifyAisarSignature(string $rawBody, string $signatureHeader, string $secret): bool
{
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signatureHeader);
}
// $rawBody must be the raw request body (e.g. file_get_contents('php://input')),
// not json_decode()'d and re-encoded.
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_AISAR_SIGNATURE'] ?? '';
if (! verifyAisarSignature($rawBody, $signature, 'YOUR_SIGNING_SECRET')) {
http_response_code(401);
exit;
}
$event = json_decode($rawBody, true);
const crypto = require('crypto');
function verifyAisarSignature(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Read the raw body with a middleware that does NOT parse JSON first
// (e.g. express.raw({ type: 'application/json' })).
app.post('/aisar/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.header('X-AISAR-Signature') || '';
if (!verifyAisarSignature(req.body, signature, process.env.AISAR_SIGNING_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
res.sendStatus(200);
});
Retries and timeouts
Delivery runs asynchronously through a queue. The request does not follow redirects. The request timeout is settings.timeoutSeconds (5–60 sec, defaults to 30).
A delivery is considered successful on a 2xx HTTP status. On failure (a non-2xx response, a timeout, or a network error) with retries enabled (retryEnabled, default true), AISAR retries with increasing backoff until maxAttempts is reached (defaults to 6):
| Attempt | Delay before it |
|---|---|
| 1 → 2 | 30 sec |
| 2 → 3 | 60 sec |
| 3 → 4 | 2 min |
| 4 → 5 | 5 min |
| 5 → 6 | 10 min |
| 6 and beyond | 30 min |
A 429 response from your server is handled specially: if you send a Retry-After header, AISAR waits at least that long. 4xx statuses other than 429 are not retried — the delivery is immediately marked failed. After maxAttempts failed attempts the delivery is marked failed for good. If a large number of consecutive deliveries fail, the integration is automatically switched to disabled.
Test delivery
Before subscribing to real events, verify that your endpoint accepts the request and the signature checks out:
curl -X POST https://api.aisar.app/v1/integrations/test-webhook \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://my-app.example.com/aisar/webhook",
"signingSecret": "YOUR_SIGNING_SECRET"
}'The test delivery event is webhook.test; the body carries the placeholder message "This is a test webhook delivery from AISAR." inside the same event/event_id/timestamp/company_id/data envelope. The target URL is checked by the same SSRF guard as real deliveries (private/loopback/link-local/metadata addresses are refused).
Viewing delivery logs
curl "https://api.aisar.app/v1/integrations/{integration}/logs?limit=50" \
-H "Authorization: Bearer YOUR_API_TOKEN"For a webhook-type integration this returns events with their nested delivery attempts (URL, response code, response body, whether a signature was sent, request duration, and the error if any) — use it to debug missing events. limit ranges from 1 to 200, defaulting to 50.