Home / Blog / Amazon API Refund on Error: Which Failed Calls Get Credits Back

Amazon API Refund on Error: Which Failed Calls Get Credits Back

How the Amazon API refund on error works at SellerMagnet: a 500 without Retry-After gives the credit back automatically, while 404, 502 and 503 stay charged and 429 is never charged. See how to confirm a refund in your Usage History and track every retry's cost with a tested Python script.

November 06, 2026
8 min read
SellerMagnet Team
Share & Bookmark
Terminal running a Python script that records each SellerMagnet API attempt with its status and net credits: a refunded 500 costs 0, a 404 and every 502 retry cost 1

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.

Net credits per answer, on every endpoint (1 credit per request).
AnswerCredit takenRefundedNet costRetried by get_with_retry
200 successyesno1no
404 not foundyesno1no
500 without Retry-Afteryesyes, at once0once
500 with Retry-Afteryesno1once
502 retryable erroryesno1 per attemptup to 4 attempts
503 with Retry-Afteryesno1 per attemptup to 4 attempts
429 too many requestsnonothing to refund0up to 4 attempts
401 / 403 key or balancenonothing to refund0no
400 from a parameter checknonothing to refund0no
400 from an endpoint checkyesno1no
no answer (client timeout)yes, if it arrivedno0 or 1up 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.

Terminal-theme table: which SellerMagnet API answers take a credit, which are refunded, and the net cost of each
Only one charged answer is corrected afterwards: the 500 without Retry-After.

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=100 to 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_retry stops 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.

Terminal-theme sequence: a 500 without Retry-After is refunded and the retry is charged, a 404 is charged and not retried, a 429 is never charged
Four answers from the example run and what each one left on the bill.

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 count of 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.

Related Articles

Continue exploring topics related to this article.

Back to Blog

Ready to Extract Amazon Data at Scale?

Start building with SellerMagnet API today. Real-time product data, competitive pricing, and review analytics at your fingertips.

150 free API credits • No credit card required • Cancel anytime