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_urlfrom a ShipStation webhook payload? - How do I handle
ORDER_NOTIFY,SHIP_NOTIFY, orITEM_SHIP_NOTIFYevents? - Why does my ShipStation webhook only contain a
resource_urlandresource_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:
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
GETresource_urlwith HTTP Basic auth (your API key : API secret) to get the actual orders/shipments. This authenticated fetch-back is the primary trust signal.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_NOTIFY | A new order is imported |
ITEM_ORDER_NOTIFY | A new order is imported (with item-level detail) |
SHIP_NOTIFY | An order is shipped |
ITEM_SHIP_NOTIFY | An order is shipped (with item-level detail) |
FULFILLMENT_SHIPPED | An external fulfillment is marked shipped |
FULFILLMENT_REJECTED | An 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
- references/overview.md - ShipStation webhook concepts, all six events, thin payloads
- references/setup.md - Subscribe via API or dashboard, get API credentials
- references/verification.md - Secret-token check, authenticated fetch, SSRF guard, V2 note
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.