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-JWTheaders) 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.bouncedorwhatsapp.receivedevents? - Why is my Bird
webhook-signatureverification failing? - How do I rotate a Bird webhook secret without dropping events?
- How do I dedupe Bird webhook retries?
Verification (core)
| Header | Value |
|---|---|
webhook-id | Identifies the delivery; retries and replays reuse it |
webhook-timestamp | Unix seconds of this delivery attempt (changes on every retry) |
webhook-signature | v1,<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
| Event | Fired when |
|---|---|
email.delivered | The receiving server accepted the message |
email.bounced | Permanent SMTP failure (bounce_type, bounce_class, bounce_code, bounce_description) |
email.complained | The recipient reported the message as spam |
whatsapp.received | An inbound WhatsApp message arrived |
preference.granted | A consent grant took effect (key: channel, handle, sender_scope, topic_id) |
preference.revoked | An 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
2xxwithin 15 seconds. Anything else, including3xxand timeouts, is a failure. Enqueue the event and process it asynchronously. - Return
400on 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_.
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_ or a sandbox send.
Reference Materials
- references/overview.md - Event catalog, payload fields, retries and replay
- references/setup.md - Create an endpoint (dashboard, CLI, API), signing secret, rotation, testing
- references/verification.md - Library and manual verification, gotchas, debugging