# Duffel Webhooks

## When to Use This Skill

* How do I receive Duffel webhooks?
* How do I verify the `X-Duffel-Signature` header?
* How do I handle `order.created` or `order.airline_initiated_change_detected` events?
* 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)

```javascript
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)

```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_TIMESTAMP_TOLERANCE_SECONDS` 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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/duffel-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/duffel-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/duffel-webhooks/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_initiated_change_detected` | An airline changed the schedule of an order |
| `order_cancellation.confirmed` | 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](https://github.com/hookdeck/webhook-skills/blob/main/skills/duffel-webhooks/references/overview.md).

> **For the full event reference**, see [Duffel receiving webhooks](https://duffel.com/docs/guides/receiving-webhooks).

## Important Headers

| Header | Description |
| --- | --- |
| `X-Duffel-Signature` | `t=<unix seconds>,v1=<lowercase hex HMAC-SHA256>` |

## Environment Variables

```bash
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

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 duffel --path /webhooks/duffel

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/duffel-webhooks/references/overview.md) - Duffel webhook concepts and events
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/duffel-webhooks/references/setup.md) - Creating a webhook and getting the secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/duffel-webhooks/references/verification.md) - Signature verification details