Agent skill

Bird Webhooks Skill

Receive and verify Bird (bird.com, formerly MessageBird) webhooks, which follow the Standard Webhooks spec. Use when setting up a Bird webhook endpoint, getting the whsec_ signing secret, debugging webhook-id / webhook-timestamp / webhook-signature verification, rotating the secret, or handling events like email.delivered, email.bounced, email.complained, whatsapp.received, preference.granted and preference.revoked.

Install this skill

npx skills add hookdeck/webhook-skills --skill bird-webhooks


Bird is an omnichannel messaging platform (email, SMS, WhatsApp, voice, verify, consent preferences). When something happens in your workspace, Bird POSTs one signed JSON event to every webhook endpoint subscribed to that event type. Bird follows the Standard Webhooks specification, so the same verification code that works for other Standard Webhooks senders works here.

Not covered: the legacy MessageBird API (MessageBird-Signature / MessageBird-Signature-JWT headers) is a different scheme. This skill is for the current bird.com webhooks.

When to Use This Skill

  • How do I receive Bird webhooks?
  • How do I verify a Bird webhook signature?
  • Where do I find my Bird webhook signing secret (whsec_...)?
  • How do I handle email.delivered, email.bounced or whatsapp.received events?
  • Why is my Bird webhook-signature verification failing?
  • How do I rotate a Bird webhook secret without dropping events?
  • How do I dedupe Bird webhook retries?

Verification (core)

HeaderValue
webhook-idIdentifies the delivery; retries and replays reuse it
webhook-timestampUnix seconds of this delivery attempt (changes on every retry)
webhook-signaturev1,<base64 HMAC-SHA256>, several space-delimited entries during the 24h rotation overlap

Signed content is {webhook-id}.{webhook-timestamp}.{raw body}, HMAC-SHA256, base64. The key is the base64-decoded part of the endpoint secret after whsec_. Bird recommends rejecting timestamps more than 5 minutes old and deduplicating on webhook-id. The examples use the standardwebhooks library (npm and PyPI), which does the timestamp check and constant-time comparison for every v1, entry. A dependency-free version is in references/verification.md. Hookdeck retries and replays resend the original webhook-timestamp, so the library rejects them once they are more than 5 minutes old. If you receive through Hookdeck and need late retries to pass, use that dependency-free version and drop or widen its timestamp window.

const { Webhook } = require('standardwebhooks');

// rawBody: exact request body string, NOT re-serialised JSON
function verifyBirdWebhook(rawBody, headers, secret = process.env.BIRD_WEBHOOK_SECRET) {
  return new Webhook(secret).verify(rawBody, {
    'webhook-id': headers['webhook-id'],
    'webhook-timestamp': headers['webhook-timestamp'],
    'webhook-signature': headers['webhook-signature'],
  }); // returns the parsed event; throws → respond 400
}

Python: from standardwebhooks.webhooks import Webhook, then Webhook(secret).verify(await request.body(), headers). Read the body with express.raw({ type: '*/*' }) in Express so any content type verifies.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Envelope

{ "type": "email.delivered", "timestamp": "2026-06-10T14:30:00Z",
  "data": { "email_id": "em_...", "recipient": "user@example.com", "recipient_role": "to",
            "tags": [{ "name": "category", "value": "welcome" }], "metadata": { "order_id": "ord_123" } } }

type follows resource.action naming. The envelope timestamp is when the event occurred. Payloads are compact facts, not full resources, so fetch the resource by ID if you need more. Deliveries are not ordered: sort by the envelope timestamp, never by arrival. Event types are never renamed and new ones are added over time, so ignore types you don't recognise.

Common Event Types

EventFired when
email.deliveredThe receiving server accepted the message
email.bouncedPermanent SMTP failure (bounce_type, bounce_class, bounce_code, bounce_description)
email.complainedThe recipient reported the message as spam
whatsapp.receivedAn inbound WhatsApp message arrived
preference.grantedA consent grant took effect (key: channel, handle, sender_scope, topic_id)
preference.revokedAn opt-out took effect

Other types in the catalog include email.accepted, email.opened, email.clicked, email.unsubscribed, sms.*, whatsapp.delivered / .read / .failed, verify.*, voice_call.*, realtime.* and preference.deleted. See references/overview.md.

Delivery Semantics

  • Return a 2xx within 15 seconds. Anything else, including 3xx and timeouts, is a failure. Enqueue the event and process it asynchronously.
  • Return 400 on failed verification. Bird retries every failure (8 attempts over about 27.5 hours), so a wrong or not-yet-deployed secret can be fixed in time. No status code stops retries early.
  • Delivery is at-least-once with no batching. Dedupe on webhook-id.
  • Failed deliveries can be replayed with POST /v1/webhooks/{webhook_id}/replay.

Environment Variables

BIRD_WEBHOOK_SECRET=whsec_xxxxx   # endpoint signing secret; handlers fail closed (500) without it

Bird returns the secret exactly once, in the create-endpoint response. If you lose it, rotate it with POST /v1/webhooks/{webhook_id}/rotate-secret.

Local Development

# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 bird --path /webhooks/bird

Register the printed URL as a Bird webhook endpoint (HTTPS, publicly reachable), then trigger a delivery with POST /v1/webhooks/{webhook_id}/test or a sandbox send.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 3, 2026

View on GitHub →