A production-ready Amazon SP-API client in TypeScript — proper auth caching, per-endpoint rate limiting, retry logic, and typed methods for Orders and FBA Inventory.
No bloated SDK. No magic. Just the patterns that actually work at scale.
I spent a year building an Amazon marketplace platform that syncs 50 k+ SKUs across 5 marketplaces, survived Black Friday at ~12,000 concurrent users, and cut a client's sync latency from 24 hours to under 15 minutes using the SP-API Notifications API + SQS + Lambda.
Every time I onboarded a new engineer to the codebase, the same two things tripped them up:
- Auth — the LWA token docs look simple but the naive implementation causes silent 401 storms under load.
- Rate limits — developers treat SP-API like a single rate-limited API. It isn't. Every operation has its own bucket.
This starter repo encodes both lessons.
Full write-up: amazon-sp-api-marketplace-automation
Built by Shubham Kansal — Full Stack & DevOps, 14 yrs, founder of DietGhar and NyayX.
- LWA token manager — caches the access token in memory, refreshes proactively 5 minutes before the 1-hour expiry, deduplicates concurrent refresh calls.
- Per-endpoint token bucket — matches Amazon's published (burst, restoreRate) pairs exactly; conservative fallback for unknown operations.
- Retry on 429/503 — exponential back-off, configurable max attempts.
- Typed methods —
getOrders,getOrdersPages(async generator),getInventorySummaries,getInventoryPages. - Zero runtime dependencies beyond
undici— use Node's built-infetchif you're on Node 18+; swap inundicifor older versions or more control. - MIT licensed.
- Node.js 20.6+ (uses
--env-fileflag; works with 18+ if you load.envmanually) - An Amazon Seller Central account with SP-API access approved
- An LWA app (OAuth client) created in Seller Central → Apps → Manage
git clone https://github.com/shubhamkansal/sp-api-node-starter.git
cd sp-api-node-starter
npm installcp .env.example .envOpen .env and fill in:
| Variable | Where to find it |
|---|---|
LWA_CLIENT_ID |
Seller Central → Apps → your app → App ID |
LWA_CLIENT_SECRET |
Same page, "Client Secret" |
SP_API_REFRESH_TOKEN |
Output of the OAuth grant flow (see below) |
SP_API_REGION |
us-east-1 (NA), eu-west-1 (EU), us-west-2 (FE) |
SP_API_MARKETPLACE_ID |
See table in .env.example |
npm run build
node --env-file=.env dist/examples/getOrders.jsAmazon's LWA flow is OAuth 2.0 with a non-obvious quirk: the access token expires after exactly 3600 seconds and you need to refresh it using your long-lived refresh token. The documentation makes this look straightforward.
The trap: if you fetch a new token on every API call (the "stateless" approach), you're making N token requests for N API calls. At 50 k SKU syncs that's thousands of redundant round-trips to api.amazon.com. Worse, if two threads race to refresh simultaneously they can both succeed — wasting a refresh and briefly using a soon-to-be-stale token.
The fix (in src/auth/lwaTokenManager.ts):
┌─────────────────────────────────────────────────────────┐
│ Token valid? ──── yes ──── return cached token │
│ │ │
│ no │
│ │ │
│ Refresh in flight? ── yes ── await existing promise │
│ │ │
│ no │
│ │ │
│ POST /auth/o2/token → cache result, set expiresAt │
└─────────────────────────────────────────────────────────┘
Proactive refresh fires 5 minutes before actual expiry. No request ever finds itself mid-execution with a token that expires on the next line.
SP-API does not have a single global rate limit. Each operation has its own token bucket defined in the docs:
| Operation | Burst | Restore rate |
|---|---|---|
getOrders |
1 req | 0.0167 req/s (~1/min) |
getOrderItems |
1 req | 0.5 req/s |
getInventorySummaries |
2 req | 2 req/s |
searchCatalogItems |
5 req | 5 req/s |
createReport |
15 req | 0.0167 req/s |
src/rateLimit/tokenBucket.ts implements a token bucket that mirrors this model exactly. The RateLimitRegistry keeps one bucket per operation name and lazily creates them with the correct config.
Pre-emptive limiting (waiting before the request) is better than reactive limiting (catching 429s) because:
- It avoids a full network round-trip.
- It avoids burning your retry quota.
- 429 responses from Amazon can themselves trigger secondary throttling.
sp-api-node-starter/
├── src/
│ ├── auth/
│ │ └── lwaTokenManager.ts # LWA OAuth token cache + proactive refresh
│ ├── rateLimit/
│ │ └── tokenBucket.ts # Per-endpoint token bucket + registry
│ ├── client/
│ │ └── spApiClient.ts # Typed SP-API client (auth + rate limit + retry)
│ └── examples/
│ └── getOrders.ts # Runnable example
├── .env.example
├── .gitignore
├── LICENSE
├── package.json
└── tsconfig.json
Adding a new SP-API operation takes three steps:
1. Add the rate limit to KNOWN_RATE_LIMITS in tokenBucket.ts:
getPricing: { burst: 2, restoreRatePerSec: 0.1 },2. Add types and a method in spApiClient.ts:
export interface GetPricingResponse { /* ... */ }
async getPricing(asin: string): Promise<GetPricingResponse> {
const qs = new URLSearchParams({ Asin: asin, ItemType: "Asin",
MarketplaceId: this.marketplaceId });
return this.request<GetPricingResponse>("getPricing",
`/products/pricing/v0/price?${qs}`);
}3. Call it.
The rate limiter and retry logic are applied automatically by request().
The example above polls getOrders. For real-time updates — the model that cut our sync latency from 24h to <15min — use the Notifications API:
- Create an SQS queue in your AWS account.
- Call
createDestination(SP-API) to register it. - Call
createSubscriptionforORDER_CHANGE,ITEM_INVENTORY_EVENT, etc. - Wire a Lambda to the SQS queue and process messages.
This eliminates polling entirely. Detailed implementation:
https://shubhamkansal.com/blog/amazon-sp-api-marketplace-automation
Do I need AWS credentials?
For basic SP-API calls: no. The only credentials you need are LWA (OAuth). AWS credentials are only required if you use the deprecated AWS Signature V4 authentication (older versions of the API), or if you're using the Notifications API with SQS/Lambda (for the AWS side, not the SP-API side).
Can I use this with Vendor Central?
Yes. The Vendor APIs (Orders, Invoices, Shipments) use the same LWA auth flow. Change the refresh_token to one obtained via a Vendor Central OAuth grant and point at the Vendor API paths.
Why undici instead of axios?
undici is Node's reference HTTP/1.1 + HTTP/2 client and ships with Node 18+ as the fetch implementation. It's faster than axios and adds zero transitive dependencies. If you need request interceptors similar to Axios, the request() method in spApiClient.ts is the right place to add them.
The getOrders burst is 1. That's tiny.
Yes. This is why pagination with NextToken is important: one getOrders call returns up to 100 orders, and you only spend 1 token per page. For backfill jobs, schedule them off-peak and use CreatedAfter/CreatedBefore windows rather than hitting the API in a tight loop.
PRs welcome. If you're adding a new API operation, please include:
- The
BucketConfigentry inKNOWN_RATE_LIMITSwith a comment linking to the SP-API docs page. - TypeScript types for the response.
- A short example in
src/examples/.
MIT — see LICENSE.
Built by Shubham Kansal — Full Stack & DevOps engineer, 14 years experience, serving US / UK / UAE / AU clients.
Full write-up and production lessons: https://shubhamkansal.com/blog/amazon-sp-api-marketplace-automation
Site: https://shubhamkansal.com