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.
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.
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:
| Term | What it actually is |
|---|---|
| Developer profile | A 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. |
| Roles | Permission 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 secret | Your app's username and password for Login with Amazon (LWA), Amazon's OAuth service |
| Refresh token | A long-lived credential that says "this seller account authorized this app". You get it once by authorizing your own app. |
| Access token | A short-lived token your script gets from LWA with the refresh token. It expires after one hour and goes on every API request. |
| Regional endpoint | The API host for your region: sellingpartnerapi-na.amazon.com (North America), -eu (Europe, Middle East, India) or -fe (Japan, Australia, Singapore) |
| Marketplace ID | Which 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
- 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.
- Fill in your contact information. Under Data Access, choose Private Developer: I build application(s) that integrate my own company with Amazon Services APIs.
- 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.
- 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… | Role | Operations it unlocks |
|---|---|---|
| Read your offer prices | Pricing | Product Pricing API, including getPricing |
| Read FBA stock levels | Amazon Fulfillment | FBA Inventory API, getInventorySummaries |
| Read or update merchant-fulfilled quantities and prices | Product Listing | Listings 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
- 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.
- 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.
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
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:
| Operation | API | Rate (req/s) | Burst | Notes |
|---|---|---|---|---|
getInventorySummaries | FBA Inventory v1 | 2 | 2 | FBA stock. Up to 50 SKUs per call when you filter by SKU, or all SKUs with nextToken. |
getPricing | Product Pricing v0 | 0.5 | 1 | Your own offer prices, up to 20 SKUs or ASINs per call |
getCompetitivePricing | Product Pricing v0 | 0.5 | 1 | Competitive prices for items you sell, up to 20 per call |
getItemOffers | Product Pricing v0 | 0.5 | 1 | Offers on one ASIN |
getListingOffers | Product Pricing v0 | 1 | 2 | Offers on one of your SKUs |
getItemOffersBatch | Product Pricing v0 | 0.1 | 1 | Several getItemOffers requests in one call |
getListingsItem | Listings Items 2021-08-01 | 5 | 10 | One listing, including merchant-fulfilled quantity |
searchListingsItems | Listings Items 2021-08-01 | 5 | 5 | Page through your listings |
- Your limits can differ. Amazon can raise or lower an account's plan. The
x-amzn-RateLimit-Limitresponse 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,
getPricinghandles 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
getPricingcan'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,,
| Variable | Default | What it does |
|---|---|---|
LWA_CLIENT_ID, LWA_CLIENT_SECRET | none | Your app's LWA credentials from step 4 |
LWA_REFRESH_TOKEN | none | The refresh token from step 5 |
SPAPI_ENDPOINT | NA endpoint | Use the -eu or -fe host for those regions |
MARKETPLACE_ID | ATVPDKIKX0DER | The 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.
getInventorySummariesacceptsstartDateTimeand 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.
| Task | Use | Why |
|---|---|---|
| Sync your FBA or merchant-fulfilled stock | SP-API, always | Your 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 prices | SP-API, always | Listings Items and Product Pricing read and change prices directly. There's nothing to scrape. |
| Orders, fees, reports, returns | SP-API, always | Private account data. Any other route means sharing your Seller Central login, which breaks Amazon's policies. |
| Competitor prices on ASINs you also sell | SP-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 sell | A 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 covers | Limited, polite scraping | Public 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
| Tool | Role | Why it fits |
|---|---|---|
| No-code alternative to the API | Connects 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. | |
| No-code alternative to the API | Product and keyword research with sales estimates, plus seller tools that sync with your account. Strong for research on products you're thinking of selling. | |
| Server for the hourly sync | Hourly-billed cloud servers in many regions. The smallest plan runs this script easily. Pick a region near the SP-API endpoint you call. | |
| Server for the hourly sync | Simple 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
urlencodefor 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.
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.