# Aircall Webhooks

Aircall is a cloud call-center / business phone system. Its webhooks push call, user,
number, contact, messaging, and conversation-intelligence events to your endpoint.

## When to Use This Skill

* How do I receive Aircall webhooks?
* How do I verify Aircall webhooks? (there is no signature header — see below)
* Why is my Aircall webhook verification failing?
* How do I handle `call.created`, `call.answered`, or `call.ended` events?
* How do I get my Aircall webhook token?
* Why did Aircall disable my webhook?

## Verification: Token in the Body, NOT an HMAC Signature

Aircall has no signature header and no cryptographic signature. Every event body
contains a top-level `token` string equal to the token issued when the webhook was
created. Verify by comparing that field against your stored token.

Do not look for `X-Aircall-Signature`, HMAC-SHA256, or Standard Webhooks headers — none
exist. Third-party blog posts that describe an Aircall HMAC header are wrong. (Aircall's
own docs loosely say "verify webhook signatures" in a code comment, but the mechanism is
a plain shared-secret comparison.)

### Verification (core)

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

// Aircall sends its shared secret verbatim as `token` in the JSON body.
// Compare in constant time so the token can't be recovered by timing.
function verifyAircallWebhook(payloadToken, expectedToken) {
  if (typeof payloadToken !== 'string' || !expectedToken) return false;
  try {
    return crypto.timingSafeEqual(
      Buffer.from(payloadToken),
      Buffer.from(expectedToken)
    );
  } catch {
    return false; // different lengths -> invalid
  }
}

// Usage: const { resource, event, timestamp, token, data } = req.body;
// if (!verifyAircallWebhook(token, process.env.AIRCALL_WEBHOOK_TOKEN)) -> 401

```

```python
import secrets

def verify_aircall_webhook(payload_token: str | None, expected_token: str | None) -> bool:
    if not payload_token or not expected_token:
        return False
    return secrets.compare_digest(payload_token, expected_token)

```

Because the secret is in the body, you do not need the raw body — parsed JSON is
fine here. (Raw body only matters for HMAC providers.) The token travels in cleartext,
so HTTPS is mandatory.

> For complete handlers with tests, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/aircall-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/aircall-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/aircall-webhooks/examples/fastapi/).

## Payload Envelope

Every event has exactly five top-level fields:

| Field | Type | Description |
| --- | --- | --- |
| `resource` | String | Resource for this event — `call`, `user`, `number`, `contact`, `message`, `integration`, `conversation_intelligence`, `ai_voice_agent`, `analytics` |
| `event` | String | Event name, e.g. `call.answered` |
| `timestamp` | Integer | UNIX timestamp (UTC) for when the payload was built |
| `token` | String | Webhook token — use this to verify |
| `data` | Object | The resource at `timestamp` |

```json
{
  "resource": "number",
  "event": "number.closed",
  "timestamp": 1585001020,
  "token": "45XXYYZZa08",
  "data": {
    "id": 456,
    "direct_link": "https://api.aircall.io/v1/numbers/123",
    "name": "My first Aircall Number",
    "digits": "+33 1 76 36 06 95",
    "country": "FR",
    "time_zone": "Europe/Paris",
    "open": false,
    "users": [{ "id": 456, "name": "Madelaine Dupont", "available": false }]
  }
}

```

`timestamp` is unsigned metadata. Do not use it as a replay/staleness control —
Aircall has no replay protection, so a tolerance check would only cause false rejections.

## Common Event Types

| Event | Triggered When | Common Use Cases |
| --- | --- | --- |
| `call.created` | Inbound call hits a number, or an agent starts an outbound call | Screen-pop, CRM lookup |
| `call.ringing_on_agent` | Call rings on a specific agent | Agent-level routing analytics |
| `call.answered` | An agent answers | Start call timer, log connect |
| `call.hungup` | Either party hangs up | Detect abandoned calls |
| `call.ended` | Call fully ended, assets finalized | Write call record, duration, cost |
| `call.tagged` / `call.untagged` | A tag is added/removed | Disposition reporting |
| `call.voicemail_left` | Caller leaves a voicemail | Voicemail follow-up queue |
| `message.received` | Inbound SMS/MMS/WhatsApp | Conversational inbox |
| `message.status_updated` | Outbound message status changes | Delivery tracking |
| `contact.created` / `contact.updated` | Contact changes | CRM sync |
| `user.connected.v2` / `user.disconnected.v2` | Agent opens/closes Workspace | Presence dashboards |
| `number.opened` / `number.closed` | Number enters/leaves business hours | Routing rules |
| `transcription.created` / `summary.created` | AI artifacts ready (AI Assist add-on) | Conversation intelligence |

> Full catalog (all 67 events, including User V1 vs V2 and AI Voice Agent): [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/aircall-webhooks/references/overview.md)

Use User V2 events (`user.created.v2`, …). V1 events are deprecated — Aircall's docs
say "This version of User events V1 will be deprecated soon. Please migrate to User
events V2."

## Delivery Semantics (Design Your Handler Around These)

* Respond 200 immediately — Aircall times out after 5 seconds. Process async.
* At least once, unordered — "an event will be delivered at least once, if generated,
  but events might not be delivered in a specific sequence/order." Handlers must be
  idempotent and must not assume ordering.
* Upsert on `call.id` — many events fire for one call; key your records on `data.id`.
* Auto-disable: a non-2xx or timeout is a failure; Aircall retries up to 50 times,
  then disables the webhook. It keeps retrying failed events for 12 hours; a success
  in that window automatically re-enables it.
* HTTPS required. No IP allowlist — "Aircall does not provide a list of static IP
  addresses to whitelist."

## Environment Variables

```bash
AIRCALL_WEBHOOK_TOKEN=df76g76dpziygs567f0   # `webhook.token` from POST /v1/webhooks

```

This is not your API key. API auth (`api_id:api_token` Basic Auth, or an OAuth2
Bearer token) is a separate secret used to manage webhooks.

## Local Development

```bash
npx hookdeck-cli listen 3000 aircall --path /webhooks/aircall

```

No account required — the CLI creates a guest account and gives you a public URL plus a
web UI for inspecting requests. Aircall requires HTTPS, which the tunnel provides.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/aircall-webhooks/references/overview.md) - Complete event catalog, payload shapes
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/aircall-webhooks/references/setup.md) - Create webhooks via API or Dashboard, get the token
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/aircall-webhooks/references/verification.md) - Token verification details and gotchas