# Guide to Aircall Webhooks: Features and Best Practices

Aircall webhooks notify your systems about cloud call-center activity: a call rings, an agent answers, a voicemail lands, a transcript is ready. If you're building on [Aircall](https://aircall.io)'s business phone platform, webhooks are how you drive screen-pops, CRM sync, and call analytics without polling.

This guide covers how Aircall webhooks work, the token-in-body verification model, the envelope and event catalog, the delivery semantics that shape your handler, and the best practices for production.

## What are Aircall webhooks?

Aircall delivers webhooks as HTTPS POSTs to an endpoint you register via the API or dashboard. The distinctive part is verification: Aircall has no signature header and no cryptographic signature. Every event body carries a top-level `token` string equal to the token issued when the webhook was created, and you verify by comparing that field against your stored token.

## Aircall webhook features

| Feature | Details |
| --- | --- |
| Configuration | `POST /v1/webhooks` or the dashboard; omitting the `events` array subscribes to all events; max 100 webhooks per company |
| Verification | Top-level `token` field in the JSON body, compared to the token issued at creation; no signature header |
| Envelope | Five fields: `resource`, `event`, `timestamp`, `token`, `data` |
| Events | 67 across calls, users, numbers, contacts, messaging, conversation intelligence, AI voice agent, and analytics |
| Timeout | 5 seconds; a slow response counts as a failure |
| Retries | Up to 50 per event, then the webhook is auto-disabled; failed events retry for up to 12 hours, and a success re-enables it |
| Delivery | At-least-once, with no ordering guarantee |
| Transport | HTTPS with a valid certificate required; no source IP allowlist |
| SDK | None |

## Common events

Aircall event names are the `event` field values. The catalog spans 67 events; the ones most integrations start with:

| Event | Fires when |
| --- | --- |
| `call.created` | An inbound call hits a number, or an agent starts an outbound call |
| `call.answered` | An agent answers |
| `call.hungup` | Either party hangs up |
| `call.ended` | The call fully ends and its assets are finalized |
| `call.tagged` / `call.untagged` | A tag is added or removed |
| `call.voicemail_left` | A caller leaves a voicemail |
| `message.received` | An inbound SMS, MMS, or WhatsApp message arrives |
| `contact.created` / `contact.updated` | A contact changes |
| `user.connected.v2` / `user.disconnected.v2` | An agent opens or closes their workspace |
| `number.opened` / `number.closed` | A number enters or leaves business hours |
| `transcription.created` / `summary.created` | AI artifacts are ready (AI Assist add-on) |

Branch on the `event` field. Two catalog details worth knowing: use the User V2 events (`user.created.v2` and friends), since Aircall's docs mark V1 for deprecation; and `user.closed.v2` fires twice when substatus is enabled, first with the availability status and then with the substatus, which is intended behavior rather than a duplicate.

> See Aircall webhook payloads in action. Inspect and replay sample Aircall webhook payloads in the [Hookdeck Console](https://console.hookdeck.com) — no account or setup required.

## Setting up Aircall webhooks

Create a webhook via the API (Basic Auth with `api_id:api_token`, or an OAuth2 Bearer token; both are API credentials, a different secret from the webhook token):

```bash
curl -X POST https://api.aircall.io/v1/webhooks \
  -u "$AIRCALL_API_ID:$AIRCALL_API_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "custom_name": "CRM sync",
    "url": "https://example.com/webhooks/aircall",
    "events": ["call.created", "call.ended", "contact.updated"]
  }'

```

The response includes `webhook.token`; store it, since it's what you'll verify against. Two subscription pitfalls: omitting `events` subscribes you to all 67 events, and on `PUT`, omitting `events` also resets the subscription to all unless you pass `events_action=add|remove`. Dashboard-created webhooks don't surface the token in the UI, so retrieve it with `GET /v1/webhooks/{webhook_id}`.

The endpoint must be HTTPS with a valid certificate. For local development, the [Hookdeck CLI](/docs/cli) (`hookdeck listen 3000 aircall --path /webhooks/aircall`) provides the HTTPS URL plus an inspector for replaying deliveries.

## Securing Aircall webhooks

Every delivery has exactly five top-level fields:

```json
{
  "resource": "call",
  "event": "call.ended",
  "timestamp": 1755001020,
  "token": "45XXYYZZa08",
  "data": { "id": 456, "...": "..." }
}

```

Verify the `token` field with a timing-safe comparison. Because the secret is in the body rather than a signature over it, you don't need the raw request body; parsing JSON before verifying is safe here.

```javascript
const crypto = require("crypto");

function verifyAircallToken(payloadToken, expectedToken) {
  if (typeof payloadToken !== "string" || !expectedToken) return false;
  try {
    return crypto.timingSafeEqual(
      Buffer.from(payloadToken),
      Buffer.from(expectedToken)
    );
  } catch {
    return false; // different lengths
  }
}

app.post("/webhooks/aircall", express.json(), (req, res) => {
  const { resource, event, token, data } = req.body;
  if (!verifyAircallToken(token, process.env.AIRCALL_WEBHOOK_TOKEN)) {
    return res.sendStatus(401);
  }

  // Respond within 5 seconds; process asynchronously
  processQueue.add({ resource, event, id: data?.id, data });
  res.sendStatus(200);
});

```

Don't use the `timestamp` field as a replay or staleness control. It's unsigned metadata with no replay protection behind it, so a tolerance check only causes false rejections. The token travels in cleartext in every payload, which is why HTTPS is mandatory.

> Make Aircall webhooks production-ready. [Hookdeck Event Gateway](/event-gateway) verifies the body token upstream, deduplicates, and durably queues every event.

## Aircall webhook limitations and pain points

### A shared token instead of a signature

The Problem: The `token` field authenticates the sender but signs nothing, so there's no payload-integrity guarantee and no replay protection, and the secret itself is present in every delivery.

Why It Happens: Aircall chose a shared-secret model over payload signing; TLS carries the integrity burden.

Workarounds:

* Compare the token timing-safely, rotate it by recreating the webhook if it leaks, and don't log request bodies with the token in them.

How Hookdeck Can Help: Hookdeck's Aircall source takes the webhook token as its one verification field and checks it at the edge, so unverified traffic never reaches your handler.

### A 5-second timeout that can disable your webhook

The Problem: A response slower than 5 seconds is a failure, 50 failed retries disable the webhook entirely, and after that you receive nothing until it's re-enabled.

Why It Happens: Aircall protects its delivery infrastructure from slow endpoints, and auto-disable is the backstop. Failed events retry for up to 12 hours, and one success in that window re-enables the webhook automatically.

Workarounds:

* Acknowledge immediately and process asynchronously, monitor for the dashboard's deactivation notification, and keep handler work off the response path.

How Hookdeck Can Help: Hookdeck acknowledges Aircall in milliseconds and [retries](/docs/retries) delivery to your handler on its own schedule, so a slow or crashed consumer never burns Aircall's 50-attempt budget or triggers auto-disable.

### At-least-once, unordered delivery

The Problem: The same event can arrive twice, and events for one call can arrive out of sequence, so a handler that inserts on `call.created` and updates on `call.ended` breaks when they arrive reversed.

Why It Happens: Aircall documents delivery as at least once with no ordering guarantee, and many events fire for a single call.

Workarounds:

* Upsert on `data.id` (Aircall's own recommendation) rather than assuming a lifecycle order, and make side effects idempotent.

How Hookdeck Can Help: [Deduplication rules](/docs/deduplication) drop repeats at the edge. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Subscription and token management pitfalls

The Problem: Omitting `events` on create or update silently subscribes you to all 67 events, the dashboard never shows a webhook's token, and messaging events only arrive in native mode (numbers in Proxy mode deliver to a separately configured callback URL).

Why It Happens: The API treats an absent `events` array as "everything", and the token is API-retrievable only.

Workarounds:

* Always pass an explicit `events` array (or `events_action` on updates), and fetch tokens via `GET /v1/webhooks/{webhook_id}`.

How Hookdeck Can Help: [Filters](/docs/filters) let you subscribe broadly but deliver narrowly, routing only the events each handler cares about, and the dashboard shows exactly which event types are actually arriving.

## Best practices

### Verify the body token on every request

Timing-safe comparison against the stored webhook token, 401 on mismatch. Skip the raw-body plumbing; it isn't needed here.

### Respond fast, process asynchronously

You have 5 seconds, and slow responses count against the 50-attempt disable budget. Acknowledge, then queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Upsert on data.id

Many events fire per call and arrive in no guaranteed order. Key call records on `data.id` and upsert.

### Subscribe explicitly

Pass an `events` array on create and `events_action` on update, or you'll receive all 67 event types.

### Prefer User V2 events

The un-suffixed user events are deprecated; use the `.v2` names.

### Watch for deactivation

Auto-disable means silence, not errors. Alert on webhook inactivity so a disabled webhook doesn't go unnoticed past the 12-hour re-enable window.

## Conclusion

Aircall webhooks verify with a token in the body rather than an HMAC signature, deliver at least once in no guaranteed order, and give you 5 seconds to respond before a failure counts toward the 50 retries that disable the webhook. Verify the token timing-safely, acknowledge fast, upsert on `data.id`, and subscribe to events explicitly.

[Hookdeck Event Gateway](https://hookdeck.com) verifies the token at the edge, absorbs Aircall's delivery pace, deduplicates, and retries your handler independently, so Aircall's timeout and auto-disable rules stop being constraints your code has to satisfy.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Aircall webhooks reliably in minutes.