Skip to content
Get started

Webhook Subscriptions

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 specification. You can safely ignore them if your current verification code works and you don’t want to use this convention.

Used by our SDK and any Standard Webhooks library.

HeaderDescription
webhook-idUnique event identifier (use as idempotency key)
webhook-timestampUnix timestamp (seconds) when the webhook was sent
webhook-signatureStandard 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.

HeaderDescription
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. You can use any Standard Webhooks library 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
Create a new webhook subscription
webhook_subscriptions.create(WebhookSubscriptionCreateParams**kwargs) -> WebhookSubscriptionCreateResponse
POST/v3/webhook-subscriptions
List all webhook subscriptions
webhook_subscriptions.list() -> WebhookSubscriptionListResponse
GET/v3/webhook-subscriptions
Get a webhook subscription by ID
webhook_subscriptions.retrieve(strsubscription_id) -> WebhookSubscription
GET/v3/webhook-subscriptions/{subscriptionId}
Update a webhook subscription
webhook_subscriptions.update(strsubscription_id, WebhookSubscriptionUpdateParams**kwargs) -> WebhookSubscription
PUT/v3/webhook-subscriptions/{subscriptionId}
Delete a webhook subscription
webhook_subscriptions.delete(strsubscription_id)
DELETE/v3/webhook-subscriptions/{subscriptionId}
ModelsExpand Collapse
class WebhookSubscription:
id: str

Unique identifier for the webhook subscription

created_at: datetime

When the subscription was created

formatdate-time
is_active: bool

Whether this subscription is currently active

subscribed_events: List[WebhookEventType]

List of event types this subscription receives

One of the following:
"message.sent"
"message.received"
"message.read"
"message.delivered"
"message.failed"
"message.edited"
"reaction.added"
"reaction.removed"
"participant.added"
"participant.removed"
"chat.created"
"chat.group_name_updated"
"chat.group_icon_updated"
"chat.group_name_update_failed"
"chat.group_icon_update_failed"
"chat.typing_indicator.started"
"chat.typing_indicator.stopped"
"phone_number.status_updated"
"call.initiated"
"call.ringing"
"call.answered"
"call.ended"
"call.failed"
"call.declined"
"call.no_answer"
"location.sharing.started"
"location.sharing.stopped"
target_url: str

URL where webhook events will be sent

formaturi
updated_at: datetime

When the subscription was last updated

formatdate-time
phone_numbers: Optional[List[str]]

Phone numbers this subscription filters for. If null or empty, events from all phone numbers are delivered.

class WebhookSubscriptionCreateResponse:

Response returned when creating a webhook subscription. Includes the signing secret which is only shown once.

id: str

Unique identifier for the webhook subscription

created_at: datetime

When the subscription was created

formatdate-time
is_active: bool

Whether this subscription is currently active

signing_secret: str

Secret for verifying webhook signatures. Store this securely - it cannot be retrieved again.

subscribed_events: List[WebhookEventType]

List of event types this subscription receives

One of the following:
"message.sent"
"message.received"
"message.read"
"message.delivered"
"message.failed"
"message.edited"
"reaction.added"
"reaction.removed"
"participant.added"
"participant.removed"
"chat.created"
"chat.group_name_updated"
"chat.group_icon_updated"
"chat.group_name_update_failed"
"chat.group_icon_update_failed"
"chat.typing_indicator.started"
"chat.typing_indicator.stopped"
"phone_number.status_updated"
"call.initiated"
"call.ringing"
"call.answered"
"call.ended"
"call.failed"
"call.declined"
"call.no_answer"
"location.sharing.started"
"location.sharing.stopped"
target_url: str

URL where webhook events will be sent

formaturi
updated_at: datetime

When the subscription was last updated

formatdate-time
phone_numbers: Optional[List[str]]

Phone numbers this subscription filters for. If null or empty, events from all phone numbers are delivered.

class WebhookSubscriptionListResponse:
subscriptions: List[WebhookSubscription]

List of webhook subscriptions

id: str

Unique identifier for the webhook subscription

created_at: datetime

When the subscription was created

formatdate-time
is_active: bool

Whether this subscription is currently active

subscribed_events: List[WebhookEventType]

List of event types this subscription receives

One of the following:
"message.sent"
"message.received"
"message.read"
"message.delivered"
"message.failed"
"message.edited"
"reaction.added"
"reaction.removed"
"participant.added"
"participant.removed"
"chat.created"
"chat.group_name_updated"
"chat.group_icon_updated"
"chat.group_name_update_failed"
"chat.group_icon_update_failed"
"chat.typing_indicator.started"
"chat.typing_indicator.stopped"
"phone_number.status_updated"
"call.initiated"
"call.ringing"
"call.answered"
"call.ended"
"call.failed"
"call.declined"
"call.no_answer"
"location.sharing.started"
"location.sharing.stopped"
target_url: str

URL where webhook events will be sent

formaturi
updated_at: datetime

When the subscription was last updated

formatdate-time
phone_numbers: Optional[List[str]]

Phone numbers this subscription filters for. If null or empty, events from all phone numbers are delivered.