David Lewin David Lewin
E-Commerce, Research & Ads

Amazon SP-API for price and inventory sync: a plain-English setup guide

Disclosure: some links in this guide are affiliate links. If you sign up through them, I may earn a commission at no extra cost to you. How this site makes money.

Short answer

To sync your own Amazon prices and stock, register as a private developer in Seller Central, give your app the Pricing and Amazon Fulfillment roles, and self-authorize it to get a refresh token. Your script swaps that refresh token for a one-hour access token at Login with Amazon (LWA), then calls the SP-API with it in the x-amz-access-token header. AWS IAM and request signing have not been required since October 2023. Stay inside each operation's usage plan: getInventorySummaries allows 2 requests per second and getPricing 0.5 per second for up to 20 SKUs.

Amazon's Selling Partner API (SP-API) is how you read and update your own seller data in code: stock levels, prices, orders and listings. The official documentation is thorough, but it's spread over hundreds of pages, and many tutorials still describe an AWS IAM setup that Amazon dropped in 2023. This guide walks through the setup in plain English, from developer registration to your first API call, and gives you one Python file that pulls your FBA inventory and current listing prices into a CSV while staying inside Amazon's published rate limits.

How this guide was made

The setup steps and rate limits come from Amazon's SP-API documentation and the official API models on GitHub, checked in October 2026. The script was tested end to end against a local mock of Login with Amazon and the SP-API that enforces the same rate and burst limits, paginates with nextToken, and returns HTTP 429 and 403 errors. It has not been run against a live seller account for this guide, so test it with your own credentials before you rely on it.

1. The pieces, in plain English

Most of the confusion comes from vocabulary. Here's what each term means for a seller who just wants to sync their own data:

TermWhat it actually is
Developer profileA one-time registration in Seller Central that says who you are and what data you need. As a seller building tools for your own business, you're a private developer.
RolesPermission groups you ask for in the profile, such as Pricing or Amazon Fulfillment. Each API operation needs one of them. These are SP-API roles, not AWS IAM roles.
App (app client)A named entry for your script, with its own roles and credentials. One profile can have several apps.
LWA client ID and secretYour app's username and password for Login with Amazon (LWA), Amazon's OAuth service
Refresh tokenA long-lived credential that says "this seller account authorized this app". You get it once by authorizing your own app.
Access tokenA short-lived token your script gets from LWA with the refresh token. It expires after one hour and goes on every API request.
Regional endpointThe API host for your region: sellingpartnerapi-na.amazon.com (North America), -eu (Europe, Middle East, India) or -fe (Japan, Australia, Singapore)
Marketplace IDWhich store you mean, such as ATVPDKIKX0DER for amazon.com. Each marketplace has a fixed ID listed in the docs.

2. Setup, step by step

Before you start

  • A Professional seller account. Developer registration runs through Seller Central.
  • Access as the account's Primary User. Amazon only lets the Primary User self-authorize an app.
  • A place to keep secrets that isn't your code or a shared drive: environment variables on the server, or a secrets manager.

Step 1: Register as a private developer

  1. In Seller Central, open the menu and go to Apps and Services > Develop Apps. If your account has moved to the Solution Provider Portal, go to Settings > Developer Profile there instead.
  2. Fill in your contact information. Under Data Access, choose Private Developer: I build application(s) that integrate my own company with Amazon Services APIs.
  3. Select the roles you need (see step 2), describe your use case in a few honest sentences ("sync FBA stock levels and our own listing prices to our inventory system every hour") and answer the security questions.
  4. Accept the Solution Provider Portal Agreement, Acceptable Use Policy and Data Protection Policy, then choose Register.

Amazon reviews the profile and may open a case asking for more detail. Watch the admin email on the account: if you don't answer a case within five days, it closes and you have to start again.

Step 2: Pick the right roles

You want to…RoleOperations it unlocks
Read your offer pricesPricingProduct Pricing API, including getPricing
Read FBA stock levelsAmazon FulfillmentFBA Inventory API, getInventorySummaries
Read or update merchant-fulfilled quantities and pricesProduct ListingListings Items API, getListingsItem and patchListingsItem

Ask only for what you'll use. Roles that give access to buyer personal data (such as names and addresses on orders) are restricted and need a much longer security review. Price and stock sync doesn't need them. Amazon's roles page maps every operation to its role.

Step 3: Skip the AWS IAM setup

Older guides tell you to create an AWS account, an IAM user and role, paste a Role ARN into your app and sign every request with AWS Signature Version 4. You don't need any of that. On October 2, 2023, Amazon announced that the SP-API no longer requires AWS IAM or Signature Version 4, in every region. Requests are authorized with the LWA access token alone. If a request still carries a signature, Amazon ignores it. If you already created IAM resources for this, you can delete them.

Step 4: Create the app and copy its LWA credentials

  1. Once the profile is approved, go back to Develop Apps and add a new app client. Give it a name, choose SP-API as the API type, and pick its roles from the ones your profile was granted.
  2. Next to the app, open the LWA credentials and copy the Client identifier (it starts with amzn1.application-oa2-client.) and the Client secret.

The client secret expires on a schedule Amazon sets, so expect to rotate it. Amazon emails the developer contact before it expires. Put a reminder in your calendar as well.

Step 5: Self-authorize to get a refresh token

On the Develop Apps page, choose Authorize app next to your app, then authorize it for your seller account. Amazon shows a refresh token that starts with Atzr|. Each click creates a new one. Copy it straight into your server's environment or secrets manager, and never into code, logs, a URL or a chat message. Anyone with the client ID, secret and refresh token can read your seller data.

Step 6: Test the credentials

Exchange the refresh token for an access token at LWA. A JSON response with an access_token and "expires_in": 3600 means the setup works:

curl -s https://api.amazon.com/auth/o2/token \
  -H 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8' \
  --data-urlencode grant_type=refresh_token \
  --data-urlencode "refresh_token=$LWA_REFRESH_TOKEN" \
  --data-urlencode "client_id=$LWA_CLIENT_ID" \
  --data-urlencode "client_secret=$LWA_CLIENT_SECRET"

An invalid_client error means the client ID or secret is wrong. An invalid_grant error means the refresh token is wrong or was revoked: authorize the app again to get a new one.

Does the SP-API cost anything?

For sellers calling the API for their own business, no. In November 2025 Amazon announced an annual fee and usage fees for third-party developers whose apps serve other sellers, and said sellers using the API only for their own business wouldn't pay. It then delayed the plan and, in May 2026, said it "will not move forward with the SP-API usage and annual fees at this time." Check the developer portal's announcements if you plan to build an app for other sellers.

3. How a request is authorized

SP-API authorization flow: from your script to your data
SP-API authorization flow: from your script to your dataScript → Request LWA token → Receive token → Make SP-API request / Wait for rate limit → Get dataScript: holds the LWA client ID, client secret and refresh tokenScriptholds the LWA client ID, client secret and refresh tokenRequest LWA token: POST to api.amazon.com/auth/o2/token with grant_type=refresh_tokenRequest LWA tokenPOST to api.amazon.com/auth/o2/token withgrant_type=refresh_tokenReceive token: access_token valid for 3,600 seconds. Cache it and reuse it for the hour.Receive tokenaccess_token valid for 3,600 seconds. Cache it and reuse it forthe hour.Make SP-API request: regional endpoint with the x-amz-access-token and user-agent headersMake SP-API requestregional endpoint with thex-amz-access-token anduser-agent headersWait for rate limit: each operation has its own rate and burst. Back off on 429.Wait for rate limiteach operation has its ownrate and burst. Back off on429.Get data: JSON payload. Follow nextToken for the next page.Get dataJSON payload. Follow nextToken for the next page.
The script asks for a new access token only when the cached one is about to expire. Hover or tab through the boxes for details.

4. Rate limits: the official numbers

Every SP-API operation has a usage plan: a steady rate in requests per second and a burst, the number of requests you can send at once before the rate applies. Think of a bucket that holds burst tokens and refills at rate tokens per second. Each request takes one token, and a request with no token left gets HTTP 429 QuotaExceeded. These are the default plans in Amazon's published API models as of October 2026:

OperationAPIRate (req/s)BurstNotes
getInventorySummariesFBA Inventory v122FBA stock. Up to 50 SKUs per call when you filter by SKU, or all SKUs with nextToken.
getPricingProduct Pricing v00.51Your own offer prices, up to 20 SKUs or ASINs per call
getCompetitivePricingProduct Pricing v00.51Competitive prices for items you sell, up to 20 per call
getItemOffersProduct Pricing v00.51Offers on one ASIN
getListingOffersProduct Pricing v012Offers on one of your SKUs
getItemOffersBatchProduct Pricing v00.11Several getItemOffers requests in one call
getListingsItemListings Items 2021-08-01510One listing, including merchant-fulfilled quantity
searchListingsItemsListings Items 2021-08-0155Page through your listings
  • Your limits can differ. Amazon can raise or lower an account's plan. The x-amzn-RateLimit-Limit response header, when it's present, shows the rate that applied to that request.
  • Limits are per operation for your app and seller account, so inventory calls don't use up your pricing quota.
  • Do the math before you schedule. At 0.5 requests per second with 20 SKUs each, getPricing handles about 10 SKUs per second. 3,000 SKUs take 150 calls, about 5 minutes. That's fine hourly and wasteful every minute.

The script below gives each operation its own token bucket with these numbers, so it waits before a request instead of sending it and collecting 429s. When a 429 still comes back, it backs off exponentially.

5. The script: spsync.py

One file, Python's standard library only. It gets and caches the LWA token, pages through your FBA inventory, looks up your listing price for each SKU in batches of 20, and writes a CSV to stdout. Credentials come from environment variables, never from the file.

#!/usr/bin/env python3
"""spsync: pull your own FBA inventory and offer prices from Amazon SP-API into a CSV.

Python 3.9+, standard library only. Credentials come from environment variables:
  LWA_CLIENT_ID, LWA_CLIENT_SECRET, LWA_REFRESH_TOKEN
  SPAPI_ENDPOINT   default https://sellingpartnerapi-na.amazon.com
  MARKETPLACE_ID   default ATVPDKIKX0DER (amazon.com)
  python3 spsync.py > stock_and_prices.csv
"""
import csv
import json
import os
import random
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

UA = 'spsync/1.0 (Language=Python/3)'  # SP-API requires a user-agent with app name and version
LWA_URL = os.environ.get('LWA_TOKEN_URL', 'https://api.amazon.com/auth/o2/token')
ENDPOINT = os.environ.get('SPAPI_ENDPOINT', 'https://sellingpartnerapi-na.amazon.com').rstrip('/')
MARKETPLACE = os.environ.get('MARKETPLACE_ID', 'ATVPDKIKX0DER')

# Usage plans from Amazon's API models: (requests per second, burst)
LIMITS = {'getInventorySummaries': (2, 2), 'getPricing': (0.5, 1)}


class Token:
    """LWA access token, refreshed a minute before its one-hour expiry."""
    def __init__(self):
        self.value, self.expires = None, 0

    def get(self, force=False):
        if force or time.time() > self.expires - 60:
            body = urllib.parse.urlencode({
                'grant_type': 'refresh_token',
                'refresh_token': os.environ['LWA_REFRESH_TOKEN'],
                'client_id': os.environ['LWA_CLIENT_ID'],
                'client_secret': os.environ['LWA_CLIENT_SECRET']}).encode()
            req = urllib.request.Request(LWA_URL, body, {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'})
            with urllib.request.urlopen(req, timeout=30) as r:
                t = json.load(r)
            self.value, self.expires = t['access_token'], time.time() + t['expires_in']
        return self.value


class Bucket:
    """Token bucket matching one operation's rate and burst, so we wait instead of getting 429s."""
    def __init__(self, rate, burst):
        self.rate, self.burst, self.tokens, self.last = rate, burst, burst, time.monotonic()

    def take(self):
        now = time.monotonic()
        self.tokens = min(self.burst, self.tokens + (now - self.last) * self.rate)
        self.last = now
        if self.tokens < 1:
            time.sleep((1 - self.tokens) / self.rate + 0.1)  # small margin for network jitter
            self.tokens, self.last = 1, time.monotonic()
        self.tokens -= 1


BUCKETS = {op: Bucket(*rb) for op, rb in LIMITS.items()}


def call(token, op, path, params, retries=5):
    """GET an SP-API operation within its rate limit. Backs off on 429/5xx, refreshes the token once on 403."""
    url = '%s%s?%s' % (ENDPOINT, path, urllib.parse.urlencode(params))
    refreshed = False
    for attempt in range(retries + 1):
        BUCKETS[op].take()
        req = urllib.request.Request(url, headers={'x-amz-access-token': token.get(), 'user-agent': UA})
        try:
            with urllib.request.urlopen(req, timeout=30) as r:
                return json.load(r)
        except urllib.error.HTTPError as e:
            if e.code == 403 and not refreshed:  # token revoked or expired early
                token.get(force=True)
                refreshed = True
                continue
            if e.code not in (429, 500, 502, 503) or attempt == retries:
                print('%s failed: HTTP %d %s' % (op, e.code, e.read()[:300]), file=sys.stderr)
                raise
            wait = min(2 ** attempt / LIMITS[op][0], 120) + random.uniform(0, 1)
            print('  %s HTTP %d (limit %s), retrying in %.0fs' % (op, e.code, e.headers.get('x-amzn-RateLimit-Limit'), wait), file=sys.stderr)
            time.sleep(wait)


def inventory(token):
    """All FBA inventory summaries for the marketplace, following nextToken."""
    params = {'granularityType': 'Marketplace', 'granularityId': MARKETPLACE,
              'marketplaceIds': MARKETPLACE, 'details': 'true'}
    rows = []
    while True:
        r = call(token, 'getInventorySummaries', '/fba/inventory/v1/summaries', params)
        rows += r['payload']['inventorySummaries']
        nxt = (r.get('pagination') or {}).get('nextToken')
        if not nxt:  # nextToken expires 30 s after it's issued, so fetch the next page right away
            return rows
        params['nextToken'] = nxt


def prices(token, skus):
    """Your current listing price per SKU, 20 SKUs per getPricing call."""
    out = {}
    for i in range(0, len(skus), 20):
        r = call(token, 'getPricing', '/products/pricing/v0/price',
                 {'MarketplaceId': MARKETPLACE, 'ItemType': 'Sku', 'Skus': ','.join(skus[i:i + 20])})
        for p in r['payload']:
            offers = (p.get('Product') or {}).get('Offers') or []
            if p.get('status') == 'Success' and offers:
                listing = offers[0]['BuyingPrice']['ListingPrice']
                out[p['SellerSKU']] = (listing['Amount'], listing['CurrencyCode'])
    return out


def main():
    token = Token()
    inv = inventory(token)
    price = prices(token, [s['sellerSku'] for s in inv])
    w = csv.writer(sys.stdout)
    w.writerow(['sku', 'asin', 'fulfillable', 'inbound', 'total', 'price', 'currency'])
    for s in inv:
        d = s.get('inventoryDetails') or {}
        inbound = sum(d.get(k, 0) for k in ('inboundWorkingQuantity', 'inboundShippedQuantity', 'inboundReceivingQuantity'))
        amount, currency = price.get(s['sellerSku'], ('', ''))
        w.writerow([s['sellerSku'], s['asin'], d.get('fulfillableQuantity', 0), inbound, s.get('totalQuantity', 0), amount, currency])
    print('%d SKUs, %d with a price' % (len(inv), len(price)), file=sys.stderr)


if __name__ == '__main__':
    main()

A few design choices worth knowing:

  • One token per hour, not per request. The access token is cached and refreshed a minute before it expires. If a request comes back 403, the script gets a fresh token once and retries, which covers a token revoked early.
  • The bucket waits, the retry backs off. The bucket keeps you under the published rate. The retry loop handles the 429s and 5xx errors that happen anyway, waiting longer each time, up to 2 minutes.
  • nextToken is used right away. Inventory page tokens expire 30 seconds after they're issued, so the script fetches pages back to back. At 2 requests per second, it never gets close to that.
  • Failed SKUs don't stop the run. A SKU that getPricing can't price (an inactive listing, for example) gets an empty price column instead of an error.

6. Run it

mkdir -p /opt/spsync && cd /opt/spsync
# copy spsync.py here, then put the credentials in a file only you can read:
cat > .env <<'EOF'
export LWA_CLIENT_ID=amzn1.application-oa2-client.xxxxxxxx
export LWA_CLIENT_SECRET=xxxxxxxx
export LWA_REFRESH_TOKEN='Atzr|xxxxxxxx'
export SPAPI_ENDPOINT=https://sellingpartnerapi-na.amazon.com
export MARKETPLACE_ID=ATVPDKIKX0DER
EOF
chmod 600 .env

. ./.env && python3 spsync.py > stock_and_prices.csv

Progress and retries go to stderr, and the CSV goes to stdout. Here's the format, from the mock API used for testing:

sku,asin,fulfillable,inbound,total,price,currency
SKU-000,B000000000,0,5,5,10.0,USD
SKU-001,B000000001,1,5,6,10.25,USD
SKU-002,B000000002,2,5,7,10.5,USD
SKU-007,B000000007,7,5,12,,
VariableDefaultWhat it does
LWA_CLIENT_ID, LWA_CLIENT_SECRETnoneYour app's LWA credentials from step 4
LWA_REFRESH_TOKENnoneThe refresh token from step 5
SPAPI_ENDPOINTNA endpointUse the -eu or -fe host for those regions
MARKETPLACE_IDATVPDKIKX0DERThe marketplace to read. Run once per marketplace.

Schedule it

For most sellers, hourly is plenty: FBA stock moves with orders and inbound shipments, not by the second. Add this with crontab -e:

0 * * * * cd /opt/spsync && . ./.env && python3 spsync.py > stock_and_prices.tmp 2>>run.log && mv stock_and_prices.tmp stock_and_prices.csv

The CSV is only replaced when the run succeeds, so whatever reads it (your ERP, a Google Sheet import or your own store) never sees a half-written file. From there:

  • Fetch only changes. getInventorySummaries accepts startDateTime and returns only SKUs whose inventory changed since then. Useful once your catalog runs into the thousands.
  • Merchant-fulfilled stock isn't in the FBA Inventory API. Read and update it with the Listings Items API (getListingsItem, patchListingsItem), which also changes your prices.
  • Use the sandbox for write calls. Each regional endpoint has a sandbox. host that returns static test responses, so you can test a price update without changing a live listing.

7. SP-API vs scraping: which one when

The SP-API only shows you data you're entitled to: your own account, plus the public catalog and offer information Amazon chooses to expose. That's the line to work from. Use the API for anything about your own business, and treat scraping Amazon's public pages as a narrow, last-resort tool for public data you can't get any other way.

TaskUseWhy
Sync your FBA or merchant-fulfilled stockSP-API, alwaysYour stock levels aren't public, and the API is the authoritative source. Scraping your own listing tells you nothing about inbound or reserved units.
Read or update your own pricesSP-API, alwaysListings Items and Product Pricing read and change prices directly. There's nothing to scrape.
Orders, fees, reports, returnsSP-API, alwaysPrivate account data. Any other route means sharing your Seller Central login, which breaks Amazon's policies.
Competitor prices on ASINs you also sellSP-API (getItemOffers, getCompetitivePricing)The API already returns the competing offers and the featured offer on items you list.
Price history and sales estimates for ASINs you don't sellA data provider (Keepa, Helium 10, Jungle Scout)They already track this at scale. Buying the data is cheaper and cleaner than collecting it.
Public product data no API or provider coversLimited, polite scrapingPublic pages only, no login, low request rates, respect robots.txt and Amazon's Conditions of Use. Never reuse Amazon's content as your own.

For other stores, our guide to monitoring Shopify competitor prices with products.json covers a public, structured feed that doesn't need scraping at all.

8. Security and compliance

  • Treat the three credentials like a password to Seller Central. Keep them in environment variables or a secrets manager, readable only by the user that runs the job. Never commit them to Git.
  • Rotate the client secret before it expires and update the server the same day, or every call starts failing with invalid_client.
  • Keep what you store to what you need. SKUs, ASINs, quantities and prices aren't personal data. If you later add order data, Amazon's Data Protection Policy sets rules for encryption, retention and access.
  • Stay inside the Acceptable Use Policy. Use the data to run your business on Amazon. Don't resell it, and don't call the API more often than you need.

9. Tools

Top picks

ToolRoleWhy it fits
Helium 10No-code alternative to the APIConnects to your Seller Central account and gives you inventory alerts, restock planning and listing tools, plus product research data for ASINs you don't sell. A good choice if you want the data without writing code.
Jungle ScoutNo-code alternative to the APIProduct and keyword research with sales estimates, plus seller tools that sync with your account. Strong for research on products you're thinking of selling.
VultrServer for the hourly syncHourly-billed cloud servers in many regions. The smallest plan runs this script easily. Pick a region near the SP-API endpoint you call.
DigitalOceanServer for the hourly syncSimple Droplets with clear docs, a comfortable choice for your first Linux server and cron job.

For a head-to-head of the two research suites, see Helium 10 vs Jungle Scout.

Alternatives: free tools and tools we have no affiliation with

We don't earn anything from these. They're here because for some readers they're the better choice.

  • Keepa API: long-running price and sales-rank history for Amazon products, paid by request tokens. The standard answer for price history on ASINs you don't sell.
  • python-amazon-sp-api: an open-source (MIT) Python wrapper that covers most SP-API endpoints, with token handling built in. Use it once you need more than the two operations in this guide.
  • Amazon's official SP-API SDK: Amazon-maintained client libraries for Python, Java, C#, JavaScript and PHP, generated from the same API models this guide's rate limits come from.
  • Oracle Cloud Free Tier or a Raspberry Pi: a free always-on machine for an hourly cron job. Keep the credentials file locked down either way.

10. Pitfalls

  • Wrong region. Calls for a UK account sent to the NA endpoint fail. Match the endpoint to the marketplace's region.
  • New roles need a new authorization. If you add a role to your app later, authorize the app again and use the new refresh token, or calls that need the new role return 403.
  • Special characters in SKUs. SKUs with spaces, slashes or commas must be URL-encoded. The script uses urlencode for this. Hand-built URLs are the usual cause of mysterious "invalid SKU" errors.
  • Pan-European FBA. Inventory is reported per marketplace you ask for. Run the script once per marketplace ID instead of assuming one call covers Europe.
  • Retrying in a tight loop. Hammering an operation after a 429 keeps the bucket empty. Wait as the script does.
  • Silent cron failures. An expired client secret fails every run. Check run.log, or alert when the CSV is more than two hours old.
Bottom line

Register as a private developer, ask for the Pricing and Amazon Fulfillment roles, create an app and self-authorize it for a refresh token. Skip AWS IAM: since October 2023, a one-hour LWA access token in the x-amz-access-token header is all a request needs. Cache that token, give each operation its own rate limit (2 requests per second for inventory, 0.5 for pricing) and back off on 429. Use the API for everything about your own account, and a data provider rather than a scraper for ASINs you don't sell.

David Lewin
David Lewin

Founder of StealthScale.io and e-commerce infrastructure engineer writing field-tested playbooks on proxies, cloud VPS, anti-detect browsers and SEO.