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

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

## 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](https://github.com/hookdeck/webhook-skills/blob/main/skills/shipstation-webhooks/references/overview.md) and the
> [ShipStation Webhooks docs](https://help.shipstation.com/hc/en-us/articles/360025856252-ShipStation-Webhooks).

## Environment Variables

```bash
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/shipstation-webhooks/references/setup.md) to subscribe (`POST /webhooks/subscribe` or the UI).

## Local Development

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/shipstation-webhooks/references/overview.md) - ShipStation webhook concepts, all six events, thin payloads
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/shipstation-webhooks/references/setup.md) - Subscribe via API or dashboard, get API credentials
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/shipstation-webhooks/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](https://github.com/hookdeck/webhook-skills/blob/main/skills/shipstation-webhooks/references/verification.md) for the V2 outline.