Agent skill
Duffel Webhooks Skill
Receive and verify Duffel webhooks. Use when setting up Duffel flight API webhook handlers, debugging X-Duffel-Signature verification, or handling events like order.created, order.airline_initiated_change_detected, order_cancellation.confirmed, payment.succeeded, or ping.triggered.
Install this skill
npx skills add hookdeck/webhook-skills --skill duffel-webhooks
When to Use This Skill
- How do I receive Duffel webhooks?
- How do I verify the
X-Duffel-Signatureheader? - How do I handle
order.createdororder.airline_events?initiated_ change_ detected - Why is my Duffel webhook signature verification failing?
- How do I test a Duffel webhook with
ping.triggered?
Essential Code (USE THIS)
Duffel sends X-Duffel-Signature: t=<unix seconds>,v1=<lowercase hex>. v1 is HMAC-SHA256 of "<t>.<raw body>", keyed with the webhook secret string's bytes as-is. The secret looks base64 (e.g. 54vFWvaSbbzYpxXPeB4YEw==) but is not decoded. Duffel has no signature-verification SDK helper in its webhook guide, so verify manually.
Verification (Node.js)
const crypto = require('crypto');
// rawBody: Buffer of the exact request bytes (express.raw, not express.json)
function verifyDuffel(rawBody, signatureHeader, secret) {
if (!secret || !signatureHeader) return false;
const parts = {};
for (const kv of signatureHeader.split(',')) {
const i = kv.indexOf('=');
if (i > 0) parts[kv.slice(0, i).trim()] = kv.slice(i + 1).trim();
}
if (!parts.t || !parts.v1) return false;
// Optional replay window, off by default: retries and replays keep the original timestamp
const expected = crypto.createHmac('sha256', secret)
.update(`${parts.t}.`).update(rawBody).digest('hex');
const a = Buffer.from(parts.v1), b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Verification (Python)
import hashlib, hmac
def verify_duffel(raw_body: bytes, signature_header: str, secret: str) -> bool:
if not secret or not signature_header:
return False
parts = dict(kv.strip().split("=", 1) for kv in signature_header.split(",") if "=" in kv)
if "t" not in parts or "v1" not in parts:
return False
signed = parts["t"].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(parts["v1"].encode(), expected.encode())
Timestamp freshness is opt-in: set DUFFEL_ to a positive number to reject older t values. The default 0 skips the check, because Hookdeck and Duffel retries and replays keep the original timestamp.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Common Event Types
The event name is the type field in the JSON body.
| Event | Description |
|---|---|
ping.triggered | Sent when you call the webhook ping action (use it to test) |
order.created | An order was created |
order.airline_ | An airline changed the schedule of an order |
order_ | An order cancellation was confirmed |
payment.succeeded | A payment succeeded |
Duffel's guide also uses order.updated. Retries can deliver duplicates, so dedupe on idempotency_key. See references/overview.md.
For the full event reference, see Duffel receiving webhooks.
Important Headers
| Header | Description |
|---|---|
X-Duffel-Signature | t=<unix seconds>,v1=<lowercase hex HMAC-SHA256> |
Environment Variables
DUFFEL_WEBHOOK_SECRET=your_webhook_secret # "secret" returned by the create-webhook call (shown once)
DUFFEL_TIMESTAMP_TOLERANCE_SECONDS=0 # optional; a positive value opts in to the replay window
Live and test mode use separate webhooks, each with its own secret.
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 duffel --path /webhooks/duffel
Reference Materials
- references/overview.md - Duffel webhook concepts and events
- references/setup.md - Creating a webhook and getting the secret
- references/verification.md - Signature verification details