---
title: Webhooks | API Docs
description: Real-time event notifications for messages, reactions, chats, and more.
---

Webhook Subscriptions allow you to receive real-time notifications when events occur on your account.

Configure webhook endpoints to receive events such as messages sent/received, delivery status changes, reactions, typing indicators, and more.

Failed deliveries (5xx, 429, network errors) are retried up to 10 times over \~25 minutes with exponential backoff. Each event includes a unique ID for deduplication.

### Webhook Headers

All webhook requests include two sets of headers. **If you have an existing integration using the `X-Webhook-*` headers, nothing changes** — those headers are still sent on every delivery and work exactly as before. The new `webhook-*` headers follow the [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) specification. You can safely ignore them if your current verification code works and you don’t want to use this convention.

#### Standard Webhooks Headers (Recommended)

Used by [our SDK](https://github.com/linq-team/linq-node) and any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks).

| Header              | Description                                        |
| ------------------- | -------------------------------------------------- |
| `webhook-id`        | Unique event identifier (use as idempotency key)   |
| `webhook-timestamp` | Unix timestamp (seconds) when the webhook was sent |
| `webhook-signature` | Standard Webhooks signature (`v1,{base64}` format) |

#### Legacy Headers (Deprecated)

Still sent on every delivery for backwards compatibility. Existing verification code using these headers continues to work — no changes required.

| Header                      | Description                                        |
| --------------------------- | -------------------------------------------------- |
| `X-Webhook-Event`           | *(deprecated)* Event type (e.g., `message.sent`)   |
| `X-Webhook-Subscription-ID` | *(deprecated)* Webhook subscription ID             |
| `X-Webhook-Timestamp`       | *(deprecated)* Unix timestamp (seconds)            |
| `X-Webhook-Signature`       | *(deprecated)* HMAC-SHA256 signature (hex-encoded) |

### Signing Secrets

Signing secrets use the Standard Webhooks format: a `whsec_` prefix followed by base64-encoded random bytes (e.g., `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw7Jxx2Oll+OE=`).

Strip the `whsec_` prefix and base64-decode the remainder to get the raw key bytes.

### Verifying Webhook Signatures

Webhooks are signed following the [Standard Webhooks specification](https://github.com/standard-webhooks/standard-webhooks). You can use any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks) to verify signatures, or implement verification manually:

**Signed content:** `{webhook-id}.{webhook-timestamp}.{body}`

**Verification Steps:**

1. Extract the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers
2. Reject if the timestamp is more than 5 minutes old (replay protection)
3. Get the raw request body bytes (do not parse and re-serialize)
4. Construct signed content: `"{webhook-id}.{webhook-timestamp}.{body}"`
5. Strip the `whsec_` prefix from your secret and base64-decode to get key bytes
6. Compute HMAC-SHA256 using the key bytes over the signed content
7. Base64-encode the result and compare with the value after `v1,` in `webhook-signature`
8. Use constant-time comparison to prevent timing attacks

**Example (Python):**

```
import base64, hmac, hashlib

def verify_webhook(secret, body, headers):
    msg_id = headers['webhook-id']
    timestamp = headers['webhook-timestamp']
    signature = headers['webhook-signature']

    secret_str = secret.removeprefix('whsec_')
    key = base64.b64decode(secret_str)

    signed_content = f"{msg_id}.{timestamp}.{body}"
    expected = base64.b64encode(
        hmac.new(key, signed_content.encode(), hashlib.sha256).digest()
    ).decode()

    for sig in signature.split(' '):
        if sig.startswith('v1,') and hmac.compare_digest(expected, sig[3:]):
            return True
    return False
```

**Example (Node.js):**

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

function verifyWebhook(secret, rawBody, headers) {
  const msgId = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signature = headers['webhook-signature'];

  const secretStr = secret.startsWith('whsec_') ? secret.slice(6) : secret;
  const keyBytes = Buffer.from(secretStr, 'base64');
  const signedContent = `${msgId}.${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac('sha256', keyBytes)
    .update(signedContent)
    .digest('base64');

  return signature.split(' ').some(sig => {
    if (!sig.startsWith('v1,')) return false;
    try {
      return crypto.timingSafeEqual(
        Buffer.from(expected, 'base64'),
        Buffer.from(sig.slice(3), 'base64')
      );
    } catch { return false; }
  });
}
```

**Security Best Practices:**

- Reject webhooks with timestamps older than 5 minutes to prevent replay attacks
- Always use constant-time comparison for signature verification
- Store your signing secret securely (e.g., environment variable, secrets manager)
- Return a 2xx status code quickly, then process the webhook asynchronously

## Setting up webhooks

Create a webhook subscription to start receiving events. Each subscription targets a URL you own, filters to the events you care about, and returns a signing secret you use to verify inbound requests. See [Webhook Subscriptions](/channel/imessage/guides/webhooks/subscriptions/index.md) for the full subscription lifecycle — create, list, retrieve, update, delete, and phone-number filtering.

## Webhook versioning

Webhook payloads are versioned using dates. Specify a version by adding `?version=YYYY-MM-DD` to your subscription URL:

```
https://your-server.com/webhook?version=2026-02-03
```

| Subscription created | Webhook version |
| -------------------- | --------------- |
| Before 2026-02-03    | `2025-01-01`    |
| 2026-02-03 or later  | `2026-02-03`    |

If no version is specified, the subscription uses the latest available version at creation time.

> **Tip:** Always specify a version explicitly to avoid unexpected payload format changes.

## Verifying signatures with an SDK

The **Verifying Webhook Signatures** section above describes the [Standard Webhooks](https://www.standardwebhooks.com/) signing scheme and how to verify it manually. If you use one of our SDKs, you don’t have to: `webhooks.unwrap()` does it for you — it checks the signature headers, throws if the signature is invalid, and returns the typed, discriminated event. Provide your signing secret via the `LINQ_WEBHOOK_SECRET` environment variable (or pass it to the client). **Always pass the raw request body — not parsed JSON — so the signature matches.**

- [TypeScript](#tab-panel-0-0)
- [Python](#tab-panel-0-1)
- [Go](#tab-panel-0-2)

```
import LinqAPIV3 from '@linqapp/sdk';


// webhookSecret defaults to process.env.LINQ_WEBHOOK_SECRET
const client = new LinqAPIV3();


// In your webhook handler:
const event = client.webhooks.unwrap(rawBody, { headers: req.headers });
// Throws on an invalid signature. `event` is fully typed.
```

```
from linq import LinqAPIV3


# webhook_secret defaults to os.environ["LINQ_WEBHOOK_SECRET"]
client = LinqAPIV3()


# In your webhook handler:
event = client.webhooks.unwrap(raw_body, headers=request.headers)
# Raises on an invalid signature. `event` is fully typed.
```

```
import "github.com/linq-team/linq-go" // imported as linqgo


// reads LINQ_WEBHOOK_SECRET from the environment
client := linqgo.NewClient()


// In your webhook handler:
event, err := client.Webhooks.Unwrap(rawBody, req.Header)
// err is non-nil on an invalid signature. event is the typed payload union.
```

## Delivery guarantees

| Guarantee          | Value                                         |
| ------------------ | --------------------------------------------- |
| Response timeout   | 10 seconds                                    |
| Retry attempts     | 10 per endpoint                               |
| Retry backoff      | Exponential with jitter, capped at 10 minutes |
| Total retry window | \~25 minutes                                  |
| Delivery model     | At-least-once (duplicates possible)           |

**Retried:** HTTP 5xx, HTTP 429, connection timeout, connection refused. **Not retried:** HTTP 4xx (except 429), DNS failures, invalid hostnames.

Your endpoint should:

1. Return `200` quickly — process asynchronously if needed
2. [Verify the signature](#verifying-signatures-with-an-sdk) using your subscription’s signing secret
3. Deduplicate using `event_id`
4. Be idempotent

## Related

- [Webhook Subscriptions](/channel/imessage/guides/webhooks/subscriptions/index.md) — create, list, update, delete subscriptions
- [Webhook Events](/channel/imessage/guides/webhooks/events/index.md) — full event list, shared envelope, and representative payloads
- [API Reference: Webhook Events](/channel/imessage/api/resources/webhook_events/index.md)
- [API Reference: Webhook Subscriptions](/channel/imessage/api/resources/webhook_subscriptions/index.md)
