An Amazon ASIN converter batch job on the SellerMagnet API is a loop, not one big request: /api/amazon-asin-converter converts exactly one ASIN or barcode per call and has no list parameter. So the batch work happens on your side. Clean the column, merge UPC, EAN and ISBN-13 spellings of the same number, drop malformed codes before they cost a credit, then send the unique codes through a small, paced thread pool that picks up where it stopped.
Key Takeaways
- The converter takes one code per request; a comma list or a repeated asin parameter is not split into lookups and still returns at most one product.
- Every request that gets past the checks costs 1 credit, so a duplicate row costs twice and a malformed code still costs a credit if you send it.
- A UPC-A and its 13-digit EAN spelling, or an ISBN-13 with and without dashes, are one code: deduplicate on the 13-digit form.
- Eight threads paced at 540 requests a minute stay inside the per-key and per-IP limits, and the quickstart's retry helper absorbs the occasional 429 or 503.
- A JSON Lines cache makes the batch resumable: a second run only sends the codes that failed.
Amazon ASIN converter batch: can one request take many codes?
No. The endpoint reads one value from the asin parameter (the name is historical: it carries an ASIN or a barcode) together with marketplaceId and conversion_direction, which is either asin-to-ean or ean-to-asin. We ran the five request shapes people try when they look for a batch mode against the endpoint's own code. None of them converts more than one code, and the ones that reach the converter are billed like any single request.
| What you send | What happens | Credits |
|---|---|---|
| asin=4006381333931 | one barcode converted | 1 |
| asin=A&asin=B | only the first asin value is read | 1 |
| asin=A,B (or a JSON array) | not split: one value, at most one product back | 1 |
| asins=A,B (no asin) | 400 Missing required parameter(s): asin | 0 |
| conversion_direction=batch | 400 Invalid conversion_direction | 1 |
The comma list is the expensive trap: it can come back as HTTP 200 with a single product, so a script that does not compare the answer with its input will think the whole list converted. The ASIN converter page describes both directions; for a single barcode the UPC and EAN to ASIN guide explains the check digit and the eanList check, and books are covered in the ISBN to ASIN guide.
How do I clean and deduplicate a mixed code column?
Supplier lists and inventory exports mix identifiers in one column, and the same product often appears twice under different spellings. Each spelling you send is a separate paid request, so normalise first and deduplicate on the result:
- ASINs: trim, upper-case and match
Bplus 9 letters or digits; these go out asasin-to-ean. - UPC-A (12 digits): prefix a
0. A UPC-A is the same GS1 number as the 13-digit EAN with a leading zero, so711719577294and0711719577294become one request. - EAN-13 and ISBN-13: strip spaces and dashes. An ISBN-13 is an EAN-13 with the 978 or 979 prefix, so
978-0-306-40615-7and9780306406157are one request too. - Check digit: test the GS1 mod-10 digit locally. A typo caught here is free; sent to the API, it costs a credit like any other request.
- Skip what cannot be fixed: Excel's
4.00638E+12has lost its digits (re-export the column as text), and an ISBN-10 must be converted to ISBN-13 first.

How many conversions can run in parallel?
Two limits shape the pace, and neither costs a credit when you hit it. Each API key may run a limited number of requests at once: a request beyond that ceiling waits up to 20 seconds for a free slot, and only then gets a 429 with Retry-After: 2, whose message names the current ceiling. Separately, one client IP may send 600 requests a minute and 15,000 an hour (as of November 2026), and that 429 carries no Retry-After at all, so your client backs off on its own. A third answer is billed: a 503 Service is busy with a Retry-After header, when the conversion data is briefly unavailable.
| Limit | Answer | Cost | Script setting |
|---|---|---|---|
| Concurrent requests per key | waits up to 20 s, then 429 with Retry-After: 2 | free | 8 threads |
| 600 a minute per client IP | 429, no Retry-After | free | 540 attempts a minute |
| 15,000 an hour per client IP | 429, no Retry-After | free | lower the pace above 15,000 |
| Data briefly busy | 503 with Retry-After | 1 credit | get_with_retry waits |
| Many codes fail after retries | outage, not bad codes | 1 per attempt | stop after 20 codes |
Eight threads stay far below the per-key ceiling, which your other jobs on the same key share. The pace matters more than the thread count: at 540 attempts a minute, retries included, 10,000 codes take more than 18 minutes from one machine, however many threads you add. For more than 15,000 codes the hourly limit takes over; set PER_MINUTE = 240 (14,400 an hour) for those runs. The rate limits and Retry-After guide covers each status in detail.

The batch converter script in Python
The script reads codes.csv (one column named code), normalises and deduplicates it, and sends each unique code once through get_with_retry on amazon.com. Each thread has its own session, every HTTP attempt waits for its slot in a shared pacer, and each attempt is counted by what it cost, because get_with_retry hides its retries but the bill does not. Final answers go to converted.jsonl as they arrive. A 401 or 403 stops new requests at once, and so do 20 codes that still fail after their retries. It uses sellermagnet.py (ApiError, new_session) and retry.py (get_with_retry) from the Python quickstart.
convert_many.py: a mixed code column in, one request per unique code, a resumable cache
"""convert_many.py: convert a mixed column of ASINs, EANs, UPCs and ISBN-13s through /amazon-asin-converter.
The converter takes ONE code per request, so "batch" here means: clean and deduplicate the list first (a call
costs a credit, a duplicate costs it twice), then run the unique codes through a small thread pool that stays
far below the per-key concurrency ceiling and paces itself under the per-IP limit. Results go to a JSON Lines
cache, so a second run only sends what is still missing, and every input row gets its answer in the output CSV.
"""
import csv
import json
import os
import re
import sys
import threading
import time
from collections import Counter
from concurrent.futures import ThreadPoolExecutor
import requests
from retry import get_with_retry
from sellermagnet import ApiError, new_session
MARKETPLACE = "ATVPDKIKX0DER" # amazon.com
COVERED = {"ATVPDKIKX0DER", "A2EUQ1WTGCTBG2", "A1AM78C64UM0Y8", "A2Q3Y263D00KWC", "A1F83G8C2ARO7P",
"A1PA6795UKMFR9", "A13V1IB3VIYZZH", "APJ6JRA9NG5V4", "A1RKKUPIHCS9HS", "A21TJRUUN4KGV",
"A1VC38T7YXB528"} # the 11 marketplaces the converter answers; any other one is a free 400
WORKERS = 8 # well below the per-key ceiling, which your other jobs share
PER_MINUTE = 540 # every HTTP attempt, retries included; the per-IP limit is 600 a minute
MAX_FAILED = 20 # codes that still fail after get_with_retry: stop, something bigger is wrong
IN_FILE, CACHE, OUT_FILE = "codes.csv", "converted.jsonl", "converted.csv"
ASIN_RE = re.compile(r"^B[0-9A-Z]{9}$")
def gs1_ok(code: str) -> bool:
"""GS1 mod-10 check digit, the same rule for EAN-13, UPC-A (as 13 digits) and ISBN-13."""
total = sum(int(d) * (3 if i % 2 else 1) for i, d in enumerate(code[:-1]))
return (10 - total % 10) % 10 == int(code[-1])
def normalise(raw: str):
"""Return ((direction, code), None) for a code worth paying for, or (None, reason) for one that is not."""
s = raw.strip().upper()
if not s:
return None, "empty cell"
if re.fullmatch(r"\d(\.\d+)?E\+\d+", s):
return None, "Excel scientific notation, digits lost"
if ASIN_RE.match(s):
return ("asin-to-ean", s), None
digits = re.sub(r"[\s-]", "", s)
if not digits.isdigit():
return None, "not an ASIN or a barcode"
if len(digits) == 10:
return None, "ISBN-10: convert it to ISBN-13 first"
if len(digits) == 12:
digits = "0" + digits # a UPC-A is an EAN-13 with a leading 0, so both spellings dedupe to one call
if len(digits) != 13:
return None, f"{len(digits)} digits: not a UPC-A, EAN-13 or ISBN-13"
if not gs1_ok(digits):
return None, "bad check digit (typo?)"
return ("ean-to-asin", digits), None
class Pacer:
"""Spaces HTTP attempts across all threads, so a large list never runs into the per-IP limit."""
def __init__(self, per_minute: int):
self.gap, self.next, self.lock = 60.0 / per_minute, 0.0, threading.Lock()
def wait(self):
with self.lock:
now = time.monotonic()
slot = max(now, self.next)
self.next = slot + self.gap
time.sleep(slot - now)
PACER, LOCK, STOP = Pacer(PER_MINUTE), threading.Lock(), threading.Event()
ATTEMPTS, COST, FAILED = Counter(), Counter(), Counter()
FREE_400 = ("Missing required", "Invalid marketplaceId", "not available for the requested marketplace")
_local = threading.local()
def _count(resp):
"""Record what one HTTP attempt cost: get_with_retry hides its retries, the bill does not."""
try:
message = str((resp.json() or {}).get("message", ""))
except ValueError:
message = ""
status = resp.status_code
if status in (401, 403, 429) or (status == 400 and any(m in message for m in FREE_400)):
kind = "free"
elif status == 500 and "Retry-After" not in resp.headers:
kind = "refunded"
else:
kind = "billed" # 200, 404, 502, 503 and the converter's own 400s
with LOCK:
ATTEMPTS[status] += 1
COST[kind] += 1
def session() -> requests.Session:
"""One session per worker thread, its get() paced and metered."""
if not hasattr(_local, "s"):
s = new_session()
plain_get = s.get
def get(url, **kw):
PACER.wait()
try:
resp = plain_get(url, **kw)
except (requests.ConnectionError, requests.Timeout):
with LOCK:
COST["unanswered"] += 1 # may or may not have reached the API and been charged
raise
_count(resp)
return resp
s.get = get
_local.s = s
return _local.s
def convert(key):
"""One code, one request. A record with final=False is reported but not cached, so the next run retries it."""
rec = _convert(key)
if not rec["final"] and rec["status"].startswith("failed"):
with LOCK:
FAILED["codes"] += 1
if FAILED["codes"] >= MAX_FAILED:
STOP.set() # many codes failing after retries: an outage, not bad codes; stop paying for it
return rec
def _convert(key):
direction, code = key
if STOP.is_set():
return {"direction": direction, "code": code, "status": "not sent (run stopped)", "final": False}
rec = {"direction": direction, "code": code, "final": True}
try:
data = get_with_retry(session(), "amazon-asin-converter", asin=code, marketplaceId=MARKETPLACE,
conversion_direction=direction)
except ApiError as err:
if err.status in (401, 403):
STOP.set() # bad key or no credits: every further request would fail the same way
if err.status == 404:
return {**rec, "status": "not found (billed)"}
if err.status == 400 and "Invalid product" in str(err):
return {**rec, "status": "rejected by the API (billed)"}
return {**rec, "status": f"failed: {err}", "final": False}
except (requests.ConnectionError, requests.Timeout) as exc:
return {**rec, "status": f"failed: {type(exc).__name__}", "final": False}
eans = data.get("eanList") or []
asin = data.get("asin") or ""
if not asin:
note = "check: no ASIN in the answer"
elif direction == "ean-to-asin":
note = "ok" if code in eans else ("check: eanList empty" if not eans else "check: code not in eanList")
else:
note = ("ok" if eans else "ok, no barcodes on record") if asin == code else f"check: answered for {asin}"
return {**rec, "status": note, "asin": asin, "eans": eans, "title": data.get("productTitle") or ""}
def load_cache() -> dict:
done = {}
if os.path.exists(CACHE):
with open(CACHE) as f:
for line in f:
r = json.loads(line)
done[(r["direction"], r["code"])] = r
return done
def main():
if MARKETPLACE not in COVERED:
sys.exit(f"{MARKETPLACE}: the converter does not cover this marketplace (free 400 on every code)")
with open(IN_FILE, newline="") as f:
reader = csv.DictReader(f)
if "code" not in (reader.fieldnames or []):
sys.exit(f"{IN_FILE}: needs a column named 'code', found {reader.fieldnames}")
rows = [r["code"] or "" for r in reader]
keys, first_row = [], {}
for n, raw in enumerate(rows, start=1):
key, reason = normalise(raw)
keys.append((key, reason))
if key and key not in first_row:
first_row[key] = n
done = load_cache()
todo = [k for k in first_row if k not in done]
with ThreadPoolExecutor(max_workers=WORKERS) as pool, open(CACHE, "a") as cache:
for key, rec in zip(todo, pool.map(convert, todo)):
done[key] = rec # failures are reported in this run's CSV but not cached: the next run resends them
if rec["final"]:
cache.write(json.dumps(rec) + "\n")
cache.flush() # written as results arrive, so a crash keeps what was already paid for
if STOP.is_set():
print(f"run stopped early (codes failed: {FAILED['codes']}): fix the cause, then run again to resume")
with open(OUT_FILE, "w", newline="") as f:
out = csv.writer(f)
out.writerow(["row", "input", "sent", "direction", "asin", "eans", "title", "status"])
sent_now = set(todo)
for n, (raw, (key, reason)) in enumerate(zip(rows, keys), start=1):
if key is None:
out.writerow([n, raw, "", "", "", "", "", f"skipped, not sent: {reason}"])
print(f"{n:>3} {raw.strip():<20} {'':<14} {'':<11} skipped: {reason}")
continue
r = done[key]
status = r["status"] if first_row[key] == n else f"same code as row {first_row[key]}, not sent again"
out.writerow([n, raw, key[1], key[0], r.get("asin", ""), " ".join(r.get("eans", [])),
r.get("title", ""), status])
if key in sent_now: # rows answered from the cache stay quiet; they are all in the CSV
print(f"{n:>3} {raw.strip():<20} {key[1]:<14} {r.get('asin', ''):<11} {status}")
skipped = sum(1 for key, _ in keys if key is None)
print(f"rows {len(rows)}, skipped before paying {skipped}, unique codes {len(first_row)}, "
f"from cache {len(first_row) - len(todo)}, sent {len(todo)}")
print(f"HTTP attempts {sum(ATTEMPTS.values()) + COST['unanswered']}: {dict(sorted(ATTEMPTS.items()))}, "
f"no answer {COST['unanswered']}")
print(f"credits billed {COST['billed']}, refunded {COST['refunded']}, free {COST['free']}, "
f"plus up to {COST['unanswered']} for attempts that got no answer")
if __name__ == "__main__":
main()
Output of two runs (illustrative data: a billed 404, a 502, a 429, a 503, a refunded 500 and a code that timed out on every attempt, then answered the next morning)
$ python3 convert_many.py
1 4006381333931 4006381333931 B0D5MN4X2Q ok
2 711719577294 0711719577294 B0C9T3KX7W ok
3 0711719577294 0711719577294 B0C9T3KX7W same code as row 2, not sent again
4 978-0-306-40615-7 9780306406157 0306406152 ok
5 9780306406157 9780306406157 0306406152 same code as row 4, not sent again
6 B0CL61F39H B0CL61F39H B0CL61F39H ok
7 b0cl61f39h B0CL61F39H B0CL61F39H same code as row 6, not sent again
8 4.00638E+12 skipped: Excel scientific notation, digits lost
9 4006381333932 skipped: bad check digit (typo?)
10 0198526636 skipped: ISBN-10: convert it to ISBN-13 first
11 5901234123457 5901234123457 not found (billed)
12 4012345678901 4012345678901 B0BRV2K8LM ok
13 4005990001231 4005990001231 B0CX7Y1P3D ok
14 8712345000011 8712345000011 B0D2H6Q9ZT ok
15 7612345600017 7612345600017 B0BW4J8N1C ok
16 5012345678900 5012345678900 failed: Timeout
17 B0DHX1R2K7 B0DHX1R2K7 B0DHX1R2K7 ok, no barcodes on record
18 3006500001018 3006500001018 B0C1F5R8VA check: code not in eanList
rows 18, skipped before paying 3, unique codes 12, from cache 0, sent 12
HTTP attempts 19: {200: 10, 404: 1, 429: 1, 500: 1, 502: 1, 503: 1}, no answer 4
credits billed 13, refunded 1, free 1, plus up to 4 for attempts that got no answer
$ python3 convert_many.py # second run, the next morning
8 4.00638E+12 skipped: Excel scientific notation, digits lost
9 4006381333932 skipped: bad check digit (typo?)
10 0198526636 skipped: ISBN-10: convert it to ISBN-13 first
16 5012345678900 5012345678900 B0CK2D7W4S ok
rows 18, skipped before paying 3, unique codes 12, from cache 11, sent 1
HTTP attempts 1: {200: 1}, no answer 0
credits billed 1, refunded 0, free 0, plus up to 0 for attempts that got no answer
converted.csv after the second run: one row per input row
row,input,sent,direction,asin,eans,title,status
1,4006381333931,4006381333931,ean-to-asin,B0D5MN4X2Q,4006381333931,"Highlighter pen set, 4 colours",ok
2,711719577294,0711719577294,ean-to-asin,B0C9T3KX7W,0711719577294,"Wireless game controller, black",ok
3,0711719577294,0711719577294,ean-to-asin,B0C9T3KX7W,0711719577294,"Wireless game controller, black","same code as row 2, not sent again"
4,978-0-306-40615-7,9780306406157,ean-to-asin,0306406152,9780306406157,"Example book title, paperback",ok
5,9780306406157,9780306406157,ean-to-asin,0306406152,9780306406157,"Example book title, paperback","same code as row 4, not sent again"
6,B0CL61F39H,B0CL61F39H,asin-to-ean,B0CL61F39H,0194253397175,"Smart speaker with display, charcoal",ok
7, b0cl61f39h ,B0CL61F39H,asin-to-ean,B0CL61F39H,0194253397175,"Smart speaker with display, charcoal","same code as row 6, not sent again"
8,4.00638E+12,,,,,,"skipped, not sent: Excel scientific notation, digits lost"
9,4006381333932,,,,,,"skipped, not sent: bad check digit (typo?)"
10,0198526636,,,,,,"skipped, not sent: ISBN-10: convert it to ISBN-13 first"
11,5901234123457,5901234123457,ean-to-asin,,,,not found (billed)
12,4012345678901,4012345678901,ean-to-asin,B0BRV2K8LM,4012345678901,"Stainless steel water bottle, 750 ml",ok
13,4005990001231,4005990001231,ean-to-asin,B0CX7Y1P3D,4005990001231,"Bamboo cutting board, large",ok
14,8712345000011,8712345000011,ean-to-asin,B0D2H6Q9ZT,8712345000011,LED desk lamp with USB port,ok
15,7612345600017,7612345600017,ean-to-asin,B0BW4J8N1C,7612345600017,"Ceramic coffee mug, 350 ml",ok
16,5012345678900,5012345678900,ean-to-asin,B0CK2D7W4S,5012345678900,"Yoga mat, non-slip, 6 mm",ok
17,B0DHX1R2K7,B0DHX1R2K7,asin-to-ean,B0DHX1R2K7,,,"ok, no barcodes on record"
18,3006500001018,3006500001018,ean-to-asin,B0C1F5R8VA,3006500001025,"Hand cream, 75 ml",check: code not in eanList
One converter answer, as the script receives it (illustrative values)
{
"success": true,
"data": {
"asin": "B0D5MN4X2Q",
"eanList": [
"4006381333931"
],
"productTitle": "Highlighter pen set, 4 colours",
"listedSince": "2023-10-12 00:00:00"
}
}
- Rows 3, 5 and 7 are spellings of rows 2, 4 and 6. They get the same answer in the CSV and cost nothing.
- Rows 8 to 10 never reach the API: Excel notation, a wrong check digit and an ISBN-10.
- Row 11 is a valid barcode that is not listed on amazon.com: a billed 404, cached so the next run does not pay for it again. Delete its line in
converted.jsonlto ask again later. - Row 16 timed out on all 4 attempts. It is reported but not cached, so the second run sent it again and got the answer.
- Rows 17 and 18 came back with something to check: an ASIN with no barcodes on record, and an answer whose
eanListdoes not contain the barcode that was sent.
The credit math of the example run
Eighteen rows went in. Three were skipped before paying and three were duplicates, so 12 unique codes were sent in 19 HTTP attempts. Thirteen attempts were billed: ten answers, the 404, the 502 before its retry and the 503 before its retry. The 500 was refunded and the 429 was free. The four attempts that timed out are the unknown: each one that reached the API cost a credit, so the run cost 13 to 17 credits. The second run cost 1. Sending every row as it came would have cost up to 6 credits more: three duplicates plus three codes the API would reject or not find. Plans and credit packs are on the pricing page, and the refund rules explain why the 500 cost nothing.

What a batch conversion cannot tell you
- Only the first match. A barcode that sits on several listings returns one ASIN. Treat it as a candidate and check
eanListandproductTitle, as the script does. - 404 means not found here. It is an answer for that marketplace, not proof that Amazon has no listing; a product sold only in Germany needs the amazon.de marketplace ID.
- 11 marketplaces. The converter covers amazon.com, .ca, .com.mx, .com.br, .co.uk, .de, .fr, .it, .es, .in and .co.jp. The script refuses to start on any other one, because every code would get the same free 400.
- Empty fields.
eanList,productTitleandlistedSincecan be null on a 200, so never read a missing barcode as a failed conversion.
Once you have the ASINs, the bulk ASIN lookup guide shows the same pool-and-cache pattern for fetching product data for each of them.
Frequently Asked Questions
Does the SellerMagnet ASIN converter have a batch endpoint?
No. /api/amazon-asin-converter converts one ASIN or barcode per request and has no list parameter. A batch is a loop on your side: deduplicate the codes, then send one request per unique code.
What happens if I send several codes in the asin parameter?
They are not split into separate lookups. A repeated asin parameter is read once, and a comma list is treated as one value, so you get at most one product back and still pay one credit.
How many credits does a batch conversion cost?
One per request that gets past the checks, retries included. Duplicates and malformed codes you remove locally cost nothing, a 429 is free and a 500 without Retry-After is refunded.
How fast can I convert 10,000 codes?
Paced at 540 requests a minute, 10,000 codes take more than 18 minutes from one machine. Retries count against the same budget, and above 15,000 codes the hourly limit sets the pace.
Can I convert ISBN-10s in the same batch?
Convert each ISBN-10 to its ISBN-13 first; the ISBN to ASIN guide shows how. The script below reports ISBN-10s as skipped instead of paying for a request that will not match.
Bottom line: there is no batch parameter, and you do not need one. Normalise and deduplicate the column so each product number is paid for once, pace every attempt under the per-IP limit, cache final answers so a rerun only sends what failed, and check each answer against the code you sent.