# 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](https://github.com/hookdeck/webhook-skills/blob/main/skills/usps-webhooks/references/verification.md).

Node:

```javascript
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:

```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:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/usps-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/usps-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/usps-webhooks/examples/fastapi/)

## Notification Envelope

```json
{
  "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. |

> `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](https://github.com/hookdeck/webhook-skills/blob/main/skills/usps-webhooks/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](https://developers.usps.com/trackingv3). Code defensively
> and keep a `default` branch for unrecognized statuses.

## Environment Variables

```bash
USPS_WEBHOOK_SECRET=your_32_character_subscription_secret   # The `secret` set when you created the subscription

```

## Local Development

```bash
# 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](https://github.com/hookdeck/webhook-skills/blob/main/skills/usps-webhooks/references/overview.md) - USPS tracking webhook concepts, events, retry/suspension behavior
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/usps-webhooks/references/setup.md) - OAuth token, creating a subscription, listener limits
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/usps-webhooks/references/verification.md) - X-HMAC signature verification details and gotchas