AISARAISAR

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:

Create a webhook 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.*` fieldDescription
urlWhere to deliver events (http/https, checked by an SSRF guard)
selectedEventKeysThe 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
signatureHeaderHeader name carrying the HMAC signature, defaults to X-AISAR-Signature
authTypenone (default), bearer or basic — adds an Authorization header in addition to the signature
timeoutSecondsDelivery request timeout, 5–60 seconds, defaults to 30
retryEnabledWhether retries on failure are enabled, defaults to true
maxAttemptsMaximum 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:

text
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.0

Every event shares a common envelope:

Example: message.created
{
  "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 fieldDescription
eventThe event type key, e.g. message.created
event_idUnique event ID (ULID), matches the X-AISAR-Delivery-Id header
timestampISO 8601 time the event was created
company_idThe company ID the event originated from
dataThe 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).

Verifying the signature — PHP
<?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);
Verifying the signature — Node.js
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):

AttemptDelay before it
1 → 230 sec
2 → 360 sec
3 → 42 min
4 → 55 min
5 → 610 min
6 and beyond30 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:

Test delivery
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

Integration 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.