ShipStation API for Rails Developers: Rate Limits, Webhooks and the Gotchas

I have built several integrations around ShipStation, and I contribute to the ShipStation Ruby gem, so I have read more of its documentation than I ever planned to. The API is capable, and it also has a handful of quirks that cost people days if nobody warns them.

This is the article I wish I could have handed to myself before the first project. It assumes you are comfortable with Rails or Ruby and are connecting a store or an internal app to ShipStation. Check the current ShipStation documentation as you go, because details change, and ShipStation also offers a newer API alongside the one I describe here. Make sure the features you need are available in the version you choose.

Authentication

The classic ShipStation API uses basic authentication with an API key and an API secret from your account settings. Treat them like a password. Keep them in encrypted credentials or environment variables, never in the repository, and use separate accounts or stores for testing when you can.

Every request needs those credentials, so wrap your API calls in a single client class. Put the base address, the credentials, timeouts, JSON handling and error handling in that one place. It makes everything else in your app shorter, and it gives you one spot to add logging and rate limit handling.

The request limit

The limit that catches people is the number of requests per minute. For the classic API it has been about 40 per minute for an account, which sounds like plenty until you sync a few hundred orders and make one call per order.

When you exceed it, you get a 429 response. ShipStation also includes information in the response headers about how many calls you have left and how many seconds until the window resets. Read those. A client that waits exactly as long as it is told is both faster and politer than one that guesses.

Practical ways to stay inside the limit:

  • Do your work in a queue with a capped number of workers for ShipStation calls, so a burst of orders does not become a burst of requests.
  • Slow down proactively when the remaining count gets low, instead of waiting for a 429.
  • Fetch in bulk. List endpoints return many records per page, and you can ask for a larger page size, so one call can cover many orders.
  • Avoid polling. Use webhooks to learn about changes instead of asking every few seconds.

I discuss the same ideas for Shopify in Shopify API Rate Limits During Black Friday. The details differ, but the approach is identical.

Creating and updating orders

The call that creates an order is also the call that updates one. If you send an order whose order key matches an existing order, ShipStation updates that order instead of making a new one. This is very useful for idempotency. Choose a stable order key, such as your internal order ID, and a retry cannot create a duplicate.

The order number is a separate field, meant for humans, and it appears on documents. Do not rely on it for uniqueness.

Each item on an order can carry a list of options, which are name and value pairs. This is the right place to put per item personalization such as engraving text. I explain how it fits together in ShipStation Custom Fields 1, 2 and 3.

Remember that if you create orders through the API, you should not also connect the same store through the standard connector. You would get every order twice.

Webhooks: the part that surprises people

You can register webhooks for events such as new orders and shipped orders. The surprising thing is what ShipStation sends. It does not send the order or the shipment. It sends a small notice containing a resource address, and your server must then call that address, with your credentials, to fetch the actual data.

That has consequences:

  • Your handler must verify the notice is genuine in whatever way fits your setup, and then make a follow up request. Those follow up requests count against your rate limit.
  • The notice tells you something happened, not what. If you get several notices close together, you may fetch overlapping data, so your processing must tolerate repeats.
  • If your server is down when the notice arrives, you may not hear about the event again. A scheduled catch up job is essential. It is the same lesson as in Shopify Webhooks Missing Orders.

My usual design is to save the notice, answer quickly, and let a background job fetch and process the data, with a reconcile job that lists recent shipments every so often as a backstop.

Shipments and tracking

For sending tracking back to the store, the shipments endpoint is what you want. You can filter by creation date, and you can ask for shipments that have been created since your last successful run. Keep a record of that time and overlap the windows slightly so that nothing slips between them.

Watch for voided shipments. When a label is voided and recreated, both records exist, and the voided one should not be the tracking number your customer receives. Check the status before you pass anything along.

Splitting is also a common cause of mismatches. One order can ship in two boxes, and your code has to decide how to map each shipment’s items back to the store order. I go deeper on this in Shopify Orders With No Tracking After ShipStation Ships Them.

Time zones and pagination

Two small things deserve a mention because they cause the oddest bugs.

First, ShipStation has historically returned and expected timestamps in Pacific time. If your code assumes UTC, then date filters will be off by hours, and records near the boundary will appear to be missing. Convert explicitly, in one place, and write a test for it.

Second, list endpoints are paginated. A loop that reads only the first page will work perfectly in development and quietly miss orders in production. Read the total page count from the response and walk every page, with a sensible delay so you stay inside the limit.

Errors worth handling

Beyond rate limits, plan for these:

  • Validation errors when an address or a field is not acceptable. These will not fix themselves, so record the reason and notify a person.
  • Server errors and timeouts. Retry with a growing delay, and make sure the retried request is idempotent.
  • Unexpected nulls. Optional fields can be empty more often than the examples suggest. Write defensive code around anything you do not control.

Log each request and response with the order identifier. When someone asks why order 12345 never shipped, you want an answer in a minute, not an afternoon.

Testing

If you can, use a separate ShipStation account or a test store so that experiments do not create real labels. Write tests for your client class that stub the HTTP responses, including a 429 with its headers, an empty page, and a notice with a bad resource address. These are the cases that will be hardest to reproduce by hand.

If you are connecting ShipStation to Shopify, WordPress or a custom Rails app and the standard options are not enough, email kevin@kevinhq.com with what you are trying to pass across. I have probably seen something close to it, and I can tell you whether the API can do it before you spend time building.


← Back to Homepage


Get occasional breakdowns

When I find something worth writing up — a fix, a pattern I keep seeing in client stores, something that actually moved revenue — I send it out. No schedule. No filler.