TL;DR
Amazon Selling Partner API (SP-API) is a REST API for accessing seller data such as orders, inventory, listings, fulfillment, reports, and notifications. Production integrations require OAuth 2.0 authorization, AWS IAM configuration, SigV4-signed requests, endpoint-specific rate-limit handling, and secure token storage.
Introduction
Amazon operates across 200+ marketplaces and handles more than 350 million products. If you are building seller tools, inventory systems, fulfillment software, or analytics products, SP-API is the integration layer for automating order sync, stock monitoring, listing changes, and reporting.
A practical SP-API implementation needs to handle:
- Amazon Developer and Seller Central registration
- IAM roles and least-privilege policies
- OAuth 2.0 seller authorization
- Login with Amazon (LWA) access-token refresh
- AWS Signature Version 4 (SigV4)
- Orders, inventory, listings, reports, and notifications
- Dynamic rate limits, retries, logging, and alerting
Apidog can help test SP-API endpoints, validate OAuth flows, inspect signed requests, mock responses, and share repeatable test scenarios with a team.
What Is Amazon SP-API?
Amazon Selling Partner API (SP-API) provides programmatic access to Seller Central data. It replaced Marketplace Web Service (MWS) with RESTful JSON endpoints, OAuth 2.0 authorization, IAM access control, and AWS SigV4 signing.
Key capabilities
SP-API can support:
- Order retrieval and shipment updates
- Inventory monitoring across marketplaces
- Listing creation, updates, and deletion
- FBA shipment workflows
- Pricing and competitive-data workflows
- Reports and analytics generation
- A+ Content management
- Brand analytics and advertising data
SP-API vs. MWS
| Feature | SP-API | MWS (Legacy) |
|---|---|---|
| Architecture | RESTful JSON | XML-based |
| Authentication | OAuth 2.0 + IAM | MWS Auth Token |
| Security | AWS SigV4 signing | Simple tokens |
| Rate limits | Dynamic per endpoint | Fixed quotas |
| Marketplaces | Unified endpoints | Region-specific |
| Status | Current | Deprecated (Dec 2025) |
Amazon announced full MWS retirement for December 2025. Migrate existing MWS integrations to SP-API as soon as possible.
Regional API endpoints
SP-API uses regional endpoints:
North America: https://sellingpartnerapi-na.amazon.com
Europe: https://sellingpartnerapi-eu.amazon.com
Far East: https://sellingpartnerapi-fe.amazon.com
Every request needs:
- A valid LWA access token
- AWS SigV4 signing
- IAM role permissions
- Request IDs in your logs for troubleshooting
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 an Amazon Developer account
Before making SP-API calls:
- Open Amazon Developer Central.
- Sign in with an Amazon account that has Seller Central access.
- Open Selling Partner API.
- Accept the Developer Agreement.
Step 2: Register an application
In Seller Central:
- Go to Apps and Services → Develop Apps.
- Select Add New App.
- Set:
- Application Name
- Application Type: self-developed or third-party
- Use Case
- Redirect URI: an HTTPS callback URL for OAuth
- Submit the application.
Amazon provides:
- Application ID
- Client ID
- Client Secret
Store them outside source control.
# .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 an IAM role
SP-API requires an IAM role.
- Open the AWS IAM Console.
- Go to Roles → Create role.
- Select Another AWS account as the trusted entity.
- Enter Amazon’s regional account ID:
| Region | Amazon account ID |
|---|---|
| North America | 906394416454 |
| Europe | 336853085554 |
| Far East | 774466381866 |
Step 4: Attach an IAM policy
Start with the required invoke permission:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"execute-api:Invoke"
],
"Resource": [
"arn:aws:execute-api:*:*:*/prod/*/sellingpartnerapi/*"
]
}
]
}
Use a descriptive role name such as SellingPartnerApiRole, then copy its ARN.
Step 5: Link the role to the application
In Seller Central:
- Go to Develop Apps.
- Open your application.
- Select Edit.
- Enter the IAM Role ARN.
- Save the configuration.
Amazon usually validates the role within minutes. Confirm that the application displays a linked status before testing API calls.
OAuth 2.0 Authentication Flow
SP-API uses Login with Amazon (LWA) OAuth 2.0 authorization. A typical seller authorization flow is:
- A seller clicks Authorize in your application.
- Redirect the seller to Amazon’s authorization URL.
- The seller signs in and grants access.
- Amazon redirects to your callback URL.
- Exchange
spapi_oauth_codefor LWA tokens. - Store the refresh token securely.
- Use the LWA access token in SigV4-signed SP-API requests.
- Refresh access tokens before they expire.
LWA access tokens typically expire after one hour.
Step 6: Generate an authorization URL
Generate and persist a random state value for CSRF protection.
const crypto = require('crypto');
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()}`;
};
const state = crypto.randomBytes(16).toString('hex');
const authUrl = generateAuthUrl(
process.env.AMAZON_CLIENT_ID,
'https://your-app.com/callback',
state
);
console.log(`Redirect seller to: ${authUrl}`);
Store state in the seller session or a short-lived database record before redirecting.
Required OAuth scopes
Request only scopes required by the integration.
| Scope | Purpose | Typical use case |
|---|---|---|
sellingpartnerapi::notifications |
Receive notifications | Webhook subscriptions |
sellingpartnerapi::migration |
Migrate from MWS | Legacy integrations |
Most SP-API access is governed by IAM and application permissions rather than OAuth scopes.
Step 7: Exchange the authorization code for LWA tokens
Handle the callback, validate state, and exchange the authorization code.
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}`);
}
return response.json();
};
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) {
console.error('Token exchange failed:', error);
res.status(500).send('Authentication failed');
}
});
Step 8: Obtain temporary AWS credentials
Use your LWA authorization and IAM configuration to obtain the credentials required for signed SP-API access.
const assumeRole = async () => {
const stsResponse = await fetch('https://sts.amazonaws.com/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
Action: 'AssumeRole',
RoleArn: 'arn:aws:iam::YOUR_ACCOUNT:role/SellingPartnerApiRole',
RoleSessionName: 'sp-api-session',
Version: '2011-06-15'
})
});
if (!stsResponse.ok) {
throw new Error(`Unable to assume role: ${stsResponse.statusText}`);
}
return stsResponse.text();
};
Use temporary credentials when possible instead of long-term IAM credentials.
Step 9: Refresh LWA access tokens
Refresh tokens before expiration. A five-minute buffer avoids failures during long-running requests.
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
})
});
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.statusText}`);
}
return response.json();
};
const ensureValidToken = async (sellerId) => {
const seller = await db.sellers.findById(sellerId);
const expiresSoon = seller.amazon_token_expires < Date.now() + 300_000;
if (!expiresSoon) {
return seller.amazon_lwa_access_token;
}
const tokens = await refreshLwaToken(seller.amazon_lwa_refresh_token);
await db.sellers.update(sellerId, {
amazon_lwa_access_token: tokens.access_token,
amazon_lwa_refresh_token: tokens.refresh_token,
amazon_token_expires: Date.now() + tokens.expires_in * 1000
});
return tokens.access_token;
};
AWS SigV4 Request Signing
Every SP-API request must use AWS Signature Version 4. The signature proves request integrity and authenticity.
The signing process creates:
- A canonical request
- A string to sign
- A derived signing key
- An
Authorizationheader
Prefer the AWS SDK signer
Avoid maintaining a custom signing implementation unless necessary. The AWS SDK signer reduces canonicalization mistakes.
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 signedRequest = await signer.sign({
method,
hostname: url.hostname,
path: url.pathname,
query: Object.fromEntries(url.searchParams),
headers: {
host: url.host,
'content-type': 'application/json',
'x-amz-access-token': accessToken,
'x-amz-date': new Date().toISOString().replace(/[:-]|\.\d{3}/g, '')
},
body: body ? JSON.stringify(body) : undefined
});
const response = await fetch(endpoint, {
method,
headers: signedRequest.headers,
body: signedRequest.body
});
const data = await response.json();
if (!response.ok) {
const message = data.errors?.[0]?.message || response.statusText;
const error = new Error(`SP-API ${response.status}: ${message}`);
error.status = response.status;
error.headers = response.headers;
error.data = data;
throw error;
}
return {
data,
headers: response.headers,
status: response.status
};
};
Custom SigV4 signing example
If you need to inspect the signing process while debugging, a custom signer can expose the canonical request and string to sign.
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;
}
hmac(key, data, encoding = 'buffer') {
return crypto.createHmac('sha256', key).update(data).digest(encoding);
}
sign(method, url, body = '', headers = {}) {
const parsedUrl = new URL(url);
const amzDate = new Date().toISOString().replace(/[:-]|\.\d{3}/g, '');
const dateStamp = amzDate.slice(0, 8);
const requestHeaders = {
...headers,
host: parsedUrl.host,
'x-amz-date': amzDate,
'content-type': 'application/json'
};
const canonicalHeaders = Object.entries(requestHeaders)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key.toLowerCase()}:${value.trim()}`)
.join('\n');
const signedHeaders = Object.keys(requestHeaders)
.sort()
.map((key) => key.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: {
...requestHeaders,
Authorization: authorization
},
canonicalRequest,
stringToSign,
signature
};
}
}
Orders API
The Orders API is commonly the first production workflow. Use it to fetch orders, retrieve line items, and confirm shipments.
Retrieve orders
Use date, marketplace, and status filters. Persist the latest successful sync timestamp and overlap requests slightly to prevent missing updates.
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: String(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}`;
return makeSpApiRequest('GET', endpoint, accessToken);
};
const orders = await getOrders(accessToken, {
createdAfter: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
orderStatuses: ['Unshipped', 'PartiallyShipped'],
marketplaceIds: ['ATVPDKIKX0DER']
});
Example order response
{
"payload": {
"orders": [
{
"amazon_order_id": "112-1234567-1234567",
"seller_order_id": "ORDER-001",
"purchase_date": "2026-03-19T10:30:00Z",
"last_update_date": "2026-03-19T14:45:00Z",
"order_status": "Unshipped",
"fulfillment_channel": "AFN",
"sales_channel": "Amazon.com",
"order_total": {
"currency_code": "USD",
"amount": "89.99"
},
"number_of_items_shipped": 0,
"number_of_items_unshipped": 2,
"marketplace_id": "ATVPDKIKX0DER",
"is_prime": true
}
],
"next_token": "eyJleHBpcmF0aW9uVGltZU9mTmV4dFRva2VuIjoxNzEwOTUwNDAwfQ=="
}
}
AFN means Fulfillment by Amazon (FBA); MFN means merchant fulfillment.
When next_token is present, continue pagination until it is empty.
Get order items
Fetch line items separately for each order when your fulfillment or reporting workflow needs SKU-level details.
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'
);
Example response:
{
"payload": {
"order_items": [
{
"asin": "B08N5WRWNW",
"seller_sku": "MYSKU-001",
"title": "Wireless Bluetooth Headphones",
"quantity_ordered": 2,
"quantity_shipped": 0,
"item_price": {
"currency_code": "USD",
"amount": "44.99"
},
"item_total": {
"currency_code": "USD",
"amount": "89.98"
}
}
]
}
}
Confirm a shipment
For merchant-fulfilled orders, send the carrier, tracking number, ship date, and item quantities.
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);
};
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
Get inventory summaries
Use the FBA Inventory API to retrieve 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}`;
return makeSpApiRequest('GET', endpoint, accessToken);
};
const inventory = await getInventorySummaries(accessToken, {
granularityId: 'ATVPDKIKX0DER',
sellerSkus: ['MYSKU-001', 'MYSKU-002']
});
Example response:
{
"payload": {
"inventorySummaries": [
{
"asin": "B08N5WRWNW",
"seller_sku": "MYSKU-001",
"condition": "NewItem",
"details": {
"quantity": 150,
"fulfillable_quantity": 145,
"inbound_shipped_quantity": 5,
"reserved_quantity": 5,
"unfulfillable_quantity": 0
},
"marketplace_id": "ATVPDKIKX0DER"
}
]
}
}
Updating inventory
SP-API does not provide a direct inventory update endpoint. Inventory changes are typically handled through:
- FBA inbound shipments
- Merchant-fulfilled order processing
- Listing updates through the Listings API
For FBA, create inbound shipment plans:
const createInboundShipmentPlan = async (accessToken, shipmentData) => {
const endpoint = 'https://sellingpartnerapi-na.amazon.com/fba/inbound/v0/plans';
const payload = {
ShipFromAddress: {
Name: shipmentData.shipFromName,
AddressLine1: shipmentData.shipFromAddress,
City: shipmentData.shipFromCity,
StateOrProvinceCode: shipmentData.shipFromState,
CountryCode: shipmentData.shipFromCountry,
PostalCode: shipmentData.shipFromPostalCode
},
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
Get listings
Retrieve listings by identifier and marketplace.
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}`;
return makeSpApiRequest('GET', endpoint, accessToken);
};
const listings = await getListings(accessToken, {
identifiers: ['B08N5WRWNW', 'B09JQKJXYZ'],
itemTypes: ['ASIN']
});
Example listing data:
{
"identifiers": {
"marketplaceId": "ATVPDKIKX0DER",
"sku": "MYSKU-001",
"asin": "B08N5WRWNW"
},
"attributes": {
"title": "Wireless Bluetooth Headphones",
"brand": "MyBrand",
"color": "Black"
},
"product_type": "LUGGAGE",
"sales_price": {
"currency_code": "USD",
"amount": "89.99"
},
"fulfillment_availability": [
{
"fulfillment_channel_code": "AFN",
"quantity": 150
}
],
"status": "ACTIVE"
}
Update a listing
Use a patch request for targeted changes.
const submitListingUpdate = async (accessToken, sku) => {
const endpoint =
`https://sellingpartnerapi-na.amazon.com/listings/2021-08-01/items/${sku}`;
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);
};
Delete 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}`;
return makeSpApiRequest('DELETE', endpoint, accessToken);
};
Reports API
Reports are asynchronous. Create a report, poll its status, then download the document when it is ready.
Create a report
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',
ORDER_ITEMS: 'GET_FLAT_FILE_ORDER_ITEMS_DATA_BY_LAST_UPDATE_GENERAL',
INVENTORY: 'GET_MERCHANT_LISTINGS_ALL_DATA',
FBA_INVENTORY: 'GET_FBA_MYI_UNSUPPRESSED_INVENTORY_DATA',
SETTLEMENT: 'GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE',
SALES_AND_TRAFFIC: 'GET_SALES_AND_TRAFFIC_REPORT',
ADVERTISING: 'GET_BRAND_ANALYTICS_SEARCH_TERMS_REPORT'
};
const report = await createReport(accessToken, REPORT_TYPES.ORDERS, {
marketplaceIds: ['ATVPDKIKX0DER'],
startTime: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
endTime: new Date()
});
Download a 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.data.payload.url);
const content = await response.text();
if (documentInfo.data.payload.compressionAlgorithm === 'GZIP') {
return decompressGzip(content);
}
return content;
};
Reports are commonly tab-delimited or JSON. Poll report status at a controlled interval rather than continuously.
Notifications API
SP-API notifications use Amazon SNS. Use notifications to reduce polling for order, inventory, and fulfillment changes.
Create a destination
First create an SNS-backed 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 });
};
Example SNS topic policy:
{
"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"
}
]
}
Create a subscription
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',
ORDER_ITEM_CHANGE: 'OrderItemChange',
ORDER_CHANGE: 'OrderChange',
FBA_ORDER_STATUS_CHANGE: 'FBAOrderStatusChange',
FBA_OUTBOUND_SHIPMENT_STATUS: 'FBAOutboundShipmentStatus',
INVENTORY_LEVELS: 'InventoryLevels',
PRICING_HEALTH: 'PricingHealth'
};
await createSubscription(accessToken, {
destinationArn: 'arn:aws:sns:us-east-1:123456789012:sp-api-notifications',
name: 'OrderStatusNotifications',
eventCode: EVENT_CODES.ORDER_STATUS_CHANGE,
marketplaceIds: ['ATVPDKIKX0DER']
});
Process SNS notifications
Your webhook must validate SNS messages, handle subscription confirmation, and return a successful response promptly.
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;
const isValid = await verifySnsSignature(payload, signature);
if (!isValid) {
return res.status(401).send('Unauthorized');
}
const message = JSON.parse(payload.toString());
if (message.Type === 'SubscriptionConfirmation') {
await fetch(message.SubscribeURL);
}
if (message.Type === 'Notification') {
const notification = JSON.parse(message.Message);
await handleSpApiNotification(notification);
}
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;
}
}
Use notifications as a trigger, then fetch the current resource state from SP-API before updating your local database.
Rate Limiting and Quotas
SP-API rate limits are dynamic and differ by operation. Check the x-amzn-RateLimit-Limit response header instead of assuming a fixed quota.
| Endpoint category | Rate limit | Burst limit |
|---|---|---|
| Orders | 10 requests/second | 20 |
| Order Items | 5 requests/second | 10 |
| Inventory | 2 requests/second | 5 |
| Listings | 10 requests/second | 20 |
| Reports | 0.5 requests/second | 1 |
| Notifications | 1 request/second | 2 |
| FBA Inbound | 2 requests/second | 5 |
Retry 429 and 503 responses
Use Retry-After when Amazon provides it. Otherwise, apply exponential backoff.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const makeRateLimitedRequest = async (
method,
endpoint,
accessToken,
body = null,
maxRetries = 5
) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await makeSpApiRequest(method, endpoint, accessToken, body);
} catch (error) {
const retryable = error.status === 429 || error.status === 503;
if (!retryable || attempt === maxRetries) {
throw error;
}
const retryAfter = Number(error.headers?.get('Retry-After'));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1000
: 2 ** attempt * 1000;
console.warn(
`SP-API request failed with ${error.status}. Retrying in ${delayMs}ms.`
);
await sleep(delayMs);
}
}
};
Queue outbound requests
A token-bucket queue prevents bursts from exceeding endpoint limits.
class RateLimitedQueue {
constructor(rateLimit, burstLimit = rateLimit * 2) {
this.rateLimit = rateLimit;
this.burstLimit = burstLimit;
this.tokens = burstLimit;
this.lastRefill = Date.now();
this.queue = [];
this.processing = false;
}
add(requestFn) {
return new Promise((resolve, reject) => {
this.queue.push({ requestFn, resolve, reject });
this.process();
});
}
refillTokens() {
const now = Date.now();
const elapsedSeconds = (now - this.lastRefill) / 1000;
this.tokens = Math.min(
this.burstLimit,
this.tokens + elapsedSeconds * this.rateLimit
);
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) {
await new Promise((resolve) => {
setTimeout(resolve, (1 / this.rateLimit) * 1000);
});
continue;
}
this.tokens -= 1;
const { requestFn, resolve, reject } = this.queue.shift();
try {
resolve(await requestFn());
} catch (error) {
reject(error);
}
}
this.processing = false;
}
}
const ordersQueue = new RateLimitedQueue(10, 20);
const orders = await ordersQueue.add(() =>
getOrders(accessToken, {
marketplaceIds: ['ATVPDKIKX0DER']
})
);
Maintain separate queues per API operation or endpoint group because quotas differ.
Security Best Practices
Keep credentials out of source code
// Bad
const AWS_ACCESS_KEY = 'AKIAIOSFODNN7EXAMPLE';
const AWS_SECRET = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
// Good
const AWS_ACCESS_KEY = process.env.AWS_ACCESS_KEY_ID;
const AWS_SECRET = process.env.AWS_SECRET_ACCESS_KEY;
Use AWS Secrets Manager or another managed secret store for production credentials.
const {
SecretsManagerClient,
GetSecretValueCommand
} = require('@aws-sdk/client-secrets-manager');
const secretsClient = new SecretsManagerClient({ region: 'us-east-1' });
const getCredentials = async () => {
const response = await secretsClient.send(
new GetSecretValueCommand({
SecretId: 'prod/sp-api/credentials'
})
);
return JSON.parse(response.SecretString);
};
Encrypt stored tokens
Protect refresh tokens and access tokens with encryption at rest, TLS in transit, limited service-account access, and audit logging.
const crypto = require('crypto');
class TokenStore {
constructor(encryptionKey) {
this.algorithm = 'aes-256-gcm';
this.key = crypto
.createHash('sha256')
.update(encryptionKey)
.digest()
.subarray(0, 32);
}
encrypt(token) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);
const encrypted = Buffer.concat([
cipher.update(token, 'utf8'),
cipher.final()
]);
return {
iv: iv.toString('hex'),
encryptedData: encrypted.toString('hex'),
authTag: cipher.getAuthTag().toString('hex')
};
}
decrypt(value) {
const decipher = crypto.createDecipheriv(
this.algorithm,
this.key,
Buffer.from(value.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(value.authTag, 'hex'));
return Buffer.concat([
decipher.update(Buffer.from(value.encryptedData, 'hex')),
decipher.final()
]).toString('utf8');
}
}
Apply IAM least privilege
Do not use broad wildcards in production when endpoint-specific permissions are possible.
{
"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/*"
}
]
}
Validate signing prerequisites
For SigV4 requests:
- Use HTTPS.
- Include all required headers in the signature.
- Keep server time synchronized.
- Rotate AWS credentials.
- Prefer IAM roles and temporary credentials.
AWS can reject requests when timestamps differ by more than five minutes.
const validateTimestamp = (amzDate) => {
const requestTime = new Date(amzDate);
const difference = Math.abs(Date.now() - requestTime.getTime());
if (difference > 5 * 60 * 1000) {
throw new Error('Request timestamp is too old. Check server clock synchronization.');
}
};
Testing SP-API Integrations with Apidog
SP-API testing involves OAuth redirects, LWA tokens, SigV4 signatures, dynamic limits, and asynchronous workflows. Test each layer independently before combining them.
1. Import the SP-API specification
In Apidog:
- Create a project.
- Import Amazon’s SP-API OpenAPI specification.
- Create sandbox and production environments.
- Add environment variables for API endpoints and credentials.
Example environment values:
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
2. Add a pre-request signing script
Use a pre-request script to add the LWA token and signed headers before each API call.
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;
const signer = new SigV4Signer(accessKey, secretKey, region);
const signedHeaders = signer.sign(method, url.href, body, {
'x-amz-access-token': accessToken
});
apidog.request.headers = {
...apidog.request.headers,
...signedHeaders.headers
};
3. Build workflow tests
Test an end-to-end order workflow:
- Exchange OAuth code for an access token.
- Request orders for a time range.
- Validate the response.
- Extract order IDs.
- Request line items for each order.
const ordersResponse = await apidog.send({
method: 'GET',
url: '/orders/v0/orders',
params: {
createdAfter: new Date(Date.now() - 86_400_000).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(
(order) => order.amazon_order_id
);
apidog.variables.set('order_ids', JSON.stringify(orderIds));
4. Mock SP-API responses
Mock responses let frontend and integration work continue without repeatedly calling live endpoints.
{
"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
}
}
5. Add API tests to CI/CD
Run sandbox tests in CI before deploying changes.
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 or Access Denied
Common causes:
- Expired LWA access token
- Invalid AWS credentials
- Incorrect IAM role configuration
- IAM role not linked to the application
- Missing
x-amz-access-token - Invalid SigV4 region, date, canonical request, or signature
Log the response body and request ID:
const error = await response.json();
console.error('SP-API authorization error:', {
status: response.status,
requestId: response.headers.get('x-amzn-requestid'),
error
});
429 Rate Limit Exceeded
Use these mitigations:
- Queue requests by endpoint.
- Respect
Retry-After. - Apply exponential backoff.
- Paginate with
next_token. - Monitor
x-amzn-RateLimit-Limit. - Request higher limits through Seller Central for approved high-volume cases.
404 Not Found
Check:
- The regional endpoint matches the marketplace.
- The marketplace ID is valid.
- The resource exists.
- The API version in the route is correct, such as
/v0/or/2021-08-01/.
400 Bad Request
Typical causes:
- Invalid ISO 8601 date values
- Missing required query parameters
- Invalid marketplace ID
- Malformed JSON body
Validate date inputs before sending requests:
const validateIsoDate = (dateString) => {
const date = new Date(dateString);
if (Number.isNaN(date.getTime())) {
throw new Error('Invalid ISO 8601 date format');
}
return dateString;
};
Reports remain in INIT_STATE
Reports can take 15–30 minutes or longer depending on report type and date range.
- Poll report status about every 30 seconds.
- Confirm that the report type is available for the seller account.
- Try a smaller date range.
- Verify required permissions.
Notifications are not arriving
Check:
- Subscription status
- SNS topic resource policy
- HTTPS endpoint reachability
- SSL certificate validity
- CloudWatch delivery logs
- Response time from the webhook
Return 200 OK within 30 seconds and automatically confirm SubscriptionConfirmation messages.
Production Deployment Checklist
Before enabling production traffic:
- [ ] Register the application in production Seller Central.
- [ ] Configure a production IAM role with least-privilege permissions.
- [ ] Update OAuth redirect URIs to production URLs.
- [ ] Encrypt tokens in storage.
- [ ] Implement automatic token refresh.
- [ ] Add endpoint-specific rate limiting and queuing.
- [ ] Configure an SNS destination and subscriptions.
- [ ] Implement retries for 429 and 503 responses.
- [ ] Log API calls and Amazon request IDs.
- [ ] Monitor rate-limit usage.
- [ ] Test multiple marketplace IDs.
- [ ] Document seller onboarding and OAuth recovery steps.
- [ ] Create a runbook for common failures.
- [ ] Alert on token refresh and authentication failures.
Monitoring and alerting
Track API success rates, rate-limit usage, token refresh failures, notification processing, and report generation status.
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,
expiringSoon: 0,
refreshFailures: 0
},
notifications: {
received: 0,
processed: 0,
failed: 0
},
reports: {
pending: 0,
completed: 0,
failed: 0
}
};
const failureRate =
metrics.apiCalls.total === 0
? 0
: 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 is above 80% capacity`);
}
}
Real-World Implementation Patterns
Multi-marketplace inventory sync
A central inventory service can use InventoryLevels notifications and scheduled inventory-summary pulls to keep marketplaces synchronized.
Implementation flow:
- Receive an
InventoryLevelsSNS notification. - Fetch or reconcile current inventory state.
- Calculate available quantity for each marketplace.
- Submit listing updates through a rate-limited queue.
- Record the request and result in an audit log.
This pattern helps reduce overselling caused by delayed manual updates.
Automated order fulfillment
A fulfillment workflow can:
- Receive an
OrderStatusChangenotification. - Fetch current order details and line items.
- Send the order to a warehouse management system.
- Receive tracking data from the warehouse.
- Confirm the shipment through SP-API.
- Store every state transition for support and reconciliation.
Seller analytics dashboard
For multi-seller applications:
- Store encrypted refresh tokens per seller.
- Refresh access tokens only when needed.
- Queue report and order requests by seller and endpoint.
- Aggregate orders, inventory, listings, and reports into normalized tables.
- Keep marketplace IDs as first-class data fields.
Sellers operating across multiple storefronts may also need an Etsy integration after Amazon. The Etsy API has comparable OAuth and listing workflows, but its scopes and rate limits should be mapped separately.
Conclusion
Amazon SP-API provides access to Seller Central workflows for orders, inventory, listings, reports, fulfillment, and notifications.
For a production-ready integration:
- Use OAuth 2.0 and IAM roles with secure token storage.
- Refresh LWA access tokens automatically.
- Sign every request using AWS SigV4.
- Use SDK signing support or a well-tested signer.
- Respect endpoint-specific rate limits with queues and backoff.
- Use SNS notifications for near-real-time order and inventory workflows.
- Log request IDs, failures, rate-limit data, and token events.
- Test OAuth, signed requests, retries, and webhook processing before production.
FAQ
What is Amazon SP-API?
Amazon Selling Partner API is a REST API for accessing Seller Central data, including orders, inventory, listings, reports, and fulfillment workflows. It replaced MWS and uses OAuth 2.0 plus AWS SigV4 signing.
How do I get Amazon SP-API credentials?
Register an application in Seller Central under Apps and Services → Develop Apps. Amazon provides an Application ID, Client ID, and Client Secret. You also need an AWS IAM role linked to the application.
Is Amazon SP-API free to use?
SP-API access is free for registered Amazon sellers. Rate limits apply and vary by endpoint. Higher limits may require Amazon approval for high-volume use cases.
What authentication does SP-API use?
SP-API uses OAuth 2.0 for seller authorization, AWS IAM for access control, and AWS SigV4 for request signing.
How should I handle SP-API rate limits?
Create endpoint-specific request queues, read x-amzn-RateLimit-Limit, respect Retry-After, use exponential backoff after HTTP 429, and paginate results with next_token.
Can I test SP-API without a live seller account?
Amazon provides a sandbox environment for SP-API development. Sandbox availability differs by endpoint, so test critical workflows against the environments available to your application.
How do webhooks work with SP-API?
SP-API notifications are delivered through Amazon SNS. Create a destination, subscribe to event types, validate SNS messages at your HTTPS endpoint, confirm subscriptions, and fetch the current resource state after receiving a notification.
What happens when an OAuth token expires?
LWA access tokens expire after approximately one hour. Use the stored refresh token to get a new access token before expiration.
How do I migrate from MWS to SP-API?
Migration includes replacing MWS tokens with OAuth 2.0, adding SigV4 signing, updating endpoint URLs, and converting XML request/response handling to JSON. MWS retirement is scheduled for December 2025.
Why am I getting 403 errors?
Common causes are expired OAuth tokens, missing access-token headers, invalid SigV4 signatures, incorrect regions, IAM permission issues, or an IAM role that is not linked to the Seller Central application.
Top comments (0)