A SellerMagnet API error is free only when it fires BEFORE the credit is deducted: a missing or unknown required parameter, or an unknown marketplaceId. Every other error - the endpoint's own 400 (a bad count, category_id, ASIN or conversion_direction), a 404, and a 502 or 503 - has already spent the credit, with one exception: a 500 is refunded. Which checks exist, and in which order, differs by endpoint, which is why the same mistake is free on one call and billed on the next.
Key Takeaways
- Only the parameter gate (missing/unknown required field, unknown marketplaceId) is free; it always runs before the credit is deducted.
- amazon-product-lookup and amazon-product-offers never validate the ASIN format, so a malformed ASIN is billed like a normal request.
- amazon-search, amazon-bestsellers and amazon-deals charge for count above 50; amazon-bestsellers also charges for an unknown category_id.
- A 404 is always billed; a 500 is always refunded; 502 and 503 are always billed, even on every retry attempt.
- amazon-get-marketplaces has no gate at all - it bills 1 credit even with no parameters.
Why is a 400 free on one endpoint and billed on the next?
The API checks requests in a fixed order: the API key, then required parameters and the marketplaceId, then a per-key concurrency slot, then the credit deduction, and only after that the endpoint's own view runs. A 400 from the first stage ("Missing required parameter(s)" or "Invalid marketplaceId") never reaches the deduction, so it is free. A 400 raised inside the view - after the deduction already ran - is billed, even though the HTTP status looks identical. The table below lists what each endpoint checks at which stage, built from the status-code and billing notes in FACTS.md.
| Endpoint | Free gate 400 | Billed endpoint 400 | 404 | 5xx / 500 |
|---|---|---|---|---|
| /amazon-product-lookup | missing asin/marketplaceId, unknown marketplaceId | none - malformed ASIN is sent upstream and billed like any other request | Yes - billed | 502/503 billed, 500 refunded |
| /amazon-product-offers | missing asin/marketplaceId, unknown marketplaceId | none - malformed ASIN is sent upstream and billed like any other request | general rule applies | 502/503 billed, 500 refunded |
| /amazon-product-statistics | missing params, marketplaceId outside the covered history domains (free since 2026-09-19) | malformed ASIN ("Invalid product ASIN") | our own parse failures refund as a 500 instead | 502/503 billed, 500 refunded |
| /amazon-product-search-estimated-sells | missing params, marketplaceId outside the covered history domains | malformed ASIN | a failed estimate refunds as a 500, never a 404 | 502/503 billed, 500 refunded |
| /amazon-search | missing params/marketplaceId | count above 50, or not a whole number | no 404 path - a page with no results is a billed 200 | 502/503 billed, 500 refunded |
| /amazon-bestsellers | missing params/marketplaceId | count above 50, an unknown category_id, or count not whole | no 404 path | 502/503 billed, 500 refunded |
| /amazon-deals | missing params/marketplaceId | count above 50, or not a whole number | no 404 path - a page with no deal data is a billed retryable 5xx | 502/503 billed, 500 refunded |
| /amazon-asin-converter | missing params, marketplaceId outside the covered history domains | a bad conversion_direction, or a malformed ASIN/EAN | unknown product - billed | 502/503 billed, 500 refunded |
| /amazon-get-marketplaces | none at all - billed 1 credit even with no parameters | none | no 404 path | general rule applies |
| /amazon-seller-review | missing sellerId/marketplaceId (format itself is NOT validated) | none - a malformed sellerId is billed like any other request | Amazon's own seller ID, or a confirmed-gone seller - billed | a blocked fetch is a billed 502 |
How do I classify an error before I decide to retry?
A small lookup table turns the rows above into code: given an endpoint, a status and the response message, decide whether a credit was spent. The function never guesses from the status alone - a 400's message, and the endpoint it came from, decide which branch applies.
billing.py - classify a response by endpoint, status and message
"""Classify a SellerMagnet API response as free or billed, per endpoint, before deciding to retry.
Rules taken from scripts/blog_studio/FACTS.md "Status codes and billing" (checked 2026-09-19 to
2026-10-13). A 400 is the one status that means two different things: a GATE 400, raised before the
credit is deducted, and an ENDPOINT 400, raised inside the view after the credit is already gone.
Which one you got depends on the endpoint and the message, not the status code alone.
"""
# Endpoints whose own validation (AFTER the credit is deducted) can still answer a 400.
# Every other endpoint only ever gates on a missing/blank required parameter or marketplaceId.
ENDPOINT_400 = {
"amazon-search": "count above 50, or not a whole number",
"amazon-bestsellers": "count above 50 (or not whole), or an unknown category_id",
"amazon-deals": "count above 50, or not a whole number",
"amazon-product-statistics": "a malformed ASIN",
"amazon-product-search-estimated-sells": "a malformed ASIN",
"amazon-asin-converter": "a bad conversion_direction, or a malformed ASIN/EAN",
}
# Endpoints that never answer 404 (a miss shows up as something else, or cannot happen).
NO_404 = frozenset({"amazon-get-marketplaces", "amazon-search", "amazon-bestsellers", "amazon-deals"})
RETRYABLE = {429, 500, 502, 503}
def classify(endpoint: str, status: int, message: str = "") -> tuple[bool, str]:
"""Return (charged, reason). charged=False never spent a credit; a refunded 500 is False too."""
if status == 200:
return True, "success"
if status == 400:
lowered = message.lower()
if "missing required parameter" in lowered or "invalid marketplaceid" in lowered:
return False, "gate 400: required parameter or marketplaceId, checked before billing"
if "not available for the requested marketplace" in lowered:
return False, "gate 400: marketplaceId outside the covered history domains, checked before billing"
if endpoint in ENDPOINT_400:
return True, f"endpoint 400 ({ENDPOINT_400[endpoint]}): raised after the credit is deducted"
return False, "gate 400: no endpoint-level 400 is documented here, treat as a pre-billing gate"
if status == 401:
return False, "key missing/unknown, zero credits, or a transient key-check error"
if status == 403:
return False, "credits hit zero in the race between the balance check and the deduction"
if status == 404:
if endpoint in NO_404:
return True, f"unexpected: {endpoint} has no 404 path in FACTS.md - re-check it"
return True, "product or seller not found - a real answer, not an outage"
if status == 429:
return False, "per-key concurrency cap (queued up to 20s) or the per-IP request budget"
if status == 500:
return False, "internal error - refunded since the 2026-09-19 deploy"
if status == 502:
return True, "upstream fetch error"
if status == 503:
return True, "temporarily unavailable - per-marketplace circuit breaker"
return True, "unknown status: assume charged until FACTS.md says otherwise"
def should_retry(status: int) -> bool:
return status in RETRYABLE
credit_ledger.py - call several endpoints and total what a run really spends
"""Call several SellerMagnet endpoints, classify each outcome with billing.classify, and total the
credits a run actually spends - including a billed 400, a billed 404 and a billed retry."""
from billing import classify
from retry import get_with_retry
from sellermagnet import ApiError, new_session
CALLS = [
("amazon-product-lookup", {"asin": "B0CL61F39H", "marketplaceId": "ATVPDKIKX0DER"}),
("amazon-product-lookup", {"asin": "B000000000", "marketplaceId": "ATVPDKIKX0DER"}), # 404
("amazon-bestsellers", {"category_id": "not-a-category", "marketplaceId": "ATVPDKIKX0DER", "count": 30}),
("amazon-product-offers", {"asin": "B0CL61F39H", "marketplaceId": "A1PA6795UKMFR9"}), # 503, then OK
("amazon-get-marketplaces", {}),
("amazon-product-lookup", {"marketplaceId": "ATVPDKIKX0DER"}), # missing asin
("amazon-product-statistics", {"asin": "B0CL61F39H", "marketplaceId": "A2NODRKZP88ZB9"}), # SE: free gate 400
]
session = new_session()
ledger = []
spent = 0
for endpoint, params in CALLS:
try:
get_with_retry(session, endpoint, **params)
status, message = 200, "success"
except ApiError as err:
status, message = err.status, str(err)
charged, reason = classify(endpoint, status, message)
spent += charged
ledger.append((endpoint, status, charged, reason))
for endpoint, status, charged, reason in ledger:
print(f"{endpoint:<24} {status:>3} {'billed' if charged else 'free':>6} {reason}")
print(f"\ncredits spent: {spent} of {len(ledger)} calls")

Output of credit_ledger.py against a fixed set of fixtures (illustrative data)
amazon-product-lookup 200 billed success
amazon-product-lookup 404 billed product or seller not found - a real answer, not an outage
amazon-bestsellers 400 billed endpoint 400 (count above 50 (or not whole), or an unknown category_id): raised after the credit is deducted
amazon-product-offers 200 billed success
amazon-get-marketplaces 200 billed success
amazon-product-lookup 400 free gate 400: required parameter or marketplaceId, checked before billing
amazon-product-statistics 400 free gate 400: marketplaceId outside the covered history domains, checked before billing
credits spent: 5 of 7 calls
Validate client-side before the call, not after
Checking count <= 50, a known category_id and the ASIN shape before sending saves the credit entirely. Catching the same mistake from the response is one credit spent to learn what a local check would have caught for free.
Which errors are free on every endpoint?
Four statuses do not depend on the endpoint: 401 (missing/unknown key, zero credits, or a transient key-check error), 403 (credits hit zero in the race between the balance check and the deduction), and 429 (the per-key concurrency cap, which waits up to 20 seconds before answering, or the per-IP request budget). A 500 is the one mid-flight status that reverses itself: the credit is deducted, the internal error happens, and the deduction is undone - so it reads as free in the ledger even though a charge briefly existed. The retry and backoff guide covers honouring Retry-After and capping attempts in more depth.

Build the full matrix into client code once, and every endpoint you add later only needs one new row: which parameters gate the request, and what the endpoint's own view still checks after the credit is gone. The marketplace coverage guide has the per-endpoint marketplace gates that sit in front of this billing check, and the Python quickstart has the full sellermagnet.py and retry.py this post imports.
Frequently Asked Questions
Is a 400 always free on the SellerMagnet API?
No. A gate 400 (missing parameter, unknown marketplaceId) is free; an endpoint's own 400 (count above 50, a bad category_id, ASIN or conversion_direction) is billed, because the credit is deducted first.
Does a malformed ASIN ever cost nothing?
Not on lookup or offers: neither validates the ASIN format, so a typo is billed and simply fails further upstream, usually as a 404.
Is a 404 ever refunded?
No. A 404 means the product or seller does not exist on that marketplace, which is a real answer, not an outage, so the credit stays charged.
Why does a 500 read as free in a credit ledger?
A 500 deducts the credit, then refunds it once the internal error is detected, so the net effect across the request is zero credits spent.
Does amazon-get-marketplaces ever return a free error?
No. It has no parameter gate at all, so every call - even with no parameters - bills 1 credit.
Bottom line: free only means "before the credit is deducted" - a missing field or an unknown marketplaceId. Everything the endpoint itself checks afterwards, from a bad category_id to a malformed ASIN on statistics, is billed the moment it runs, and a 404 or 502/503 is billed on every attempt. Only a 500 gives the credit back.