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-HMAC webhook signature?
  • How do I create a USPS tracking subscription?
  • How do I handle Delivered or Out for Delivery tracking 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-HMAC signature and/or IP allowlisting. If you set no secret and 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 JSONJSON.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:

Schemapayload contains
Tracking Subscription EventA tracking summary for one item — tracking number, current status, recent trackingEvents. Sent with subscriptionType: "TRACKING".
Scan Event Extract Subscription EventA 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.

TRACKING is the confirmed subscriptionType value. 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 unrecognized subscriptionType. See references/overview.md.

Within a Tracking Subscription Event payload, the status typically falls into these milestones:

Tracking statusFires when
Pre-ShipmentShipping label created, USPS awaiting the item
AcceptedUSPS has taken possession of the item
In TransitItem is moving through the USPS network
Out for DeliveryItem is out for delivery today
DeliveredItem was delivered
Available for PickupItem is held at a facility for pickup
Delivery AttemptDelivery was attempted but not completed
AlertException or delay requiring attention

The authoritative tracking payload schema and status values are defined by the USPS Tracking API. Code defensively and keep a default branch 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Aug 3, 2026

View on GitHub →