The SellerMagnet API reports failures through the HTTP status and a JSON body with success, error and message. Only three statuses are worth retrying - 429 (too many requests), 502 (upstream error) and 503 (temporarily unavailable) - plus a 401 whose message reads Temporary error. For 429 and 503, wait at least the Retry-After header before trying again; the Python and Node.js code below does both. Everything else is a problem with the request itself.
Key Takeaways
- Retry only 429, 502 and 503; a 400, 401 or 404 will fail the same way again.
- Requests rejected before billing - 400, 401, 403 and 429 - never cost a credit.
- A 404 for a missing product does cost a credit, so record missing ASINs instead of re-requesting them.
- Always wait at least the number of seconds in the Retry-After header before retrying.
- Branch on the JSON message as well as the status: an out-of-credits account also answers 401.
What does each SellerMagnet API status code mean?
A status code is the fastest signal, but two of them carry more than one meaning, so the message field decides the reaction. The table reflects the API's behaviour as of September 2026, including whether the request has already been charged.
| Status | When it happens | Credit charged | Retry? |
|---|---|---|---|
| 400 | A required parameter is missing or invalid (ASIN, marketplaceId, count above 50) | No | No - fix the request |
| 401 | api_key is missing or unknown, or the account has no credits left (see message) | No | No - except a Temporary error validating API key |
| 403 | Credits ran out between the balance check and the charge | No | No - top up |
| 404 | The product or seller does not exist in that marketplace | Yes | No - record it as missing |
| 429 | Per-key concurrency cap after a 20-second queue, or the per-IP budget | No | Yes - after Retry-After, or back off if it is absent |
| 502 | An upstream error while fetching data | Yes | Yes - with backoff |
| 503 | Temporarily unavailable, usually one marketplace's circuit breaker | Yes | Yes - after Retry-After |

What are the SellerMagnet API rate limits?
Two limits apply. Each API key has a cap on concurrent requests; when it is full, the API holds the next request for up to 20 seconds waiting for a free slot, and only answers 429 with Retry-After: 2 if none opens. The 429 message states the current cap. Separately, each client IP has a budget of 600 requests per minute and 15,000 per hour as of September 2026. The per-IP 429 carries no Retry-After and no JSON body, so your client has to fall back to its own backoff.
In practice the concurrency cap is the one you meet first. A burst of scheduled jobs that all fire at the top of the minute queues and drains on its own; a sustained flood beyond the cap is what turns into 429s. The bulk lookup guide shows how to size a worker pool so that never happens.
How do I retry with Retry-After and exponential backoff?
Exponential backoff is a retry policy where each wait doubles: 1, 2, 4, 8 seconds and so on, capped at a maximum. Adding random jitter stops many clients from retrying in lockstep. When the response carries Retry-After, wait at least that long, because the server is telling you exactly when capacity returns.
A retrying GET that honours Retry-After and never retries a billed miss
import os
import random
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
import requests
API = "https://sellermagnet-api.com/api"
RETRYABLE = {429, 502, 503}
def retry_after_seconds(resp) -> float:
"""Retry-After is seconds or an HTTP date (RFC 9110); the per-IP 429 omits it."""
value = resp.headers.get("Retry-After") if resp is not None else None
if not value:
return 0.0
try:
return max(0.0, float(value))
except ValueError:
try:
when = parsedate_to_datetime(value)
return max(0.0, (when - datetime.now(timezone.utc)).total_seconds())
except (TypeError, ValueError):
return 0.0
def call(endpoint: str, max_attempts: int = 6, **params) -> dict:
params["api_key"] = os.environ["SELLERMAGNET_API_KEY"]
for attempt in range(1, max_attempts + 1):
try:
resp = requests.get(f"{API}/{endpoint}", params=params, timeout=60)
except (requests.ConnectionError, requests.Timeout):
resp = None # network blip: treat like a 502
try:
body = resp.json() if resp is not None else {}
except ValueError:
body = {} # e.g. a proxy's HTML error page
status = resp.status_code if resp is not None else 502
if status == 200 and body.get("success"):
return body["data"]
transient_auth = status == 401 and "Temporary error" in body.get("message", "")
if (status not in RETRYABLE and not transient_auth) or attempt == max_attempts:
raise RuntimeError(f"{status}: {body.get('message', 'request failed')}")
backoff = min(2 ** (attempt - 1), 32) + random.uniform(0, 1)
time.sleep(max(backoff, retry_after_seconds(resp)))
product = call("amazon-product-lookup", asin="B0CLTBHXWQ", marketplaceId="APJ6JRA9NG5V4")
print(product["productInfo"]["title"])
Every retried 502 or 503 is billed again
A 429 is free to retry, but 502 and 503 are charged per attempt. When a 503 arrives, pause every request to that marketplace for the Retry-After period instead of retrying each one - the breaker is per marketplace, so other marketplaces keep working.

The same policy in Node.js
The Node.js version uses the built-in fetch from Node 18. For a complete client with search and a worker pool, see the Node.js quickstart.
Node 18+ with the built-in fetch - no dependencies
const API = "https://sellermagnet-api.com/api";
const RETRYABLE = new Set([429, 502, 503]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// Retry-After is seconds or an HTTP date; the per-IP 429 sends none.
function retryAfterSeconds(headers) {
const value = headers.get("Retry-After");
if (!value) return 0;
const seconds = Number(value);
if (Number.isFinite(seconds)) return Math.max(0, seconds);
const date = Date.parse(value);
return Number.isNaN(date) ? 0 : Math.max(0, (date - Date.now()) / 1000);
}
export async function call(endpoint, params, maxAttempts = 6) {
const url = new URL(`${API}/${endpoint}`);
for (const [k, v] of Object.entries({ ...params, api_key: process.env.SELLERMAGNET_API_KEY })) {
url.searchParams.set(k, v);
}
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
let res, body = {};
try {
res = await fetch(url, { signal: AbortSignal.timeout(60_000) });
body = await res.json().catch(() => ({}));
} catch {
res = { status: 502, headers: new Headers() }; // network blip
}
if (res.status === 200 && body.success) return body.data;
const transientAuth = res.status === 401 && /Temporary error/.test(body.message ?? "");
if ((!RETRYABLE.has(res.status) && !transientAuth) || attempt === maxAttempts) {
throw new Error(`${res.status}: ${body.message ?? "request failed"}`);
}
const backoff = Math.min(2 ** (attempt - 1), 32) + Math.random();
await sleep(Math.max(backoff, retryAfterSeconds(res.headers)) * 1000);
}
}
Can I use urllib3's Retry instead?
Yes, for the common cases. Mounting a urllib3 Retry on a requests.Session retries 429, 502 and 503 with exponential backoff and honours Retry-After. It cannot read the JSON message, so it will not retry the transient 401; keep the custom loop above if you need that.
Declarative retries with requests + urllib3
import os
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
retry = Retry(
total=5,
backoff_factor=1, # waits grow as backoff_factor x 2^n seconds
status_forcelist=[429, 502, 503], # only the retryable statuses
allowed_methods=["GET"],
respect_retry_after_header=True,
raise_on_status=False, # hand back the last response instead of raising
)
session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry))
resp = session.get(
"https://sellermagnet-api.com/api/amazon-product-lookup",
params={"asin": "B0CLTBHXWQ", "marketplaceId": "APJ6JRA9NG5V4",
"api_key": os.environ["SELLERMAGNET_API_KEY"]},
timeout=60,
)
print(resp.status_code)
How do I see the headers when debugging?
Add -i to curl to print the status line and headers above the body. A 429 shows Retry-After: 2 and a message naming the concurrency cap; a 503 carries a Retry-After of 30 or 60 seconds.
Print status and headers for one request
curl -si "https://sellermagnet-api.com/api/amazon-product-lookup" \
--get \
--data-urlencode "asin=B0CLTBHXWQ" \
--data-urlencode "marketplaceId=APJ6JRA9NG5V4" \
--data-urlencode "api_key=$SELLERMAGNET_API_KEY" | head -n 20
A missing product is not a transient error
A 404 for an ASIN that does not exist in that marketplace has already been charged. Retrying it spends another credit to learn the same thing, so store it as missing and move on.
Before going to production, check the API status page for any open incident, and see pricing for how credits are counted. A request is charged one credit once it clears validation, authentication and the concurrency queue, whatever happens next - including a 404, 502 or 503.
Frequently Asked Questions
Does a 429 response cost a credit?
No. A 429 is returned before the credit is charged, so waiting and retrying costs only the successful attempt.
How long should I wait after a 429?
At least the Retry-After value: 2 seconds for the per-key concurrency cap. The per-IP 429 sends no Retry-After, so fall back to exponential backoff with jitter.
Why did I get a 401 with a valid key?
Read the message. A 401 also means the account has no credits left, or a temporary key-validation error that is safe to retry with backoff.
Should I retry a 404?
No. The product or seller does not exist in that marketplace, and the request has already been charged.
What is the rate limit per IP address?
600 requests per minute and 15,000 per hour per client IP as of September 2026, alongside the per-key concurrency cap.
Bottom line: retry 429, 502 and 503 with backoff and Retry-After, stop on everything else, and never re-request a 404. That policy keeps both your error rate and your credit spend flat. Ready-made snippets for other languages are on the code examples page.