DEV Community

Cover image for How to Integrate Amazon SP API: Step-by-Step Tutorial
Wanda
Wanda

Posted on • Originally published at apidog.com

How to Integrate Amazon SP API: Step-by-Step Tutorial

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.

Try Apidog today


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)
Enter fullscreen mode Exit fullscreen mode

All requests need:

  1. AWS SigV4 signature
  2. OAuth access token
  3. IAM role permissions
  4. 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

  1. Go to Amazon Developer Central
  2. Sign in (Seller Central access required)
  3. Navigate to Selling Partner API
  4. Accept the Developer Agreement

Step 2: Register Your Application

  1. Log in to Seller Central
  2. Go to Apps and Services > Develop Apps
  3. Click Add New App
  4. 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"
Enter fullscreen mode Exit fullscreen mode

Step 3: Create IAM Role for SP-API

  1. Log in to AWS IAM Console
  2. Go to Roles > Create Role
  3. Select Another AWS account
  4. Enter Amazon’s account ID for your region:
    • North America: 906394416454
    • Europe: 336853085554
    • Far East: 774466381866

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/*"
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Name your role (e.g. SellingPartnerApiRole). Save the ARN.

Step 5: Link IAM Role to Application

  1. In Seller Central, go to Develop Apps
  2. Select your app
  3. Edit > IAM Role ARN
  4. Enter your IAM role ARN
  5. 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)
Enter fullscreen mode Exit fullscreen mode

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}`);
Enter fullscreen mode Exit fullscreen mode

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
  };
};
Enter fullscreen mode Exit fullscreen mode

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');
  }
});
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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']
});
Enter fullscreen mode Exit fullscreen mode

Order Response Structure

{
  "payload": {
    "orders": [
      {
        "amazon_order_id": "112-1234567-1234567",
        "order_status": "Unshipped",
        "purchase_date": "2026-03-19T10:30:00Z",
        ...
      }
    ],
    "next_token": "eyJleHBpcmF0aW9uVGltZU9mTmV4dFRva2VuIjoxNzEwOTUwNDAwfQ=="
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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 }
  ]
});
Enter fullscreen mode Exit fullscreen mode

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']
});
Enter fullscreen mode Exit fullscreen mode

Updating Inventory

SP-API doesn't support direct inventory update endpoints. Use:

  1. FBA Shipments: Send inventory to Amazon warehouses
  2. MFN Orders: Inventory reduced automatically when orders ship
  3. 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);
};
Enter fullscreen mode Exit fullscreen mode

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']
});
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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()
});
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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']
});
Enter fullscreen mode Exit fullscreen mode

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'
    }
  ]
};
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
      }
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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/*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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.');
  }
};
Enter fullscreen mode Exit fullscreen mode

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

  1. Download Amazon’s SP-API OpenAPI spec from the repo
  2. Create a new project in Apidog
  3. Import the spec file
  4. 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
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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-token header.
  • Error logging:
const error = await response.json();
console.error('Auth error:', error);
// Check for InvalidSignature, AccessDenied, ExpiredToken
Enter fullscreen mode Exit fullscreen mode

429 Rate Limit Exceeded

  • Queue requests and use exponential backoff.
  • Batch with next_token for 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;
};
Enter fullscreen mode Exit fullscreen mode

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`);
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. SNS notification on InventoryLevels
  2. Central system recalculates inventory
  3. API updates sent to all marketplaces
  4. 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)