Agent skill

ShipStation Webhooks Skill

Receive and verify ShipStation webhooks. Use when setting up ShipStation webhook handlers, securing endpoints that have no signature (secret token in the URL), fetching the thin resource_url payload with Basic auth, or handling ORDER_NOTIFY, ITEM_ORDER_NOTIFY, SHIP_NOTIFY, ITEM_SHIP_NOTIFY, FULFILLMENT_SHIPPED, and FULFILLMENT_REJECTED events.

Install this skill

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


When to Use This Skill

  • How do I receive ShipStation webhooks?
  • How do I secure a ShipStation webhook endpoint when there is no signature?
  • How do I fetch the resource_url from a ShipStation webhook payload?
  • How do I handle ORDER_NOTIFY, SHIP_NOTIFY, or ITEM_SHIP_NOTIFY events?
  • Why does my ShipStation webhook only contain a resource_url and resource_type?

How ShipStation V1 Webhooks Work

This skill targets the ShipStation V1 API (ssapi.shipstation.com), the source you connect to Hookdeck.

Two things make V1 different from most webhook providers:

  1. Thin payloads. ShipStation does not send the resource data. It POSTs a small JSON body with a URL you must fetch back:

    { "resource_url": "https://ssapi.shipstation.com/orders?...", "resource_type": "ORDER_NOTIFY" }
    

    You GET resource_url with HTTP Basic auth (your API key : API secret) to get the actual orders/shipments. This authenticated fetch-back is the primary trust signal.

  2. No signature. V1 has no HMAC / no signing secret — there is nothing to verify cryptographically. Protect the endpoint by putting an unguessable secret token in the target URL (https://you.com/webhooks/shipstation?token=…) and comparing it timing-safe on every request, over HTTPS. Combined with the authed fetch-back, this is the trust model.

Verification (core)

There is no signature. Verify the shared secret token from the query string (timing-safe), then fetch the real resource with Basic auth. Pass only ShipStation hosts to the fetch (SSRF guard).

const crypto = require('crypto');

// 1. Timing-safe compare of the ?token= query param against your secret
function verifyToken(provided, expected) {
  if (!provided || !expected) return false;
  const a = Buffer.from(provided);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// resource_url hosts are reportedly numbered (ssapi1/ssapi2.shipstation.com) — match a pattern, not one host
const SHIPSTATION_HOST_RE = /^ssapi\d*\.shipstation\.com$/;

// 2. Fetch the thin payload's resource_url with Basic auth (API key : API secret)
async function fetchResource(resourceUrl, key, secret) {
  if (!SHIPSTATION_HOST_RE.test(new URL(resourceUrl).hostname)) {
    throw new Error('Refusing to fetch non-ShipStation host'); // SSRF guard
  }
  const auth = Buffer.from(`${key}:${secret}`).toString('base64');
  const res = await fetch(resourceUrl, { headers: { Authorization: `Basic ${auth}` } });
  if (res.status === 429) throw new Error(`Rate limited; reset ${res.headers.get('X-Rate-Limit-Reset')}s`);
  if (!res.ok) throw new Error(`Fetch failed: ${res.status}`);
  return res.json();
}

For complete handlers with route wiring, event dispatch, and tests, see:

Common Event Types

resource_type on the webhook body is one of the six V1 events you subscribed to:

Event (resource_type)Triggered When
ORDER_NOTIFYA new order is imported
ITEM_ORDER_NOTIFYA new order is imported (with item-level detail)
SHIP_NOTIFYAn order is shipped
ITEM_SHIP_NOTIFYAn order is shipped (with item-level detail)
FULFILLMENT_SHIPPEDAn external fulfillment is marked shipped
FULFILLMENT_REJECTEDAn external fulfillment is rejected

For the full list, see references/overview.md and the ShipStation Webhooks docs.

Environment Variables

SHIPSTATION_WEBHOOK_SECRET=an_unguessable_random_string   # token embedded in the target URL (?token=)
SHIPSTATION_API_KEY=your_api_key                          # for Basic auth when fetching resource_url
SHIPSTATION_API_SECRET=your_api_secret                    # for Basic auth when fetching resource_url

Get the API key/secret from ShipStation → Settings → Account → API Settings. See references/setup.md to subscribe (POST /webhooks/subscribe or the UI).

Local Development

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

Reference Materials

ShipStation API V2 (ShipEngine)

The newer ShipStation API V2 (api.shipstation.com/v2, docs.shipstation.com) is ShipEngine-based and is a different product: different events (batch, track, rate, report_complete, …) and RSA-SHA256 signatures (x-shipengine-rsa-sha256-key-id / -signature, x-shipengine-timestamp, JWKS at https://api.shipengine.com/jwks; 10s ack window, retries ~2× ~30 min apart). This skill targets V1. See references/verification.md for the V2 outline.


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Aug 3, 2026

View on GitHub →