# Lob Webhooks

## When to Use This Skill

* How do I receive Lob webhooks?
* How do I verify the `Lob-Signature` header (HMAC-SHA256 over `Lob-Signature-Timestamp` + `.` + body)?
* How do I handle `postcard.delivered`, `letter.mailed`, or `check.in_transit` events?
* Why does my Lob webhook signature verification fail for events from the webhook debugger?
* How do I add optional replay protection with `Lob-Signature-Timestamp`?

## Setup note

Events sent from Lob's webhook debugger are signed with a fixed test secret, so they fail verification. Real deliveries are signed with the webhook's secret.

## Verification (core)

Lob has no webhook SDK, so verify manually. `Lob-Signature` is the bare lowercase **hex**
HMAC-SHA256 digest (no prefix), keyed with the webhook's secret (its UTF-8 bytes, as-is), over
the **bytes** of: the `Lob-Signature-Timestamp` header value, a literal `.`, then the **raw
request body**. Never parse and re-serialize the body before verifying.

```javascript
const crypto = require('crypto');

function verifyLob(rawBody /* Buffer */, headers, secret) {
  const signature = headers['lob-signature'];
  const timestamp = headers['lob-signature-timestamp'];
  if (!secret || !signature || !timestamp) return false;
  if (!/^[0-9a-fA-F]{64}$/.test(signature)) return false; // validate before decoding

  const message = Buffer.concat([
    Buffer.from(timestamp, 'latin1'), // header exactly as received
    Buffer.from('.'),
    rawBody,                          // raw bytes, never a re-encoded string
  ]);
  const expected = crypto.createHmac('sha256', secret).update(message).digest();
  const received = Buffer.from(signature.toLowerCase(), 'hex'); // hex is case-insensitive
  return expected.length === received.length && crypto.timingSafeEqual(expected, received);
}

```

Respond `401` when a header is missing or empty, the signature is malformed, or it does not
match. In Express use `express.raw({ type: '*/*' })` so the exact bytes are verified whatever the
content type.

**Timestamp tolerance is off by default.** Lob's step 4 (reject old timestamps, 5 minutes
recommended) is optional. Hookdeck retries and replays re-send the original headers and
timestamp, and Hookdeck itself applies no window, so a fixed window would reject legitimate
retries. The examples offer an opt-in `LOB_WEBHOOK_TOLERANCE_SECONDS` (unset or `0` = off).
Deduplicate on the event `id` instead.

> **For complete handlers with route wiring, event dispatch, and tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/lob-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/lob-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/lob-webhooks/examples/fastapi/).

## Common Event Types

Lob delivers the entire event object as a `POST`. Dispatch on `event_type.id`; the mailpiece
or other resource is in the event's `body`. There are 93 event types in all (see [Lob's event types](https://docs.lob.com/#all_event_types)).

| `event_type.id` | Triggered When |
| --- | --- |
| `postcard.created` | A postcard is created |
| `postcard.delivered` | A postcard is delivered (USPS tracking event) |
| `postcard.re-routed` | A postcard is re-routed (note the hyphen) |
| `letter.mailed` | A letter is mailed |
| `letter.delivered` | A letter is delivered |
| `check.in_transit` | A check is in transit |
| `self_mailer.rendered_thumbnails` | Thumbnails for a self mailer have been rendered |

> **For the payload shape and more events**, see [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/lob-webhooks/references/overview.md).

## Important Headers

| Header | Description |
| --- | --- |
| `Lob-Signature` | Lowercase hex HMAC-SHA256 digest, no prefix |
| `Lob-Signature-Timestamp` | Timestamp that is part of the signed input (unit not documented) |

## Environment Variables

```bash
LOB_WEBHOOK_SECRET=your_lob_webhook_secret   # webhook details page in the Lob dashboard
LOB_WEBHOOK_TOLERANCE_SECONDS=0              # optional; unset or 0 = no timestamp check

```

## Local Development

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/lob-webhooks/references/overview.md) - Lob webhook concepts, events, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/lob-webhooks/references/setup.md) - Dashboard configuration and secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/lob-webhooks/references/verification.md) - Signature verification details and gotchas