Agent skill

Lob Webhooks Skill

Receive and verify Lob webhooks. Use when setting up Lob print-and-mail webhook handlers, debugging Lob-Signature verification, or handling events like postcard.delivered, letter.mailed, check.in_transit, and self_mailer.rendered_thumbnails.

Install this skill

npx skills add hookdeck/webhook-skills --skill 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.

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/, examples/nextjs/, 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).

event_type.idTriggered When
postcard.createdA postcard is created
postcard.deliveredA postcard is delivered (USPS tracking event)
postcard.re-routedA postcard is re-routed (note the hyphen)
letter.mailedA letter is mailed
letter.deliveredA letter is delivered
check.in_transitA check is in transit
self_mailer.rendered_thumbnailsThumbnails for a self mailer have been rendered

For the payload shape and more events, see references/overview.md.

Important Headers

HeaderDescription
Lob-SignatureLowercase hex HMAC-SHA256 digest, no prefix
Lob-Signature-TimestampTimestamp that is part of the signed input (unit not documented)

Environment Variables

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

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

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 10, 2026

View on GitHub →