Agent skill
USPS Webhooks Skill
Receive and verify USPS webhooks (Subscriptions - Tracking API v3.2). Use when setting up USPS tracking webhook handlers, debugging X-HMAC signature verification, creating tracking subscriptions, or handling package tracking events like Out for Delivery, Delivered, or Available for Pickup.
Install this skill
npx skills add hookdeck/webhook-skills --skill usps-webhooks
USPS delivers webhooks through the Subscriptions - Tracking API (v3.2). You create a subscription (POST /subscriptions) with a listenerURL, filterProperties (by Mailer ID or tracking number), and an optional 32-char secret. USPS then POSTs a notification to your listener URL every time a tracked package updates.
When to Use This Skill
- How do I receive USPS tracking webhooks?
- How do I verify the USPS
X-HMACwebhook signature? - How do I create a USPS tracking subscription?
- How do I handle
DeliveredorOut for Deliverytracking events? - Why is my USPS webhook signature verification failing?
Verification (core)
USPS signs timestamp + payload — the notification envelope's timestamp field concatenated with the raw, stringified payload field — with HMAC-SHA256 keyed on your subscription secret, and sends the Base64 digest in the X-HMAC header (deprecated alias: hmac-header).
You must parse the envelope to read timestamp and payload, then compute the HMAC over their concatenation. Do not re-serialize the inner payload — sign the raw string exactly as received. Compare timing-safe.
The OAuth2 token (used to create subscriptions) is not sent on delivery. Per-message authenticity comes from the
X-HMACsignature and/or IP allowlisting. If you set nosecretand no IP allowlist, there is no per-message verification.
When USPS_WEBHOOK_SECRET is unset, a subscription created without a secret sends no X-HMAC header at all — there is nothing to verify. Do not pass the missing secret into createHmac / hmac.new; that throws and turns a configuration problem into an opaque 500. The examples here branch explicitly: they log a one-time warning that notifications are being processed with no per-message verification (and that IP allowlisting should be used instead), then process the delivery. Swap that branch for a rejection if your deployment cannot rely on an allowlist — see references/verification.md.
Node:
const crypto = require('crypto');
function verifyUspsSignature(timestamp, payload, hmacHeader, secret) {
if (!hmacHeader || !secret) return false; // nothing to verify against
const expected = crypto
.createHmac('sha256', secret)
.update(timestamp + payload) // payload = raw stringified JSON, unmodified
.digest('base64');
try {
return crypto.timingSafeEqual(Buffer.from(hmacHeader), Buffer.from(expected));
} catch {
return false; // length mismatch = invalid
}
}
Python:
import hmac, hashlib, base64
def verify_usps_signature(timestamp: str, payload: str, hmac_header: str, secret: str) -> bool:
if not hmac_header or not secret: # nothing to verify against
return False
expected = base64.b64encode(
hmac.new(secret.encode(), (timestamp + payload).encode(), hashlib.sha256).digest()
).decode()
return hmac.compare_digest(hmac_header, expected)
For complete handlers with route wiring, event dispatch, and tests, see:
Notification Envelope
{
"subscriptionId": "a1b2c3d4-...",
"subscriptionType": "TRACKING",
"timestamp": "2026-07-23T14:32:00Z",
"payload": "{\"trackingNumber\":\"9400100000000000000000\",\"status\":\"Delivered\"}",
"links": [{ "rel": "self", "href": "https://api.usps.com/..." }]
}
payload is a stringified JSON — JSON.parse() it after verification to read tracking details. The HMAC is computed over timestamp + payload using the raw payload string (not the parsed object).
Event Types (Two Payload Schemas)
USPS has no event-name enum. The subscribable event filter exposes a single value, ALL_UPDATES, so USPS sends a notification for every update. What varies is the shape of the payload string, and the envelope subscriptionType tells you which of the two schemas you received:
| Schema | payload contains |
|---|---|
| Tracking Subscription Event | A tracking summary for one item — tracking number, current status, recent trackingEvents. Sent with subscriptionType: "TRACKING". |
| Scan Event Extract Subscription Event | A single raw scan record — one physical scan (event code, date/time, facility/ZIP, tracking number) rather than a rolled-up status. For feed-style ingestion of every scan. |
TRACKINGis the confirmedsubscriptionTypevalue. The exact string USPS sends for the scan event extract schema, and that payload's field names, could not be confirmed from the developer portal — log your first delivery, then add an explicit branch. Always keep a fallback branch for an unrecognizedsubscriptionType. See references/overview.md.
Within a Tracking Subscription Event payload, the status typically falls into these milestones:
| Tracking status | Fires when |
|---|---|
Pre-Shipment | Shipping label created, USPS awaiting the item |
Accepted | USPS has taken possession of the item |
In Transit | Item is moving through the USPS network |
Out for Delivery | Item is out for delivery today |
Delivered | Item was delivered |
Available for Pickup | Item is held at a facility for pickup |
Delivery Attempt | Delivery was attempted but not completed |
Alert | Exception or delay requiring attention |
The authoritative tracking payload schema and status values are defined by the USPS Tracking API. Code defensively and keep a
defaultbranch for unrecognized statuses.
Environment Variables
USPS_WEBHOOK_SECRET=your_32_character_subscription_secret # The `secret` set when you created the subscription
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 usps --path /webhooks/usps
Use the printed URL as the listenerURL when you create the subscription.
Reference Materials
- references/overview.md - USPS tracking webhook concepts, events, retry/suspension behavior
- references/setup.md - OAuth token, creating a subscription, listener limits
- references/verification.md - X-HMAC signature verification details and gotchas