ShipStation webhooks are easy to switch on, and surprisingly easy to get wrong.
The main trap is this: the webhook POST does not contain the order or shipment. It contains a `resource_url` that you must fetch, and you have to do it in a way that survives retries, duplicates and API limits.
The short version
- ShipStation webhooks (in the ShipStation app) post a small payload with resource_type and resource_url, you must GET the resource_url to read the actual orders or shipments.
- Assume retries and duplicates, design idempotency first, then make every downstream write safe to run twice.
- Respond 200 quickly, then process asynchronously, slow webhook handlers cause the sender to retry and you will double run work.
- Rate limits apply to the follow up GET calls, so you need batching and backoff, not one API call per item.
What do ShipStation webhooks send?
If you are configuring webhooks in the ShipStation web app, the payload is intentionally small. ShipStation posts a JSON body with a `resource_type` and a `resource_url`, and you then call the REST API to retrieve the actual data. ShipStation’s own help docs spell it out: you "must make a GET call" to the `resource_url`, with authentication, and the response follows the same structure as their list endpoints for orders or shipments. (ShipStation Webhooks help article).
A few details matter in production:
- One webhook payload can represent many records. For "On New Orders", ShipStation notes that orders created at the same time are included in a single webhook payload, and that payload is created per import action. That is why the `resource_url` includes an `importBatch` parameter in the example. (ShipStation Webhooks help article).
- Shipment notifications have different behaviour. "On Orders Shipped" triggers when a new outbound label is created, with explicit exclusions (returns, fulfilment provider shipments, and Mark as Shipped). Individual shipments create individual events, batch shipments create one event, and the `resource_url` includes a `batchId`. (ShipStation Webhooks help article).
- The URL is the contract. The official webhook model describes `resource_url` as the URL to retrieve the resource that triggered the webhook, accessed using ShipStation API Basic Auth credentials, and notes a 200 character limit. (ShipStation webhook model).
This is why a lot of advice you will find online is wrong. People expect to parse an order from the webhook body, then wonder why they "miss" data. There was never any data there to miss.
Why do you have to fetch the resource_url?
ShipStation uses the webhook POST as a notification, not as the full event. The `resource_url` points to an API endpoint that returns the data you actually need.
For ShipStation app webhooks (the classic ones you set up in the UI), ShipStation’s docs explicitly tie them to their list endpoints, "List Orders" for order webhooks and "List Shipments" for shipment webhooks. (ShipStation Webhooks help article).
What this means operationally:
- Your webhook handler does not need to do much work. It needs to validate, store the event, and acknowledge.
- The heavy work happens after, when you GET the `resource_url` and transform the result into whatever your business system needs.
In practice, the fetch step is where you hit the real constraints. Most failures are not "ShipStation did not send it". They are:
- The webhook was delivered twice, and your automation created duplicates.
- The webhook arrived once, but your handler was slow, so ShipStation retried, causing duplicates.
- You fetched the `resource_url` per item, hit rate limits, and silently dropped work.
ShipStation documents API rate limiting separately. Their published default is 200 requests per minute, and they describe returning a `Retry-After` header when you are rate limited. (ShipStation rate limits).
So if your webhook implies 200 orders, and your workflow does "GET per order" instead of "GET once per importBatch", you can throttle yourself very quickly.
Which ShipStation webhook types actually matter for ops?
Most UK SMEs using ShipStation want two things: react to new paid orders, and react to labels being created.
In the ShipStation app UI, those map to:
| Business moment | ShipStation webhook | What it means | What you fetch |
|---|---|---|---|
| A paid order has arrived in ShipStation | On New Orders (`ORDER_NOTIFY`) | Triggered on creation in most statuses, and when Awaiting Payment moves to another status (except Cancelled) | GET `resource_url` which returns orders for that `importBatch` (ShipStation Webhooks help article) |
| Label created for an outbound shipment | On Orders Shipped (`SHIP_NOTIFY`) | Triggered on outbound label creation. Excludes returns, fulfilment provider shipments and Mark as Shipped | GET `resource_url` which returns shipments for that `batchId` (ShipStation Webhooks help article) |
| Label created, but you care about line items | Item level variants (`ITEM_ORDER_NOTIFY`, `ITEM_SHIP_NOTIFY`) | Same triggers, but the URL includes `includeOrderItems=True` or `includeShipmentItems=True` | GET `resource_url` and expect items included (ShipStation Webhooks help article) |
A blunt limitation: these webhooks cover what ShipStation chose to expose, not every status transition you might care about. If you need cancel events, edits after import, or non label status changes, you typically end up polling or integrating upstream of ShipStation.
Retries and duplicates: design for them, do not fight them
ShipStation’s public docs are clear about what you do next (fetch the `resource_url`), but most webhook problems are about what happens when something fails in the middle.
A webhook system will retry if it thinks delivery failed. Even if ShipStation only delivered once, your own automation platform can also re run executions (manual retries, node level retries, queue re delivery). The practical result is the same: you might process the same event twice.
So treat webhook deliveries as at least once, and make your workflow idempotent.
A safe pattern that works in n8n
A reliable ShipStation webhook workflow in n8n usually has five steps:
- Receive the webhook (Webhook node) and validate the shape. At minimum, check you have `resource_type` and `resource_url`.
- Respond 200 quickly to avoid creating your own retries. n8n’s Webhook tooling makes a distinction between Test and Production URLs, and in production you want a published workflow and a predictable response behaviour. (n8n Webhook node common issues).
- Claim an idempotency key before you fetch or write anything. In n8n this usually means writing to a Data Table or database with a unique constraint.
- Fetch the `resource_url` with an HTTP Request node, using ShipStation Basic Auth for V1 endpoints. ShipStation’s V1 API uses Basic HTTP authentication with API key and secret. (ShipStation API requirements).
- Write side effects (Sheets rows, Slack messages, calls to a custom API) using the same dedupe key or a downstream idempotency key if the target supports it.
n8n themselves publish an idempotency guard template that shows this exact idea: store a key, stop if it has been seen already, and only proceed once you own that key. (n8n workflow: idempotency guard with Data Tables).
Choosing a dedupe key for ShipStation webhooks
For ShipStation app webhooks, the webhook body does not include a delivery id. In practice you build your own dedupe key from what you do have.
Two patterns that hold up:
- Key by `resource_type + resource_url`. This works because the `resource_url` encodes the importBatch or batchId for the action. If ShipStation retries, you see the same URL.
- Key by `resource_type + extracted batch identifier`. Parse `importBatch` or `batchId` from the query string and store that as a separate field for reporting.
Then make sure that the first write you do is the idempotency claim. If the claim fails because the key already exists, stop the workflow.
Backoff when fetching the resource_url
You cannot treat ShipStation as an infinite API. ShipStation document a default allowance of 200 requests per minute and recommend using `Retry-After` when you hit rate limits. (ShipStation rate limits).
In n8n this usually means:
- One GET per webhook event, not per order.
- If you are rate limited, sleep for `Retry-After` seconds and retry the GET.
- Keep the event in a pending state so you can replay it if your workflow fails after fetching.
This is the missing bit on many ranking pages. People talk about "use webhooks instead of polling", but do not mention that you have just swapped polling for bursty reads, and you still need backoff.
Common ShipStation webhook automations (and where they break)
Below are automations we see in small and mid sized teams. They are all feasible with ShipStation plus n8n, but each has a failure mode worth designing around.
1) New order triage to Slack
Automation: On `ORDER_NOTIFY`, fetch orders and post a summary into a Slack channel, for example high value orders, next day services, or address warnings.
Failure mode: duplicates spam the channel. Fix it by idempotency on the importBatch, and by posting one message per importBatch rather than one per order.
2) Append rows into Google Sheets for ops reporting
Automation: On `SHIP_NOTIFY`, fetch shipments and append a row per shipment to a Google Sheet used for daily dispatch reporting.
Failure mode: Sheets is not a database. If the webhook is replayed, you get duplicate rows and someone spends Friday afternoon de duping.
Fix it by writing a stable unique key into the sheet, and using an upsert pattern. If you cannot upsert cleanly in Sheets, write to a real store first (Data Table, Postgres), then generate the sheet view from that.
3) Push shipments into a custom API
Automation: On shipments, call your own API to update order status in your ERP, warranty system or customer portal.
Failure mode: the API call succeeds but the n8n execution later errors, so the whole run gets retried and you call your API twice.
Fix it by making your API endpoint idempotent. Accept an idempotency key and treat repeated requests as a no op.
4) Exceptions queue: address issues, split shipments, fulfilment provider edge cases
Automation: For certain stores or services, route the fetched payload into an exceptions queue, for example a Slack channel plus a Google Sheet row.
Failure mode: false confidence. ShipStation’s own notes list cases that do not trigger `SHIP_NOTIFY` such as fulfilments created through a fulfilment provider, return shipments, or Mark as Shipped. (ShipStation Webhooks help article).
Fix it by being explicit about coverage. If part of your operation uses fulfilment providers, you may need the fulfilment webhooks, or you may need to integrate at the provider instead.
A quick self check you can run this week
If you already have ShipStation webhooks pointing at n8n, you can sanity check reliability in under an hour.
- Find your webhook workflow and confirm it stores a dedupe key before it posts to Slack, writes to Sheets, or calls your API.
- Confirm you are fetching `resource_url` once per event, not doing extra ShipStation API calls inside loops.
- Confirm you have an answer for rate limiting. ShipStation publishes a default 200 requests per minute limit, so you need backoff on 429s. (ShipStation rate limits).
- Confirm you can reprocess safely. Pick one historical webhook payload, replay it, and verify you do not create duplicate rows or messages.
If any of those are not true, the automation works until the day it matters.
If you want this to run quietly for months
Most teams do not need more automation, they need fewer silent failures. At Swarm Labs, we build webhook driven integrations in n8n with idempotency, backoff and monitoring baked in, and we keep them running. If you are at the stage where ShipStation is a key system and you are tired of chasing duplicates, take a look at our AI automation approach and the sort of integration work we do across tools on our services page.
ShipStation webhook automations, built and monitored
If you want ShipStation webhooks implemented with a proper dedupe strategy, retry handling and ongoing monitoring, we offer ShipStation Webhook Automation Build and Managed Monitoring (n8n). It covers the webhook listener, the fetch step, the idempotency guard, and the operational bits like alerting when runs fail or backlog builds. If that is what you need, talk to us about your integration.