Agent skill

Shippo Webhooks Skill

Receive and verify Shippo webhooks. Use when setting up Shippo webhook handlers, debugging Shippo-Auth-Signature HMAC verification, securing endpoints with a URL token or IP allowlist, or handling shipping events like track_updated, transaction_created, transaction_updated, batch_created, batch_purchased, or automations_label_resolved.

Install this skill

npx skills add hookdeck/webhook-skills --skill shippo-webhooks


When to Use This Skill

  • How do I receive Shippo webhooks (tracking updates, label purchases, batches)?
  • How do I verify the Shippo Shippo-Auth-Signature header?
  • How do I secure a Shippo webhook endpoint without HMAC (URL token, IP allowlist)?
  • How do I handle track_updated or transaction_created events?
  • Why is my Shippo webhook signature verification failing?

How Shippo Secures Webhooks

Shippo offers three options, which can be combined:

  1. HMAC signature (strongest). Not self-serve: you ask your Shippo account manager (or sales) for "HMAC Webhook Setup", and Shippo provisions the secret. Setup can take up to 10 business days.
  2. Self-generated token in the webhook URL, e.g. https://your.app/webhooks/shippo?token=<random>. Shippo sends it back as a query parameter, so you compare it to your stored value.
  3. IP allowlist of Shippo's published US/EU egress IPs (see references/setup.md).

Verification (core)

The HMAC header looks like Shippo-Auth-Signature: t=1688493073,v1=24036c00…. The signature is the hex HMAC-SHA256 of "{t}.{raw body}", keyed with the secret Shippo gave you. No Shippo SDK has a verification helper, so you verify manually, against the raw request body (don't JSON.parse first).

const crypto = require('crypto');

// Replay check is off by default (toleranceSec = 0): retries and replays carry the
// original timestamp. Pass a positive toleranceSec to opt in.
function verifyShippoSignature(rawBody, header, secret, toleranceSec = 0) {
  const parts = header.split(',').map((part) => {
    const i = part.indexOf('=');
    return i === -1 ? [part.trim(), ''] : [part.slice(0, i).trim(), part.slice(i + 1).trim()];
  });
  const t = parts.find(([k]) => k === 't')?.[1];
  const sigs = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
  if (!t || !/^\d+$/.test(t) || sigs.length === 0) return false;
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(t));
  if (toleranceSec > 0 && age > toleranceSec) return false;
  const expected = crypto.createHmac('sha256', secret)
    .update(`${t}.`).update(rawBody).digest('hex');
  return sigs.some((sig) => sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
}

Without HMAC, compare the token query parameter to your stored token with a timing-safe comparison. The examples support both modes.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Common Event Types

The event name is in the JSON body's event field; the object is in data.

EventTriggered Whendata contains
track_updatedA tracked shipment's status changesTracking object
transaction_createdA transaction (label purchase) is createdTransaction object
transaction_updatedA transaction is updatedTransaction object
batch_createdA batch finishes creating (async)Batch info
batch_purchasedA batch's labels finish purchasing (async)Batch info
automations_label_resolvedA Shippo Automations label request reaches a terminal stateAutomations label result

Subscribing to all delivers every event type. Payloads include a test boolean, and requests carry a Shippo-API-Version header.

For the full event reference, see Shippo's webhook documentation.

Delivery Behavior

  • Return a 2xx within 3 seconds. Process heavy work asynchronously.
  • Shippo retries twice on timeouts, 408, 429, or 5xx. Other 4xx responses are not retried.
  • There is no event ID. Dedupe on natural keys (e.g. request_id for automations_label_resolved, object_id + status for transactions).

Environment Variables

SHIPPO_WEBHOOK_SECRET=your_hmac_secret   # HMAC secret provisioned by Shippo (optional if using a token)
SHIPPO_WEBHOOK_TOKEN=your_random_token   # Token you appended as ?token=… (optional if using HMAC)

Local Development

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

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Oct 28, 2026

View on GitHub →