Gareth Wilson Gareth Wilson

Guide to Lob Webhooks: Features and Best Practices

Published


Lob webhooks tell your systems what happens to the mail you send through Lob: a letter is rendered, mailed, scanned in transit, delivered, re-routed or returned. Lob is a print-and-mail and address verification API, and its webhooks carry both its own processing events and the USPS tracking events it receives, so you can notify customers, trigger follow-ups and reconcile mail without polling.

This guide covers how Lob webhooks are created and signed, the event object and event types, Lob's retry, disable and replay rules, and the practices that keep a Lob receiver reliable.

What are Lob webhooks?

A Lob webhook is a subscription: a URL plus the event types it should receive, in either the Test or the Live environment. When a subscribed event occurs in that environment, Lob sends a POST with the whole event object as JSON.

Each webhook has its own secret, shown on its details page in the dashboard. Every delivery carries a Lob-Signature-Timestamp header and a Lob-Signature header, the hex HMAC-SHA256 of the timestamp, a ., and the raw body. One exception catches people out: events sent from Lob's webhook debugger are signed with the literal string secret, not the webhook's secret.

Lob webhook features

FeatureDetails
ConfigurationDashboard Webhooks page, per environment (Test or Live)
LimitsLive webhooks are limited by your Print & Mail edition. No limit in Test
Rate limitOptional, per webhook, in requests per 5-second interval
Signature headerLob-Signature, bare hex
Timestamp headerLob-Signature-Timestamp. Format not documented
SchemeHMAC-SHA256
Signed content<timestamp>.<raw body>
SecretOne per webhook, on its details page. Debugger events use secret
Other authOptional Basic auth from https://user:pass@... in the webhook URL
PayloadThe full event object. PII and PHI fields are redacted by default
TLSHTTPS only, with a fully chained certificate
Retries8 attempts over about 27.5 hours
Auto-disableAfter 5 consecutive days of failures, with an email
ReplayFrom the dashboard, for all non-2xx events since a start date
Source IPsNot published

Common events

Lob lists about 93 event types, grouped by resource. The common ones follow the same pattern for postcards, self mailers, letters and checks:

StageEvents (letters shown)Fires when
Processingletter.created, letter.rejected, letter.rendered_pdf, letter.rendered_thumbnails, letter.deletedThe mailpiece is created or rejected at creation, its PDF or thumbnails are ready, or it's canceled
USPS trackingletter.mailed, letter.in_transit, letter.in_local_area, letter.processed_for_delivery, letter.deliveredUSPS scans the piece through the mailstream
Exceptionsletter.re-routed, letter.returned_to_sender, letter.failed, letter.international_exitThe piece gets a Re-Routed, Returned to Sender or International Exit scan, or a failed rendering or tracking event
Engagementletter.viewed, letter.informed_delivery.email_sent, .email_opened, .email_clicked_throughThe recipient scans the QR code or opens the URL, or acts on an Informed Delivery email
Certified mailletter.certified.mailed, .in_transit, .delivered, .pickup_available, .issue and moreTracking for certified letters
Return envelopesletter.return_envelope.created, .in_transit, .processed_for_delivery, .returned_to_sender and moreTracking for USPS Courtesy Reply Mail, for Enterprise customers
Other resourcesaddress.created, address.deleted, bank_account.created, bank_account.deleted, bank_account.verifiedAddress and bank account changes

Note the hyphen in re-routed. Tracking events exist only in the Live environment, so you can't subscribe to them in Test. Use the debugger to send a mock one.

Every delivery is an event object. Here is a trimmed version of Lob's example, with redacted fields:

{
  "event_type": {
    "resource": "self mailers",
    "enabled_for_test": true,
    "id": "self_mailer.rendered_thumbnails",
    "object": "event_type"
  },
  "reference_id": "sfm_c29bbbca5f7bd489",
  "date_created": "2021-12-16T01:34:18.159Z",
  "id": "evt_d47ff1b815373f67",
  "body": {
    "id": "sfm_c29bbbca5f7bd489",
    "description": "self mailer create",
    "metadata": { "secretId": "REDACTED" },
    "to": { "name": "REDACTED", "address_line1": "REDACTED" },
    "url": "REDACTED",
    "carrier": "USPS",
    "tracking_events": [],
    "size": "6x18_bifold",
    "mail_type": "usps_first_class",
    "expected_delivery_date": "2021-12-27",
    "send_date": "2021-12-16T01:39:12.078Z",
    "object": "self_mailer"
  },
  "object": "event"
}

Route on event_type.id. The mailpiece or other resource is in body. By default Lob replaces the to and from addresses, URLs, metadata, descriptions and merge variables with REDACTED, plus the memo and bank details on checks. You can change this under Account > Webhooks Payload in settings, but Lob can't recover payload data retroactively for events sent while redaction was on. Events follow the API version on your account at the time the event is created.

Setting up Lob webhooks

  1. In the Lob dashboard, select Webhooks, choose the Test or Live environment and click Create.
  2. Enter a description and your HTTPS URL, set a rate limit if you need one, and select the event types.
  3. Click Create, then open the webhook's details page and copy its secret, for example as LOB_WEBHOOK_SECRET.

Your endpoint must accept POST with application/json, be reachable by Lob, and present a fully chained TLS certificate, or every attempt fails. Exempt it from CSRF protection if your framework enables that by default.

With Hookdeck. Lob is a supported source type in Hookdeck. Create a source with the Lob type, paste the webhook's secret from its details page, and use the Hookdeck source URL as the webhook URL in Lob. 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. The source accepts POST only and checks Lob-Signature against <Lob-Signature-Timestamp>.<raw body>. It doesn't check Basic auth credentials. A request that fails the check still gets a 200 from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service.

Securing Lob webhooks

Join the timestamp header, a literal ., and the raw body bytes. Compute an HMAC-SHA256 with the webhook's secret as a UTF-8 string and compare the hex digests in constant time.

const crypto = require('crypto');
const express = require('express');

function verifyLobSignature(rawBody, signature, timestamp, secret) {
  if (!signature || !timestamp || !secret) return false;
  if (!/^[0-9a-f]{64}$/i.test(signature)) return false; // validate before decoding

  const signed = Buffer.concat([Buffer.from(`${timestamp}.`, 'latin1'), rawBody]);
  const expected = crypto.createHmac('sha256', secret).update(signed).digest();
  const received = Buffer.from(signature, 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/lob', express.raw({ type: '*/*' }), (req, res) => {
  // Use the string "secret" only when testing with Lob's webhook debugger.
  const secret = process.env.LOB_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  const ok = verifyLobSignature(
    req.body,
    req.get('lob-signature'),
    req.get('lob-signature-timestamp'),
    secret
  );
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on event.id, route on event.event_type.id, then process asynchronously.
  res.sendStatus(200);
});

The same check in Python:

import hashlib
import hmac
import re

HEX_64 = re.compile(r"[0-9a-fA-F]{64}")


def verify_lob_signature(raw_body: bytes, signature, timestamp, secret: str) -> bool:
    if not signature or not timestamp or not secret:
        return False
    if not HEX_64.fullmatch(signature):
        return False
    signed = timestamp.encode("latin-1") + b"." + raw_body
    expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).digest()
    return hmac.compare_digest(bytes.fromhex(signature), expected)

Lob's fourth verification step, checking that the timestamp is within a tolerance (it suggests 5 minutes), is marked optional. Lob doesn't say what format Lob-Signature-Timestamp uses, so confirm it from a real delivery before you add such a check. Hookdeck applies no timestamp tolerance, and its retries and replays resend the original headers. Lob's own replays may also arrive long after the event, so a strict window is a poor fit here. Deduplicate on the event id instead.

Make Lob webhooks production-ready. Hookdeck Event Gateway verifies the Lob-Signature header, stores every mailpiece and tracking event, and delivers it to your services with retries you control.

Lob webhook limitations and pain points

Debugger events never verify

The Problem: Test events from the debugger fail verification with the webhook's real secret.

Why It Happens: Lob signs debugger requests with the fixed string secret. The webhook's own secret signs only real deliveries.

Workarounds:

  • Verify debugger traffic with secret in a test environment only, never in production.
  • Test the real secret with Test-environment events from your Test API key.

How Hookdeck Can Help: Hookdeck shows each debugger request as failed verification in the request log, with the full headers and body, so you can still inspect the debugger's event shape.

Tracking events are delayed and arrive in batches

The Problem: Events arrive late, or several tracking events arrive together.

Why It Happens: Lob says events may not be sent in real time. When you subscribe to a later tracking event such as postcard.processed_for_delivery, you also receive the earlier ones, such as postcard.in_transit and postcard.mailed.

Workarounds:

  • Use body.tracking_events[].time for the USPS scan time, not the arrival time of the webhook.
  • Make state transitions tolerant of events that arrive out of order.

How Hookdeck Can Help: Hookdeck filters can route tracking events to a different destination from processing events, so a burst of scans doesn't hold up other work.

Redacted payloads

The Problem: Addresses, URLs and metadata come through as REDACTED.

Why It Happens: Lob redacts fields that could contain PII or PHI by default, and can't recover them for events sent while redaction was on.

Workarounds:

  • Look up the mailpiece by reference_id through the API when you need unredacted fields.
  • Change the Webhooks Payload setting only if your receiver is approved to hold that data.

How Hookdeck Can Help: Keeping redaction on means Hookdeck stores only the redacted payload, which limits where sensitive data lives.

A long outage disables the subscription

The Problem: After a few days of failures, events stop entirely.

Why It Happens: Lob makes 8 attempts over about 27.5 hours, then gives up on that event. If every webhook for a subscription fails for 5 consecutive days, Lob disables it and emails you.

Workarounds:

  • Re-enable the webhook in the dashboard after fixing the endpoint, then use Replay Events with a start date to queue every non-2xx event since then.
  • Return 2xx quickly so timeouts don't count as failures.

How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives, so your downtime doesn't count against Lob's disable policy, and then delivers to your service with automatic retries on your schedule.

Best practices

Verify against the raw body

Lob warns that parsing or re-serialising the body can change it. Use express.raw() in Express, await request.arrayBuffer() in a Next.js route handler, and await request.body() in FastAPI.

Deduplicate on the event id

Retries and dashboard replays resend events. Store each evt_... ID and skip ones you've already processed. See our guide to webhook idempotency.

Return useful response bodies

Lob stores whatever you return, whatever the status, and shows it in the dashboard. A short JSON body with an error reason makes failed attempts easier to debug.

Set a rate limit if your service needs one

Lob can throttle deliveries per webhook, in requests per 5-second interval. Use it when a burst of tracking events could overwhelm your receiver.

Acknowledge fast and process asynchronously

Lob asks for a 2xx "in a timely manner" and recommends returning it straight away, then doing complex work afterwards. See why to process webhooks asynchronously.

Conclusion

Lob signs each delivery with Lob-Signature, a bare hex HMAC-SHA256 of <Lob-Signature-Timestamp>.<raw body>, keyed with the webhook's secret, except for debugger events, which use secret. Verify against the raw bytes, route on event_type.id, deduplicate on the event id, and expect tracking events to arrive late, together and redacted.

Hookdeck Event Gateway sits in front of your receiver, checks every Lob signature, and holds each event until your service has processed it, so an outage on your side doesn't lead Lob to disable your subscription.

Get started with Hookdeck for free and handle Lob webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.