TL;DR
Amazon Selling Partner API (SP-API) is a REST API providing access to order, inventory, listing, and fulfillment data for Amazon sellers. It uses OAuth 2.0 authentication with IAM roles, requires AWS SigV4 signing, and has endpoint-specific rate limits (0.1–100 requests/sec). This guide gives you implementation-ready steps for setup, authentication, endpoint usage, webhook subscriptions, and deploying to production.
Introduction
Amazon manages 350M+ products across 200+ marketplaces. If you're building e-commerce, inventory, or analytics tools for Amazon sellers, SP-API integration is essential.
Sellers often lose 20–30 hours/week to manual order, inventory, and listing management. Integrating SP-API automates order sync, inventory updates, and listing management across marketplaces.
This article is an actionable guide to building a production-grade Amazon SP-API integration. You'll get code-focused steps for IAM role setup, OAuth 2.0, SigV4 signing, endpoint access, webhooks, and error handling.
💡 Tip: Apidog accelerates SP-API integration testing, OAuth validation, signature debugging, and team collaboration with mocks and API sharing.
What Is Amazon SP-API?
Amazon Selling Partner API (SP-API) is a RESTful API for programmatically accessing Seller Central data. It replaces MWS with JSON, improved security, and more endpoints.
Key Capabilities
SP-API enables:
- Order retrieval/status updates
- Multi-marketplace inventory management
- Listing creation, update, and deletion
- FBA shipment management
- Pricing and competitive data
- Reports and analytics
- A+ Content and brand analytics
- Advertising data access
SP-API vs MWS Comparison
| Feature | SP-API | MWS (Legacy) |
|---|---|---|
| Architecture | RESTful JSON | XML-based |
| Authentication | OAuth 2.0 + IAM | MWS Auth Token |
| Security | AWS SigV4 | Simple tokens |
| Rate Limits | Dynamic/per endpoint | Fixed quotas |
| Marketplaces | Unified endpoints | Region-specific |
| Status | Current | Deprecated (Dec 2025) |
Migrate from MWS to SP-API before December 2025.
API Architecture Overview
Regional API endpoints:
https://sellingpartnerapi-na.amazon.com (North America)
https://sellingpartnerapi-eu.amazon.com (Europe)
https://sellingpartnerapi-fe.amazon.com (Far East)
All requests need:
- AWS SigV4 signature
- OAuth access token
- IAM role permissions
- Request ID header
Supported Marketplaces
| Region | Marketplaces | API Endpoint |
|---|---|---|
| North America | US, CA, MX | sellingpartnerapi-na.amazon.com |
| Europe | UK, DE, FR, IT, ES, NL, SE, PL, TR, EG, IN, AE, SA | sellingpartnerapi-eu.amazon.com |
| Far East | JP, AU, SG, BR | sellingpartnerapi-fe.amazon.com |
Getting Started: Account and IAM Setup
Step 1: Create Your Amazon Developer Account
- Go to Amazon Developer Central
- Sign in (Seller Central access required)
- Navigate to Selling Partner API
- Accept the Developer Agreement
Step 2: Register Your Application
- Log in to Seller Central
- Go to Apps and Services > Develop Apps
- Click Add New App
- Fill details:
- Application Name
- Type: “Self-developed” or “Third-party”
- Use Case: Integration purpose
- Redirect URI: HTTPS OAuth callback
You’ll receive:
- Application ID
- Client ID (OAuth)
- Client Secret
Store credentials securely: Use environment variables, not code.
# .env
AMAZON_APPLICATION_ID="amzn1.application.xxxxx"
AMAZON_CLIENT_ID="amzn1.account.xxxxx"
AMAZON_CLIENT_SECRET="your_client_secret_here"
AMAZON_SELLER_ID="your_seller_id_here"
AWS_ACCESS_KEY_ID="your_aws_access_key"
AWS_SECRET_ACCESS_KEY="your_aws_secret_key"
AWS_REGION="us-east-1"
Step 3: Create IAM Role for SP-API
- Log in to AWS IAM Console
- Go to Roles > Create Role
- Select Another AWS account
- Enter Amazon’s account ID for your region:
- North America:
906394416454 - Europe:
336853085554 - Far East:
774466381866
- North America:
Step 4: Configure IAM Policy
Attach this policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"execute-api:Invoke"
],
"Resource": [
"arn:aws:execute-api:*:*:*/prod/*/sellingpartnerapi/*"
]
}
]
}
Name your role (e.g. SellingPartnerApiRole). Save the ARN.
Step 5: Link IAM Role to Application
- In Seller Central, go to Develop Apps
- Select your app
- Edit > IAM Role ARN
- Enter your IAM role ARN
- Save
You’ll see status “Linked” when complete.
OAuth 2.0 Authentication Flow
Understanding SP-API OAuth
OAuth flow:
1. Seller clicks "Authorize"
2. Redirect to Amazon OAuth
3. Seller logs in, grants permissions
4. Redirect back with code
5. Exchange code for LWA token
6. Exchange LWA token for SP-API access token
7. Use token for SigV4-signed API calls
8. Refresh token as needed (expires in 1 hour)
Step 6: Generate Authorization URL
const generateAuthUrl = (clientId, redirectUri, state) => {
const baseUrl = 'https://www.amazon.com/sp/apps/oauth/authorize';
const params = new URLSearchParams({
application_id: process.env.AMAZON_APPLICATION_ID,
client_id: clientId,
redirect_uri: redirectUri,
state,
scope: 'sellingpartnerapi::notifications'
});
return `${baseUrl}?${params.toString()}`;
};
// Example usage
const authUrl = generateAuthUrl(
process.env.AMAZON_CLIENT_ID,
'https://your-app.com/callback',
crypto.randomBytes(16).toString('hex')
);
console.log(`Redirect user to: ${authUrl}`);
Required OAuth Scopes
| Scope | Description | Use Case |
|---|---|---|
sellingpartnerapi::notifications |
Receive notifications | Webhook subscriptions |
sellingpartnerapi::migration |
Migrate from MWS | Legacy integrations |
Most access is managed by IAM, not scopes.
Step 7: Exchange Code for LWA Token
const exchangeCodeForLwaToken = async (code, redirectUri) => {
const response = await fetch('https://api.amazon.com/auth/o2/token', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.AMAZON_CLIENT_ID,
client_secret: process.env.AMAZON_CLIENT_SECRET,
redirect_uri: redirectUri,
code
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(`LWA Token Error: ${error.error_description}`);
}
const data = await response.json();
return {
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_in: data.expires_in,
token_type: data.token_type
};
};
Callback handler:
app.get('/callback', async (req, res) => {
const { spapi_oauth_code, state } = req.query;
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state parameter');
}
try {
const tokens = await exchangeCodeForLwaToken(spapi_oauth_code, 'https://your-app.com/callback');
await db.sellers.update(req.session.sellerId, {
amazon_lwa_access_token: tokens.access_token,
amazon_lwa_refresh_token: tokens.refresh_token,
amazon_token_expires: Date.now() + (tokens.expires_in * 1000)
});
res.redirect('/dashboard');
} catch (error) {
res.status(500).send('Authentication failed');
}
});
Step 8: Exchange LWA Token for SP-API Credentials
const assumeRole = async (lwaAccessToken) => {
const response = await fetch('https://api.amazon.com/auth/o2/token', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.AMAZON_CLIENT_ID,
client_secret: process.env.AMAZON_CLIENT_SECRET,
scope: 'sellingpartnerapi::notifications'
})
});
const data = await response.json();
// Exchange for AWS credentials via STS
const stsResponse = await fetch('https://sts.amazonaws.com/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Bearer ${data.access_token}`
},
body: new URLSearchParams({
Action: 'AssumeRole',
RoleArn: 'arn:aws:iam::YOUR_ACCOUNT:role/SellingPartnerApiRole',
RoleSessionName: 'sp-api-session',
Version: '2011-06-15'
})
});
return stsResponse;
};
Step 9: Implement Token Refresh
const refreshLwaToken = async (refreshToken) => {
const response = await fetch('https://api.amazon.com/auth/o2/token', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: process.env.AMAZON_CLIENT_ID,
client_secret: process.env.AMAZON_CLIENT_SECRET,
refresh_token: refreshToken
})
});
const data = await response.json();
return {
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_in: data.expires_in
};
};
// Middleware to ensure valid token
const ensureValidToken = async (sellerId) => {
const seller = await db.sellers.findById(sellerId);
if (seller.amazon_token_expires < Date.now() + 300000) {
const newTokens = await refreshLwaToken(seller.amazon_lwa_refresh_token);
await db.sellers.update(sellerId, {
amazon_lwa_access_token: newTokens.access_token,
amazon_lwa_refresh_token: newTokens.refresh_token,
amazon_token_expires: Date.now() + (newTokens.expires_in * 1000)
});
return newTokens.access_token;
}
return seller.amazon_lwa_access_token;
};
AWS SigV4 Request Signing
SigV4 Signing Process
All SP-API requests must be AWS Signature Version 4 signed. You can implement it manually:
const crypto = require('crypto');
class SigV4Signer {
constructor(accessKey, secretKey, region, service = 'execute-api') {
this.accessKey = accessKey;
this.secretKey = secretKey;
this.region = region;
this.service = service;
}
sign(method, url, body = '', headers = {}) {
const parsedUrl = new URL(url);
const now = new Date();
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '');
const dateStamp = amzDate.slice(0, 8);
headers['host'] = parsedUrl.host;
headers['x-amz-date'] = amzDate;
headers['x-amz-access-token'] = this.accessToken;
headers['content-type'] = 'application/json';
const canonicalHeaders = Object.entries(headers)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k.toLowerCase()}:${v.trim()}`)
.join('\n');
const signedHeaders = Object.keys(headers).sort().map(k => k.toLowerCase()).join(';');
const payloadHash = crypto.createHash('sha256').update(body).digest('hex');
const canonicalRequest = [
method.toUpperCase(),
parsedUrl.pathname,
parsedUrl.search.slice(1),
canonicalHeaders,
'',
signedHeaders,
payloadHash
].join('\n');
const algorithm = 'AWS4-HMAC-SHA256';
const credentialScope = `${dateStamp}/${this.region}/${this.service}/aws4_request`;
const stringToSign = [
algorithm,
amzDate,
credentialScope,
crypto.createHash('sha256').update(canonicalRequest).digest('hex')
].join('\n');
const kDate = this.hmac(`AWS4${this.secretKey}`, dateStamp);
const kRegion = this.hmac(kDate, this.region);
const kService = this.hmac(kRegion, this.service);
const kSigning = this.hmac(kService, 'aws4_request');
const signature = this.hmac(kSigning, stringToSign, 'hex');
const authorization = `${algorithm} Credential=${this.accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
return {
headers: {
...headers,
'Authorization': authorization
},
canonicalRequest,
stringToSign,
signature
};
}
hmac(key, data, encoding = 'buffer') {
return crypto.createHmac('sha256', key).update(data).digest(encoding);
}
}
Using AWS SDK for SigV4
You can simplify with @aws-sdk/signature-v4:
const { SignatureV4 } = require('@aws-sdk/signature-v4');
const { Sha256 } = require('@aws-crypto/sha256-js');
const signer = new SignatureV4({
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
},
region: 'us-east-1',
service: 'execute-api',
sha256: Sha256
});
const makeSpApiRequest = async (method, endpoint, accessToken, body = null) => {
const url = new URL(endpoint);
const headers = {
'host': url.host,
'content-type': 'application/json',
'x-amz-access-token': accessToken,
'x-amz-date': new Date().toISOString().replace(/[:-]|\.\d{3}/g, '')
};
const signedRequest = await signer.sign({
method,
hostname: url.hostname,
path: url.pathname,
query: Object.fromEntries(url.searchParams),
headers,
body: body ? JSON.stringify(body) : undefined
});
const response = await fetch(endpoint, {
method,
headers: signedRequest.headers,
body: signedRequest.body
});
if (!response.ok) {
const error = await response.json();
throw new Error(`SP-API Error: ${error.errors?.[0]?.message || response.statusText}`);
}
return response.json();
};
Orders API
Retrieving Orders
Fetch orders with filters:
const getOrders = async (accessToken, options = {}) => {
const params = new URLSearchParams({
createdAfter: options.createdAfter,
createdBefore: options.createdBefore,
orderStatuses: options.orderStatuses?.join(',') || '',
marketplaceIds: options.marketplaceIds?.join(',') || ['ATVPDKIKX0DER'],
maxResultsPerPage: options.maxResultsPerPage || 100
});
for (const [key, value] of params.entries()) {
if (!value) params.delete(key);
}
const endpoint = `https://sellingpartnerapi-na.amazon.com/orders/v0/orders?${params.toString()}`;
return makeSpApiRequest('GET', endpoint, accessToken);
};
// Example: orders from last 24 hours
const orders = await getOrders(accessToken, {
createdAfter: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
orderStatuses: ['Unshipped', 'PartiallyShipped'],
marketplaceIds: ['ATVPDKIKX0DER']
});
Order Response Structure
{
"payload": {
"orders": [
{
"amazon_order_id": "112-1234567-1234567",
"order_status": "Unshipped",
"purchase_date": "2026-03-19T10:30:00Z",
...
}
],
"next_token": "eyJleHBpcmF0aW9uVGltZU9mTmV4dFRva2VuIjoxNzEwOTUwNDAwfQ=="
}
}
Getting Order Items
const getOrderItems = async (accessToken, orderId) => {
const endpoint = `https://sellingpartnerapi-na.amazon.com/orders/v0/orders/${orderId}/orderItems`;
return makeSpApiRequest('GET', endpoint, accessToken);
};
const orderItems = await getOrderItems(accessToken, '112-1234567-1234567');
Updating Shipment Status
const confirmShipment = async (accessToken, orderId, shipmentData) => {
const endpoint = `https://sellingpartnerapi-na.amazon.com/orders/v0/orders/${orderId}/shipmentConfirmation`;
const payload = {
packageDetails: {
packageReferenceId: shipmentData.packageReferenceId || '1',
carrier_code: shipmentData.carrierCode,
tracking_number: shipmentData.trackingNumber,
ship_date: shipmentData.shipDate || new Date().toISOString(),
items: shipmentData.items.map(item => ({
order_item_id: item.orderItemId,
quantity: item.quantity
}))
}
};
return makeSpApiRequest('POST', endpoint, accessToken, payload);
};
// Usage
await confirmShipment(accessToken, '112-1234567-1234567', {
carrierCode: 'USPS',
trackingNumber: '9400111899223456789012',
items: [
{ orderItemId: '12345678901234', quantity: 2 }
]
});
Common Carrier Codes
| Carrier | Carrier Code |
|---|---|
| USPS | USPS |
| FedEx | FEDEX |
| UPS | UPS |
| DHL | DHL |
| Canada Post | CANADA_POST |
| Royal Mail | ROYAL_MAIL |
| Australia Post | AUSTRALIA_POST |
| Amazon Logistics | AMZN_UK |
Inventory API
Getting Inventory Summaries
const getInventorySummaries = async (accessToken, options = {}) => {
const params = new URLSearchParams({
granularityType: options.granularityType || 'Marketplace',
granularityId: options.granularityId || 'ATVPDKIKX0DER',
startDateTime: options.startDateTime || '',
sellerSkus: options.sellerSkus?.join(',') || ''
});
const endpoint = `https://sellingpartnerapi-na.amazon.com/fba/inventory/v1/summaries?${params.toString()}`;
return makeSpApiRequest('GET', endpoint, accessToken);
};
// Example
const inventory = await getInventorySummaries(accessToken, {
granularityId: 'ATVPDKIKX0DER',
sellerSkus: ['MYSKU-001', 'MYSKU-002']
});
Updating Inventory
SP-API doesn't support direct inventory update endpoints. Use:
- FBA Shipments: Send inventory to Amazon warehouses
- MFN Orders: Inventory reduced automatically when orders ship
- Listings API: Adjust quantity via listing updates
Example: Create inbound shipment plan (FBA):
const createInboundShipmentPlan = async (accessToken, shipmentData) => {
const endpoint = 'https://sellingpartnerapi-na.amazon.com/fba/inbound/v0/plans';
const payload = {
ShipFromAddress: { ... },
LabelPrepPreference: 'SELLER_LABEL',
InboundPlanItems: shipmentData.items.map(item => ({
SellerSKU: item.sku,
ASIN: item.asin,
Quantity: item.quantity,
Condition: 'NewItem'
}))
};
return makeSpApiRequest('POST', endpoint, accessToken, payload);
};
Listings API
Getting Listings
const getListings = async (accessToken, options = {}) => {
const params = new URLSearchParams({
marketplaceIds: options.marketplaceIds?.join(',') || ['ATVPDKIKX0DER'],
itemTypes: options.itemTypes?.join(',') || ['ASIN', 'SKU'],
identifiers: options.identifiers?.join(',') || '',
issuesLocale: options.locale || 'en_US'
});
const endpoint = `https://sellingpartnerapi-na.amazon.com/listings/2021-08-01/items?${params.toString()}`;
return makeSpApiRequest('GET', endpoint, accessToken);
};
const listings = await getListings(accessToken, {
identifiers: ['B08N5WRWNW', 'B09JQKJXYZ'],
itemTypes: ['ASIN']
});
Creating or Updating Listings
const submitListingUpdate = async (accessToken, listingData) => {
const endpoint = 'https://sellingpartnerapi-na.amazon.com/listings/2021-08-01/items/MYSKU-001';
const payload = {
productType: 'LUGGAGE',
patches: [
{
op: 'replace',
path: '/attributes/title',
value: 'Updated Wireless Bluetooth Headphones - Premium Sound'
},
{
op: 'replace',
path: '/salesPrice',
value: {
currencyCode: 'USD',
amount: '79.99'
}
}
]
};
return makeSpApiRequest('PATCH', endpoint, accessToken, payload);
};
Deleting a Listing
const deleteListing = async (accessToken, sku, marketplaceIds) => {
const params = new URLSearchParams({
marketplaceIds: marketplaceIds.join(',')
});
const endpoint = `https://sellingpartnerapi-na.amazon.com/listings/2021-08-01/items/${sku}?${params.toString()}`;
return makeSpApiRequest('DELETE', endpoint, accessToken);
};
Reports API
Creating Report Schedules
const createReport = async (accessToken, reportType, dateRange) => {
const endpoint = 'https://sellingpartnerapi-na.amazon.com/reports/2021-06-30/reports';
const payload = {
reportType,
marketplaceIds: dateRange.marketplaceIds || ['ATVPDKIKX0DER'],
dataStartTime: dateRange.startTime?.toISOString(),
dataEndTime: dateRange.endTime?.toISOString()
};
return makeSpApiRequest('POST', endpoint, accessToken, payload);
};
const REPORT_TYPES = {
ORDERS: 'GET_FLAT_FILE_ALL_ORDERS_DATA_BY_LAST_UPDATE_GENERAL',
// ... other types
};
const report = await createReport(accessToken, REPORT_TYPES.ORDERS, {
marketplaceIds: ['ATVPDKIKX0DER'],
startTime: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
endTime: new Date()
});
Getting Report Document
const getReportDocument = async (accessToken, reportId) => {
const endpoint = `https://sellingpartnerapi-na.amazon.com/reports/2021-06-30/reports/${reportId}/document`;
return makeSpApiRequest('GET', endpoint, accessToken);
};
const downloadReport = async (accessToken, reportId) => {
const documentInfo = await getReportDocument(accessToken, reportId);
const response = await fetch(documentInfo.payload.url);
const content = await response.text();
if (documentInfo.payload.compressionAlgorithm === 'GZIP') {
const decompressed = await decompressGzip(content);
return decompressed;
}
return content;
};
Notifications API
Creating Subscriptions
const createSubscription = async (accessToken, subscriptionData) => {
const endpoint = 'https://sellingpartnerapi-na.amazon.com/notifications/v1/subscriptions';
const payload = {
payload: {
destination: {
resource: subscriptionData.destinationArn,
name: subscriptionData.name
},
modelVersion: '1.0',
eventFilter: {
eventCode: subscriptionData.eventCode,
marketplaceIds: subscriptionData.marketplaceIds
}
}
};
return makeSpApiRequest('POST', endpoint, accessToken, payload);
};
const EVENT_CODES = {
ORDER_STATUS_CHANGE: 'OrderStatusChange',
// ...
};
await createSubscription(accessToken, {
destinationArn: 'arn:aws:sns:us-east-1:123456789012:sp-api-notifications',
name: 'OrderStatusNotifications',
eventCode: EVENT_CODES.ORDER_STATUS_CHANGE,
marketplaceIds: ['ATVPDKIKX0DER']
});
Setting Up SNS Destination
const createSnsDestination = async (accessToken, destinationData) => {
const endpoint = 'https://sellingpartnerapi-na.amazon.com/notifications/v1/destinations';
const payload = {
resource: destinationData.snsTopicArn,
name: destinationData.name
};
return makeSpApiRequest('POST', endpoint, accessToken, { payload });
};
// SNS topic policy should allow Amazon to publish
const snsTopicPolicy = {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: { Service: 'notifications.amazon.com' },
Action: 'SNS:Publish',
Resource: 'arn:aws:sns:us-east-1:123456789012:sp-api-notifications'
}
]
};
Processing Notifications
const express = require('express');
const app = express();
app.post('/webhooks/amazon', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['x-amz-sns-message-signature'];
const payload = req.body;
// TODO: verify SNS signature
const message = JSON.parse(payload.toString());
switch (message.Type) {
case 'SubscriptionConfirmation':
await fetch(message.SubscribeURL);
break;
case 'Notification':
const notification = JSON.parse(message.Message);
await handleSpApiNotification(notification);
break;
}
res.status(200).send('OK');
});
async function handleSpApiNotification(notification) {
const { notificationType, payload } = notification;
switch (notificationType) {
case 'OrderStatusChange':
await syncOrderStatus(payload.amazonOrderId);
break;
case 'OrderChange':
await syncOrderDetails(payload.amazonOrderId);
break;
case 'InventoryLevels':
await updateInventoryCache(payload);
break;
}
}
Rate Limiting and Quotas
Understanding Rate Limits
| Endpoint Category | Rate Limit | Burst Limit |
|---|---|---|
| Orders | 10 req/sec | 20 |
| Order Items | 5 req/sec | 10 |
| Inventory | 2 req/sec | 5 |
| Listings | 10 req/sec | 20 |
| Reports | 0.5 req/sec | 1 |
| Notifications | 1 req/sec | 2 |
| FBA Inbound | 2 req/sec | 5 |
Check the x-amzn-RateLimit-Limit header in responses.
Implementing Rate Limit Handling
Use exponential backoff for retries:
const makeRateLimitedRequest = async (method, endpoint, accessToken, body = null, maxRetries = 5) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await makeSpApiRequest(method, endpoint, accessToken, body);
return response;
} catch (error) {
if (error.message.includes('429') && attempt < maxRetries) {
const retryAfter = error.headers?.get('Retry-After') || Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
} else if (error.message.includes('503') && attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
};
Request Queuing Example
class RateLimitedQueue {
constructor(rateLimit, burstLimit = null) {
this.rateLimit = rateLimit;
this.burstLimit = burstLimit || rateLimit * 2;
this.tokens = this.burstLimit;
this.lastRefill = Date.now();
this.queue = [];
this.processing = false;
}
async add(requestFn) {
return new Promise((resolve, reject) => {
this.queue.push({ requestFn, resolve, reject });
this.process();
});
}
refillTokens() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
const tokensToAdd = elapsed * this.rateLimit;
this.tokens = Math.min(this.burstLimit, this.tokens + tokensToAdd);
this.lastRefill = now;
}
async process() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
while (this.queue.length > 0) {
this.refillTokens();
if (this.tokens < 1) {
const waitTime = (1 / this.rateLimit) * 1000;
await new Promise(r => setTimeout(r, waitTime));
continue;
}
this.tokens--;
const { requestFn, resolve, reject } = this.queue.shift();
try {
const result = await requestFn();
resolve(result);
} catch (error) {
reject(error);
}
}
this.processing = false;
}
}
// Usage
const ordersQueue = new RateLimitedQueue(10, 20);
const orders = await ordersQueue.add(() => getOrders(accessToken, options));
Security Best Practices
Credential Management
- Never hardcode credentials. Use environment variables or AWS Secrets Manager.
// Bad
const AWS_ACCESS_KEY = 'AKIAIOSFODNN7EXAMPLE';
// Good
const AWS_ACCESS_KEY = process.env.AWS_ACCESS_KEY_ID;
Using AWS Secrets Manager:
const { SecretsManagerClient } = require('@aws-sdk/client-secrets-manager');
const secretsClient = new SecretsManagerClient({ region: 'us-east-1' });
const getCredentials = async () => {
const response = await secretsClient.send({
Name: 'prod/sp-api/credentials'
});
return JSON.parse(response.SecretString);
};
Token Storage Requirements
- Encrypt tokens at rest (AES-256)
- Always use HTTPS/TLS 1.2+
- Restrict token access to service accounts
- Log all token access/refresh events
- Refresh tokens before expiry
const crypto = require('crypto');
class TokenStore {
constructor(encryptionKey) {
this.algorithm = 'aes-256-gcm';
this.key = crypto.createHash('sha256').update(encryptionKey).digest('hex').slice(0, 32);
}
encrypt(token) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.algorithm, Buffer.from(this.key), iv);
let encrypted = cipher.update(token, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag().toString('hex');
return {
iv: iv.toString('hex'),
encryptedData: encrypted,
authTag
};
}
decrypt(encryptedData) {
const decipher = crypto.createDecipheriv(
this.algorithm,
Buffer.from(this.key),
Buffer.from(encryptedData.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
let decrypted = decipher.update(encryptedData.encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
IAM Least Privilege
Grant only the API permissions your integration needs:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "SPAPIOrdersAccess",
"Effect": "Allow",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:*:*:*/prod/*/sellingpartnerapi/orders/*"
},
{
"Sid": "SPAPIInventoryAccess",
"Effect": "Allow",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:*:*:*/prod/*/sellingpartnerapi/fba/inventory/*"
}
]
}
Scope permissions, avoid wildcards in production.
Request Signing Security
- Use HTTPS
- Include all required headers in signature
- Ensure timestamps are within 5 minutes of AWS
- Rotate credentials regularly
- Prefer IAM roles over long-term credentials
// Validate request timestamp
const validateTimestamp = (amzDate) => {
const now = new Date();
const requestTime = new Date(amzDate);
const diff = Math.abs(now - requestTime);
if (diff > 5 * 60 * 1000) {
throw new Error('Request timestamp too old. Sync your server clock.');
}
};
Testing SP-API Integrations with Apidog
Why Test SP-API Integrations
SP-API integrations are complex. Testing with Apidog lets you:
- Validate OAuth flows
- Debug SigV4 signatures
- Test rate limit handling
- Mock responses
- Document and share API flows
Setting Up Apidog for SP-API Testing
Step 1: Import SP-API OpenAPI Spec
- Download Amazon’s SP-API OpenAPI spec from the repo
- Create a new project in Apidog
- Import the spec file
- Set up environment variables for credentials
Step 2: Configure Environment Variables
Base URL (Sandbox): https://sandbox.sellingpartnerapi-na.amazon.com
Base URL (Production): https://sellingpartnerapi-na.amazon.com
LWA Access Token: {{lwa_access_token}}
AWS Access Key: {{aws_access_key}}
AWS Secret Key: {{aws_secret_key}}
Region: us-east-1
Step 3: Create Pre-request Scripts
Automate SigV4 signing in Apidog:
// Apidog pre-request script for SigV4 signing
const crypto = require('crypto');
const accessKey = apidog.variables.get('aws_access_key');
const secretKey = apidog.variables.get('aws_secret_key');
const accessToken = apidog.variables.get('lwa_access_token');
const region = apidog.variables.get('region');
const method = apidog.request.method;
const url = new URL(apidog.request.url);
const body = apidog.request.body;
// Generate SigV4 signature
const signer = new SigV4Signer(accessKey, secretKey, region);
const signedHeaders = signer.sign(method, url.href, body, {
'x-amz-access-token': accessToken
});
// Set headers for the request
apidog.request.headers = {
...apidog.request.headers,
...signedHeaders.headers
};
Step 4: Create Test Scenarios
// Test: Get Orders from Last 24 Hours
const ordersResponse = await apidog.send({
method: 'GET',
url: '/orders/v0/orders',
params: {
createdAfter: new Date(Date.now() - 86400000).toISOString(),
marketplaceIds: 'ATVPDKIKX0DER'
}
});
apidog.assert(ordersResponse.status === 200, 'Orders request failed');
apidog.assert(ordersResponse.data.payload.orders.length > 0, 'No orders found');
const orderIds = ordersResponse.data.payload.orders.map(o => o.amazon_order_id);
apidog.variables.set('order_ids', JSON.stringify(orderIds));
Mocking SP-API Responses
Use Apidog’s mock feature for frontend/dev:
{
"payload": {
"orders": [
{
"amazon_order_id": "112-{{randomNumber}}-{{randomNumber}}",
"order_status": "Unshipped",
"purchase_date": "{{now}}",
"order_total": {
"currency_code": "USD",
"amount": "{{randomFloat 10 500}}"
}
}
],
"next_token": null
}
}
Debugging Common Issues
SigV4 Signature Mismatch:
Check header order, required headers, canonical request, timestamp.
OAuth Token Errors:
Test with a protected endpoint:
const tokenCheck = await apidog.send({
method: 'GET',
url: '/orders/v0/orders',
params: { createdAfter: new Date().toISOString() }
});
if (tokenCheck.status === 401) {
// Token expired - refresh
}
Automating Tests in CI/CD
Integrate Apidog in your pipeline:
# GitHub Actions
name: SP-API Integration Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Apidog Tests
uses: apidog/test-action@v1
with:
project-id: ${{ secrets.APIDOG_PROJECT_ID }}
api-key: ${{ secrets.APIDOG_API_KEY }}
environment: sandbox
- name: Notify on Failure
if: failure()
run: |
echo "SP-API tests failed - check Apidog dashboard"
Marketplace ID Reference
| Country | Marketplace ID |
|---|---|
| United States | ATVPDKIKX0DER |
| Canada | A2EUQ1WTGCTBG2 |
| Mexico | A1AM78C64UM0Y8 |
| United Kingdom | A1F83G8C2ARO7P |
| Germany | A1PA6795UKMFR9 |
| France | A13V1IB3VIYZZH |
| Italy | APJ6JRA9NG5V4 |
| Spain | A1RKKUPIHCS9HS |
| Japan | A1VC38T7YXB528 |
| Australia | A39IBJ37TRP1C6 |
| India | A21TJRUUN4KGV |
| Brazil | A2Q3Y263D00KWC |
Troubleshooting Common Issues
403 Unauthorized
- Check AWS credentials, IAM role linkage, OAuth token expiry, SigV4 region, and
x-amz-access-tokenheader. - Error logging:
const error = await response.json();
console.error('Auth error:', error);
// Check for InvalidSignature, AccessDenied, ExpiredToken
429 Rate Limit Exceeded
- Queue requests and use exponential backoff.
- Batch with
next_tokenfor pagination. - Monitor
x-amzn-RateLimit-Limit.
404 Not Found
- Verify regional endpoint, marketplace ID, resource existence, and API version.
400 Bad Request
- Use ISO 8601 dates
- Check all required fields/IDs
- Validate JSON
const validateIsoDate = (dateString) => {
const date = new Date(dateString);
if (isNaN(date.getTime())) throw new Error('Invalid ISO 8601 date format');
return dateString;
};
Reports Stay in INIT_STATE
- Wait 15–30 min
- Use smaller date ranges
- Poll status every 30s
- Check permissions
Notifications Not Arriving
- Check subscription status
- SNS topic policy allows Amazon
- HTTPS endpoint with valid SSL
- 200 OK response within 30s
Production Deployment Checklist
- [ ] Register app in production Seller Central
- [ ] Configure production IAM role (least privilege)
- [ ] Update redirect URIs
- [ ] Secure token storage (encryption)
- [ ] Auto token refresh logic
- [ ] Rate limiting & queuing
- [ ] SNS destination for notifications
- [ ] Error handling & logging (with request IDs)
- [ ] Monitor rate limit usage
- [ ] Runbook for issues
- [ ] Test multi-marketplace IDs
- [ ] Document OAuth onboarding
- [ ] Retry logic with backoff
- [ ] Alerting on auth failures
Monitoring & Alerting Example
const metrics = {
apiCalls: { total: 0, successful: 0, failed: 0, rateLimited: 0 },
rateLimitUsage: {
orders: { current: 0, limit: 10 },
inventory: { current: 0, limit: 2 },
listings: { current: 0, limit: 10 }
},
oauthTokens: { active: 0, expiring_soon: 0, refresh_failures: 0 },
notifications: { received: 0, processed: 0, failed: 0 },
reports: { pending: 0, completed: 0, failed: 0 }
};
const failureRate = metrics.apiCalls.failed / metrics.apiCalls.total;
if (failureRate > 0.05) {
sendAlert('SP-API failure rate above 5%');
}
for (const [endpoint, usage] of Object.entries(metrics.rateLimitUsage)) {
if (usage.current / usage.limit > 0.8) {
sendAlert(`${endpoint} rate limit at 80% capacity`);
}
}
Real-World Use Cases
Multi-Marketplace Inventory Sync
- Problem: Overselling due to manual inventory updates
- Solution: SP-API webhooks + rate-limited queues for real-time sync
- Result: 0 overselling, 25+ hours/week saved
Flow:
- SNS notification on
InventoryLevels - Central system recalculates inventory
- API updates sent to all marketplaces
- Audit logged
Automated Order Fulfillment
- Problem: 200+ daily orders, manual entry
- Solution: SP-API to WMS integration
- Result: 2 min order-to-warehouse
Key steps:
- Webhook on
OrderStatusChange - Details to WMS via REST
- Tracking # updated via SP-API
- Customer auto-notified
Analytics Dashboard
- Problem: Sellers lack unified reporting
- Solution: OAuth-based, multi-seller data aggregation
- Result: Real-time dashboard for sales, inventory, ads
Data includes:
- Orders/order items
- FBA inventory & shipments
- Listings performance/pricing
- Brand analytics/search terms
Conclusion
Amazon SP-API enables robust seller automation with modern security. Key implementation points:
- Use OAuth 2.0 + IAM roles and automate token refresh
- All calls require SigV4 signing—use SDKs or validated libraries
- Monitor and handle endpoint-level rate limits
- Use SNS notifications for real-time sync
- Build in error handling and retries for reliability
- Apidog streamlines API testing and collaboration
FAQ Section
What is Amazon SP-API?
Amazon Selling Partner API (SP-API) is a REST API for seller central data (orders, listings, inventory, etc.), replacing legacy MWS and adding OAuth 2.0 and AWS SigV4 security.
How do I get Amazon SP-API credentials?
Register your app in Seller Central > Apps and Services > Develop Apps. You'll get a Client ID, Client Secret, and Application ID. Create an IAM role and link to your app.
Is Amazon SP-API free to use?
Yes. It's free for registered sellers, with per-endpoint rate limits. Higher limits require Amazon approval.
What authentication does SP-API use?
OAuth 2.0 for authorization, IAM roles for access control, and AWS SigV4 signing for all requests.
How do I handle SP-API rate limits?
Implement request queuing, monitor x-amzn-RateLimit-Limit, and backoff on HTTP 429s. Limits differ by endpoint.
Can I test SP-API without a live seller account?
Yes, use the sandbox environment. Not all endpoints are available, but you can test core flows.
How do webhooks work with SP-API?
Create event subscriptions, configure an SNS topic, and implement an HTTPS endpoint to receive notifications. Confirm subscription messages automatically.
What happens when OAuth token expires?
Tokens expire after 1 hour. Use the refresh token to obtain a new access token automatically.
How do I migrate from MWS to SP-API?
Update authentication to OAuth 2.0, implement SigV4 signing, update endpoints, and switch from XML to JSON payloads.
Why am I getting 403 Unauthorized errors?
Check for expired OAuth token, incorrect IAM role, invalid signature, missing x-amz-access-token, or missing IAM linkage. Check error codes in the response.
Top comments (0)