Amazon SP-API Tutorial: Real Marketplace Automation
The Amazon Selling Partner API is not difficult in the way most APIs are difficult. The endpoints are documented and the data model is comprehensible. What makes it hard is that its authentication, rate limiting, and delivery semantics all behave differently from what the documentation leads you to expect, and each one fails quietly rather than loudly. Building marketplace automation for a seller managing more than 50,000 SKUs across five marketplaces meant working through all three. The system that came out of it replaced 24-hour sync cycles with under fifteen minutes while making roughly 90% fewer API calls, and the architecture change that produced both was the same one: stop polling and start listening. This is what I would tell someone starting on SP-API today.
The auth story nobody explains clearly
SP-API uses Login with Amazon for authorisation, and the confusion comes from how many credentials are involved and how differently they behave.
You hold a long-lived refresh token, which is issued once when a seller authorises your application and does not expire. You exchange it for a short-lived access token, which does expire, and which you attach to each request. The refresh token is the credential that matters — treat it as a secret with the same seriousness as a database password, because it represents standing authorisation to act on a seller's account.
The practical failure here is token refresh under concurrency. If a dozen Lambda invocations all notice an expired access token at the same moment, they will all request a new one simultaneously, and you will hit the authorisation endpoint's own limits. Cache the access token in something shared — a DynamoDB item or a Redis key — with the expiry recorded, and refresh it ahead of expiry rather than on failure. Refreshing reactively means every token expiry produces a burst of errors before it produces a working token.
Rate limits are per-operation, and they are not what the docs imply
SP-API rate limits are applied per operation, not per application, and they use a token bucket with a restore rate rather than a fixed window. Two things follow from that, and both surprise people.
First, an aggregate request budget is meaningless. You can be well under any sensible overall rate and still be throttled on one endpoint because that specific bucket is empty. Budgeting has to be per operation.
Second, burst capacity is real but it is a loan. The bucket allows a burst above the sustained rate, and if you spend it you must then run below the restore rate to refill it. Systems that treat the burst as their normal throughput work perfectly in testing and throttle continuously in production.
Handle 429 responses with exponential backoff and jitter, and treat sustained throttling as a signal to change architecture rather than to retry harder. If you are consistently throttled, you are asking the wrong question of the API — which is what the next section is about.
The sandbox will not tell you what you need to know
The sandbox returns static, predetermined responses. It is genuinely useful for confirming that your request signing works and your parsing handles the documented shape.
It will not show you the things that actually break systems: real throttling behaviour, the latency distribution of production endpoints, eventual consistency between when you write a change and when a read reflects it, or the variety of edge cases in real listing data. Sellers accumulate listings over years, and some of them have missing attributes, unusual character encodings, or states the documentation does not describe.
Plan for a staging phase against a real seller account with a small subset of listings. The sandbox tells you your code compiles against the contract; only production data tells you your assumptions about the contract were right.
Do not poll for inventory — subscribe to notifications
This is the change that mattered most, and it is why the sync went from 24 hours to under fifteen minutes while using far fewer API calls.
Polling is the obvious design and it scales badly in exactly the wrong direction. With 50,000 SKUs across five marketplaces, a polling loop spends nearly all of its request budget confirming that nothing changed, and the time between a real change and your system noticing it is bounded by how long a full cycle takes. Making it faster means polling harder, which means throttling sooner.
SP-API notifications invert that. Amazon tells you when something changes. The architecture that follows is a straightforward event pipeline: SP-API notifications land in EventBridge, EventBridge routes to SQS, and SQS drives Lambda consumers that apply the change.
The queue between the router and the consumers is not incidental — it is what makes the system survivable. Marketplace events do not arrive evenly. They arrive in bursts when a seller bulk-edits listings or a marketplace pushes a batch of updates, and a queue absorbs a burst that would otherwise become throttling or dropped events. It also gives you a retry boundary and a dead-letter queue for the events that fail repeatedly, which is where you will find your real bugs.
Two properties of the notification stream have to be designed for. Delivery is at-least-once, so consumers must be idempotent — process the same event twice and the result must be identical. And ordering is not guaranteed, so an event carrying a stale value can arrive after a newer one. Both are solved the same way: include a version or timestamp in the payload, compare it to what you have stored, and discard anything older than your current state.
The repricing engine, and why it runs on a schedule
Repricing is the part sellers care most about, and it is the part where event-driven design stops being the right answer.
The engine runs on a fifteen-minute EventBridge schedule rather than reacting to every competitor price change, and that is deliberate. Reacting instantly to each change produces oscillation — you undercut a competitor, they undercut you, and within an hour you have raced to a price neither of you wanted. A fixed cadence damps the loop.
Whatever the pricing rules are, floor prices belong in the data model rather than the rule logic, and they should be enforced at write time. A repricing bug that undercuts a competitor by a cent is an annoyance. One that ignores a floor and sells inventory below cost is not recoverable by fixing the code afterwards.
Log every price change with the inputs that produced it — the competitor prices observed, the rule that fired, and the resulting price. When a seller asks why an item is priced the way it is, and they will, the answer needs to be reconstructable rather than inferred.
What the finished architecture looks like
Notifications from SP-API into EventBridge, fanned through SQS into Lambda consumers for inventory and listing changes. DynamoDB for state that needs single-digit-millisecond reads keyed by SKU. ECS Fargate for the longer-running reconciliation work that does not fit Lambda's execution model. CloudWatch for metrics, with alarms on queue depth and consumer error rate rather than on request counts.
Queue depth is the metric worth watching most closely. It is the earliest honest signal that consumers have fallen behind, and it moves before customers notice anything is wrong.
Reports are a different API with different rules
Real-time notifications handle change. Bulk state is a separate mechanism, and it behaves nothing like the rest of the API.
Requesting a report is asynchronous: you create a request, poll for its status, and when it is ready you receive a document reference to download and decompress. The reports are large, they are generated on Amazon's schedule rather than yours, and the same report requested twice in quick succession may return identical data because generation is throttled independently.
Use reports for reconciliation, not for operations. The pattern that works is event-driven state for anything a seller sees in real time, plus a periodic full report to catch drift. Drift will happen — a missed notification, a consumer error that exhausted its retries, an event processed out of order despite versioning. Reconciliation is how you find out before the seller does.
Five marketplaces are five different systems
Marketplace-specific behaviour is the part that scales worst with ambition, because each marketplace adds its own exceptions rather than more of the same.
Endpoints are regional, and a seller operating across regions needs separate authorisation per region. Category and attribute requirements differ, so a listing valid in one marketplace can be rejected in another for a missing required attribute. Currency, tax handling, and fulfilment options vary. Even where the API shape is identical, the validation behind it is not.
Design for this by keeping marketplace-specific rules in configuration rather than in code branches. A per-marketplace configuration record that describes required attributes and constraints stays maintainable as marketplaces are added; a function full of conditionals does not survive the third one.
Instrument the pipeline, not just the API calls
The failure mode of an event-driven integration is silence. A polling loop that breaks stops producing results and you notice. A notification consumer that stops receiving events looks exactly like a period with no changes, and marketplaces genuinely do go quiet overnight.
Alarm on the absence of expected activity, not only on errors. Queue depth rising means consumers are falling behind; queue depth flat at zero for longer than the seller's usual quiet period means the pipeline may have stopped receiving anything at all. Both need alerts, and the second one is the one most systems lack.
Track the age of the oldest unprocessed message rather than the count. Depth tells you how much work is waiting; age tells you how stale the seller's data has become, which is the number that actually corresponds to their experience of the system.
Need help applying this to your project?
Book a free consultation →