Amazon API refund on error, in short: the SellerMagnet API gives a credit back automatically when a request ends in HTTP 500 without a Retry-After header. That applies to every endpoint since 19 Sep 2026, every time, however long the request ran, and there is no ticket to file. A 404, a 502, a 503 and a 500 that carries Retry-After stay charged. A 429 and the parameter checks are never charged at all.
Key Takeaways
- A 500 without Retry-After is refunded every time: the balance gets the credit back and the request's usage row reads Credits Used 0.
- 404, 502, 503 and a 500 with Retry-After keep the credit, and every retry of a 502 or 503 costs another one.
- 429, 401, 403 and the parameter-check 400s are rejected before a credit is taken, so they never show up in the usage history.
- The credit is taken before the data is fetched: a client that gives up early has paid and gets nothing back.
- The Usage History on your dashboard's Analytics page shows each refund as Credits Used 0, Status 500.
Amazon API refund on error: the rule in one table
Every request passes the key check, the parameter checks and a limit on how many requests one key may run at once. Only then is the credit taken, and only then is the data fetched. So anything rejected before that point costs nothing, anything after it is charged, and the refund is the one correction afterwards. This is about SellerMagnet API credits, not about refunding an Amazon order or an Amazon Pay charge. Each request costs 1 credit; the table shows what each answer leaves on your bill, and how often the quickstart's get_with_retry sends the request again.
| Answer | Credit taken | Refunded | Net cost | Retried by get_with_retry |
|---|---|---|---|---|
| 200 success | yes | no | 1 | no |
| 404 not found | yes | no | 1 | no |
| 500 without Retry-After | yes | yes, at once | 0 | once |
| 500 with Retry-After | yes | no | 1 | once |
| 502 retryable error | yes | no | 1 per attempt | up to 4 attempts |
| 503 with Retry-After | yes | no | 1 per attempt | up to 4 attempts |
| 429 too many requests | no | nothing to refund | 0 | up to 4 attempts |
| 401 / 403 key or balance | no | nothing to refund | 0 | no |
| 400 from a parameter check | no | nothing to refund | 0 | no |
| 400 from an endpoint check | yes | no | 1 | no |
| no answer (client timeout) | yes, if it arrived | no | 0 or 1 | up to 4 attempts |
The parameter-check 400s are a missing parameter, an unknown marketplaceId, an endpoint the marketplace does not cover and an unknown or ambiguous bestseller category. The endpoint-check 400s come after the charge: count above 50 ("Count Max: 50") or a malformed ASIN on statistics and estimated sales. The credit map by endpoint lists each one per endpoint.

Why is a 500 refunded but a 502 is not?
Because they mean different things. A 500 is the API's own failure: an internal error, or an answer it could not complete, such as a failed sales estimate or an incomplete offers result. An unexpected crash counts as a 500 too. Since 19 Sep 2026 that credit goes back automatically, after 1 second or after 100. A 502 means the data could not be fetched from Amazon on this attempt, and a 503 carries a Retry-After header that asks you to wait that many seconds. Both are retryable and both stay charged, so every retry costs another credit. A 404 is a real answer: the product or seller does not exist on that marketplace.
The header is the deciding detail. A 500 that carries Retry-After is treated like a 502 or 503 and keeps the credit, so read the header, not only the status code, when you predict a bill. The body of a refunded 500 looks like any other error, so the status and the missing header are all you need:
The body of a refunded 500 (HTTP status 500, no Retry-After header)
{
"success": false,
"error": "Internal server error",
"message": "An internal server error occurred"
}
How do I confirm a refund landed?
Open the Analytics page of your SellerMagnet dashboard. Its Usage History table lists one row per request that got past the checks: Timestamp (UTC), Endpoint, API Key (the first 10 characters), Credits Used and Status. A refunded request reads Credits Used 0 and Status 500. A charged failure reads Credits Used 1 with its status: 404, 502 or 503. Requests rejected before the charge (429, 401, 403 and the parameter-check 400s) have no row at all.
- The refund is booked before the 500 leaves the API, so the row is already corrected when your code reads the error.
- The Usage Over Time chart and the credits-used total on the dashboard home add up Credits Used, so refunded requests never count there.
- The table shows 10 rows a page; add
per_page=100to the page address to see 100. - Rows are kept for at least 120 days, long enough to check any recent bill.
Track refunds per request in Python
To compare a bill with the Usage History you need your own record of every attempt, and get_with_retry hides its retries. The script below wraps the session in a small meter: each HTTP attempt lands in credit_ledger.csv with its UTC time, status, Retry-After header and net credits, retries included. It reads the Buy Box for a list of ASINs on amazon.com, skips duplicates and malformed ASINs before paying (offers does not check the ASIN format, so a typo would be a charged request) and stops on a 401 or 403. It uses sellermagnet.py (ApiError, new_session) and retry.py (get_with_retry) from the Python quickstart.
credit_ledger.py: Buy Box per ASIN, plus a ledger of what every attempt cost
"""credit_ledger.py: read the Buy Box for a list of ASINs and record what every attempt cost in credits.
get_with_retry hides its retries, so the session is wrapped: every HTTP attempt lands in credit_ledger.csv with its
status and net credits. A 500 without Retry-After is refunded; 404, 502, 503 and a 500 with Retry-After are charged;
429, 401, 403 and the parameter checks are rejected before a credit is taken. Compare the totals with the Usage
History on the Analytics page of your dashboard.
Helpers: sellermagnet.py and retry.py from https://sellermagnet-api.com/blog/amazon-product-api-python
"""
import csv
import re
import time
import requests
from retry import get_with_retry
from sellermagnet import ApiError, new_session
MARKETPLACE = "ATVPDKIKX0DER" # amazon.com
ENDPOINT = "amazon-product-offers"
ASINS = ["B0CL61F39H", "B0DHX1R2K7", "B0DJ4M8Q2C", "B0DKP7T3VW", "B0DL2N9F6E", "B0DM5R1H8A", "B0DN8C4T0Y",
"B0DP3W6K9U", "b0cl61f39h", "B0DR1234"]
LEDGER = "credit_ledger.csv"
# The two parameter checks that run before a credit is taken. /amazon-product-offers has no other 400; on
# other endpoints some 400s are charged (Count Max: 50, a malformed ASIN on statistics).
FREE_400 = ("Missing required parameter", "Invalid marketplaceId")
def net_credits(status: int, retry_after: str, message: str) -> int:
"""Credits one answered attempt costs once any refund has landed."""
if status == 500:
return 1 if retry_after else 0 # refunded, unless the 500 carries Retry-After
if status in (401, 403, 429):
return 0 # rejected before a credit is taken
if status == 400:
return 0 if message.startswith(FREE_400) else 1
return 1 # 200, 404, 502, 503: charged
class MeteredSession:
"""Hands every GET to a requests.Session and records each attempt, retries included."""
def __init__(self, session: requests.Session):
self.session, self.rows = session, []
def get(self, url, params=None, timeout=None):
row = {"utc": time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(time.time())),
"endpoint": url.rsplit("/", 1)[1], "asin": (params or {}).get("asin", ""),
"status": "no answer", "retry_after": "", "credits": "?"}
self.rows.append(row)
resp = self.session.get(url, params=params, timeout=timeout) # a timeout leaves the row at "?"
message = ""
if resp.status_code == 400:
try:
message = resp.json().get("message", "")
except ValueError: # not JSON: treat it as charged
pass
retry_after = resp.headers.get("Retry-After", "")
row.update(status=resp.status_code, retry_after=retry_after,
credits=net_credits(resp.status_code, retry_after, message))
return resp
def main(asins: list) -> None:
meter = MeteredSession(new_session())
seen = set()
print(f"{ENDPOINT} on {MARKETPLACE}, {len(asins)} input rows")
print(f"{'asin':<12}{'attempts':<16}{'credits':>7} result")
for raw in asins:
asin = raw.strip().upper()
if not re.fullmatch(r"[A-Z0-9]{10}", asin) or asin in seen:
# offers does not check the ASIN format, so a typo would be a charged request
why = "duplicate" if asin in seen else "not an ASIN"
print(f"{raw:<12}{'-':<16}{0:>7} skipped: {why}, nothing sent")
continue
seen.add(asin)
first = len(meter.rows)
stop = False
try:
data = get_with_retry(meter, ENDPOINT, asin=asin, marketplaceId=MARKETPLACE)
total = (data.get("buyBox") or {}).get("totalPrice")
result = f"Buy Box {total:.2f}" if total else "no Buy Box"
result += f", {len(data.get('offers') or [])} offers"
except ApiError as err:
result, stop = f"failed: {err}", err.status in (401, 403)
except (requests.ConnectionError, requests.Timeout) as err:
result = f"failed: {type(err).__name__}, no answer"
rows = meter.rows[first:]
spent = sum(r["credits"] for r in rows if r["credits"] != "?")
unknown = sum(r["credits"] == "?" for r in rows)
cost = f"{spent}+{unknown}?" if unknown else str(spent)
print(f"{asin:<12}{','.join(str(r['status']) for r in rows):<16}{cost:>7} {result}")
if stop: # a bad key or an empty balance fails every request after it
print("stopped: fix the key or the balance first")
break
rows = meter.rows
answered = [r for r in rows if r["credits"] != "?"]
charged = sum(r["credits"] for r in answered)
refunded = sum(r["status"] == 500 and r["credits"] == 0 for r in answered)
unknown = len(rows) - len(answered)
extra = f" (plus up to {unknown})" if unknown else ""
print(f"\nattempts {len(rows)}: {charged} charged, {refunded} refunded, "
f"{len(answered) - charged - refunded} free, {unknown} without an answer")
print(f"net spend: {charged} credits{extra}")
if rows:
print(f"dashboard check, Usage History from {rows[0]['utc']} UTC:")
print(f" {charged + refunded} rows expected{extra}, {refunded} with Credits Used 0 and status 500")
with open(LEDGER, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["utc", "endpoint", "asin", "status", "retry_after", "credits"])
writer.writeheader()
writer.writerows(rows)
print(f"ledger: {LEDGER} ({len(rows)} attempts)")
if __name__ == "__main__":
main(ASINS)
Output (illustrative data: a refunded 500, two refunded 500s in a row, a billed 404, a free 429, two charged 502s, a timeout and a 503)
amazon-product-offers on ATVPDKIKX0DER, 10 input rows
asin attempts credits result
B0CL61F39H 200 1 Buy Box 444.98, 6 offers
B0DHX1R2K7 404 1 failed: 404: Invalid product
B0DJ4M8Q2C 500,200 1 Buy Box 444.98, 6 offers
B0DKP7T3VW 500,500 0 failed: 500: An internal server error occurred
B0DL2N9F6E 429,200 1 no Buy Box, 0 offers
B0DM5R1H8A 502,502,200 3 Buy Box 444.98, 6 offers
B0DN8C4T0Y no answer,200 1+1? Buy Box 444.98, 6 offers
B0DP3W6K9U 503,200 2 Buy Box 19.99, 3 offers
b0cl61f39h - 0 skipped: duplicate, nothing sent
B0DR1234 - 0 skipped: not an ASIN, nothing sent
attempts 15: 10 charged, 3 refunded, 1 free, 1 without an answer
net spend: 10 credits (plus up to 1)
dashboard check, Usage History from 2026-11-06 13:00:00 UTC:
13 rows expected (plus up to 1), 3 with Credits Used 0 and status 500
ledger: credit_ledger.csv (15 attempts)
credit_ledger.csv from the same run: one line per HTTP attempt
utc,endpoint,asin,status,retry_after,credits
2026-11-06 13:00:00,amazon-product-offers,B0CL61F39H,200,,1
2026-11-06 13:00:01,amazon-product-offers,B0DHX1R2K7,404,,1
2026-11-06 13:00:02,amazon-product-offers,B0DJ4M8Q2C,500,,0
2026-11-06 13:00:03,amazon-product-offers,B0DJ4M8Q2C,200,,1
2026-11-06 13:00:04,amazon-product-offers,B0DKP7T3VW,500,,0
2026-11-06 13:00:05,amazon-product-offers,B0DKP7T3VW,500,,0
2026-11-06 13:00:06,amazon-product-offers,B0DL2N9F6E,429,2,0
2026-11-06 13:00:07,amazon-product-offers,B0DL2N9F6E,200,,1
2026-11-06 13:00:08,amazon-product-offers,B0DM5R1H8A,502,,1
2026-11-06 13:00:09,amazon-product-offers,B0DM5R1H8A,502,,1
2026-11-06 13:00:10,amazon-product-offers,B0DM5R1H8A,200,,1
2026-11-06 13:00:11,amazon-product-offers,B0DN8C4T0Y,no answer,,?
2026-11-06 13:00:12,amazon-product-offers,B0DN8C4T0Y,200,,1
2026-11-06 13:00:13,amazon-product-offers,B0DP3W6K9U,503,60,1
2026-11-06 13:00:14,amazon-product-offers,B0DP3W6K9U,200,,1
- 500,200 / 1 credit is a refunded 500 followed by a charged answer: the refund keeps the retry at the price of one request.
- 500,500 / 0 credits cost nothing and returned nothing.
get_with_retrystops after the second 500 in a row and reports it, so try that ASIN again later. - 429,200 / 1 credit: the 429 was rejected before any charge. Its row has no credits and no Usage History entry.
- 502,502,200 / 3 credits: each 502 attempt is charged, which is why the helper caps a request at 4 attempts.
- no answer,200 / 1+1?: the first attempt timed out. If it reached the API, it cost a credit that is not refunded; only the Usage History can tell.
What did the example run cost?
Ten input rows went in. Two were skipped before paying (a duplicate and an 8-character ASIN), so 8 ASINs were sent in 15 HTTP attempts. Ten attempts were charged, three 500s were refunded and one 429 was free, which leaves 10 credits plus up to 1 for the attempt that timed out. Without the refund the same run would have cost 13. The Usage History for that minute should show 13 rows (14 if the timed-out attempt arrived), 3 of them with Credits Used 0 and Status 500. If your count differs, the CSV tells you which attempt to look for. Credit packs and plans are on the pricing page.

What the refund does not cover
- Client timeouts. The credit is taken before the data is fetched, so a request your client abandons is paid. Use a 120 second timeout, as explained in how long to wait for an answer.
- 502 and 503 retries. Every attempt is charged. Cap attempts and respect Retry-After, as in the 502 retry guide.
- 404s and endpoint-check 400s. A typo in an ASIN or a
countof 60 costs a credit; validate input before you send it. - Duplicates. Asking for the same ASIN twice costs twice. Deduplicate the input list first.
429s are not on the list because they are never charged: they mean too many requests at once on one key, or more than 600 a minute from one address. The rate limits and Retry-After guide shows how to back off. If errors pile up across many requests, the status page shows whether the API is having a bad hour.
Frequently Asked Questions
Does the SellerMagnet API refund credits for failed requests?
Yes, for one kind of failure: an HTTP 500 without a Retry-After header. The credit goes back on every endpoint, every time, with no ticket to file. 404, 502 and 503 answers stay charged.
Is a 404 refunded?
No. A 404 means the product or seller was not found on that marketplace. It is a real answer and costs 1 credit, so check ASINs and seller IDs before you send them.
Do I pay for a 429 Too Many Requests?
No. A 429 is rejected before a credit is taken, so there is nothing to refund and no row in your usage history. Wait the Retry-After seconds and send the request again.
How long does a refund take?
No time at all: the credit is back before the 500 reaches your client. The request's row in the Usage History then reads Credits Used 0 and Status 500.
Is a client timeout refunded?
No. The credit is taken before the data is fetched, so a request your client abandons is paid even though you never see the answer. Give the API 120 seconds before you give up.
Bottom line: a 500 without Retry-After is refunded automatically, every time, and everything else that got past the checks is charged per attempt. Read the status and the Retry-After header to predict the bill, keep a ledger of every attempt, and check it against the Usage History rows, where a refund reads Credits Used 0 and Status 500.