TL;DR: I built a platform syncing 50k+ SKUs across 5 Amazon marketplaces. Polling the Catalog API nearly killed it. Switching to event-driven inventory sync (Notifications API → SQS → Lambda) cut sync latency from 24 hours to under 15 minutes and dropped API call volume by 90%. Here's everything I wish I'd known going in.
Amazon's Selling Partner API is powerful and genuinely complex. The official docs are long and the gotchas are buried. After building a production platform managing 50,000+ SKUs across 5 marketplaces, this is my ground-level guide — auth, rate limits, sync architecture, and a repricing engine that holds up under real load.
The auth story nobody explains clearly
SP-API does not use IAM credentials the way most AWS services do. The flow is:
- Register your app in Seller Central and get an LWA (Login with Amazon) client ID and client secret.
- Each seller authorises your app and hands you a refresh token scoped to their account.
- Your app exchanges the refresh token for a short-lived access token (valid 1 hour).
- You attach that access token to every API call via the
x-amz-access-tokenheader alongside a standard AWS Signature V4 header for your IAM role.
// Minimal LWA token refresh (Node.js)
const axios = require('axios');
async function refreshAccessToken(refreshToken) {
const { data } = await axios.post('https://api.amazon.com/auth/o2/token', {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: process.env.LWA_CLIENT_ID,
client_secret: process.env.LWA_CLIENT_SECRET,
});
return {
accessToken: data.access_token,
expiresAt: Date.now() + (data.expires_in - 60) * 1000, // refresh 60s early
};
}
Critical: refresh proactively, not reactively. If you retry a failed request on token expiry you burn your rate limit quota on the retry. Cache the token, check expiresAt before every call, and refresh in the background.
Rate limits are per-plan, per-endpoint
This trips almost everyone. SP-API is not a single rate limit bucket. Every operation has its own burst rate and restore rate:
| Endpoint | Burst | Restore rate |
|---|---|---|
getListingsItem |
5 req/s | 1 req/s |
getCompetitivePricing |
10 req/s | 0.1 req/s |
getInventorySummaries |
2 req/s | 2 req/s |
getOrders |
0.0167 req/s | 0.0167 req/s |
Model each endpoint's rate limit independently using a token bucket:
class TokenBucket {
constructor({ burst, restoreRate }) {
this.tokens = burst;
this.burst = burst;
this.restoreRate = restoreRate; // tokens per second
this.lastRefill = Date.now();
}
consume() {
this._refill();
if (this.tokens < 1) return false;
this.tokens -= 1;
return true;
}
_refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(this.burst, this.tokens + elapsed * this.restoreRate);
this.lastRefill = now;
}
}
// One bucket per endpoint
const buckets = {
getListingsItem: new TokenBucket({ burst: 5, restoreRate: 1 }),
getCompetitivePricing: new TokenBucket({ burst: 10, restoreRate: 0.1 }),
};
HTTP 429 counts against you — throttled calls still consume some quota on Amazon's side. Shape your traffic before it leaves your servers.
The sandbox is not production
This cost me two days. The SP-API sandbox:
- Uses static test data that doesn't reflect real catalogue behaviour
- Accepts certain call patterns that production will reject (e.g. bulk listing submissions with edge-case Unicode in titles)
- Does not simulate webhook delivery realistically
Always test auth flows, webhook delivery, and edge-case SKU data against a real production test seller account before going anywhere near live inventory. The sandbox is only useful for verifying your request/response shapes.
Inventory sync: don't poll, stream
Polling the Catalog Items API for 50,000 SKUs every 15 minutes is how you get throttled into oblivion. Our original architecture:
Cron (every 15 min) → Lambda → SP-API Catalog Items (50k calls) → RDS
This hit rate limits constantly, had a sync latency of up to 24 hours under throttling, and cost more than it should. The right architecture:
SP-API Notifications API
↓
Amazon SQS
↓
Lambda (batch processor)
↓
RDS
Step-by-step:
// 1. Create an SQS destination in SP-API (one-time setup)
const spApiClient = new SellingPartnerAPI({ /* config */ });
await spApiClient.notifications.createDestination({
name: 'inventory-updates-queue',
resourceSpecification: {
sqs: { arn: process.env.SQS_ARN }
}
});
// 2. Subscribe to inventory events per marketplace
await spApiClient.notifications.createSubscription({
notificationType: 'ITEM_INVENTORY_UPDATE',
destinationId: destinationId,
payloadVersion: '1.0',
});
// 3. Lambda processes SQS batches — only changed SKUs
exports.handler = async (event) => {
const updates = event.Records.map(r => JSON.parse(r.body));
for (const update of updates) {
await db.query(
`UPDATE inventory SET quantity = $1, updated_at = NOW()
WHERE seller_sku = $2 AND marketplace_id = $3`,
[update.quantity, update.sellerSku, update.marketplaceId]
);
}
};
Results after the migration:
- Sync latency: 24 hours → under 15 minutes
- API call volume: down 90%
- Lambda cost: down proportionally (you only process what changed)
The repricing engine
The Buy Box is determined by price, fulfilment method (FBA beats FBM in most categories), and seller health metrics — roughly in that order. A minimal repricing loop:
// EventBridge triggers this Lambda every 15 minutes
exports.repricerHandler = async () => {
const activeSKUs = await db.query(
`SELECT seller_sku, floor_price, ceiling_price, current_price
FROM listings WHERE repricing_enabled = true`
);
for (const batch of chunk(activeSKUs.rows, 10)) {
// 1. Fetch competitor prices
const competitiveData = await spApi.getCompetitivePricing({
marketplaceId: process.env.MARKETPLACE_ID,
asins: batch.map(s => s.asin),
});
for (const sku of batch) {
const lowestCompetitor = getLowestOffer(competitiveData, sku.asin);
// 2. Apply floor/ceiling rules
const newPrice = clamp(
lowestCompetitor - 0.01, // beat by 1 cent
sku.floor_price,
sku.ceiling_price
);
if (newPrice === sku.current_price) continue; // no update needed
// 3. Submit price change
await spApi.patchListingsItem({
sellerId: process.env.SELLER_ID,
sku: sku.seller_sku,
marketplaceIds: [process.env.MARKETPLACE_ID],
body: {
productType: 'PRODUCT',
patches: [{ op: 'replace', path: '/attributes/purchasable_offer', value: [{ our_price: [{ schedule: [{ value_with_tax: newPrice }] }] }] }]
}
});
}
}
};
Run repricing every 15 minutes. Faster than that and you risk pattern-matching Amazon's bot detection. Slower and you lose the Buy Box to competitors who reprice more aggressively.
Lessons in short
- Build your LWA auth layer around proactive token refresh, not reactive retry
- Model every endpoint's rate limit as a separate token bucket
- Never use the sandbox as a proxy for production behaviour
- Subscribe to Notifications API events instead of polling — the API was designed for this
- Batch Listings API writes (max 10 per request); batch Competitive Pricing reads similarly
- Keep your repricer on a 15-minute EventBridge schedule; it's frequent enough without triggering fraud signals
The SP-API ecosystem rewards patience and the event-driven mindset. Once the infrastructure is right, the business logic is the fun part.
Originally published at https://shubhamkansal.com/blog/amazon-sp-api-marketplace-automation. I'm Shubham Kansal, a freelance Full Stack & DevOps engineer — more at https://shubhamkansal.com.
Top comments (0)