Home / Blog / Amazon ASIN Converter Batch: Convert Lists of ASINs and Barcodes

Amazon ASIN Converter Batch: Convert Lists of ASINs and Barcodes

An Amazon ASIN converter batch job with the SellerMagnet API: the converter takes one code per request, so the batch is a loop you control. Deduplicate UPC, EAN and ISBN-13 spellings first, check digits for free, then run the unique codes through a paced thread pool that resumes where it stopped. Tested Python script, real credit math.

November 07, 2026
8 min read
SellerMagnet Team
Share & Bookmark
Terminal running a Python batch converter: duplicate UPC and ISBN-13 rows are sent once, and the summary shows the credits billed for the whole column

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 the converter does with batch-style requests (checked on 6 Nov 2026).
What you sendWhat happensCredits
asin=4006381333931one barcode converted1
asin=A&asin=Bonly the first asin value is read1
asin=A,B (or a JSON array)not split: one value, at most one product back1
asins=A,B (no asin)400 Missing required parameter(s): asin0
conversion_direction=batch400 Invalid conversion_direction1

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 B plus 9 letters or digits; these go out as asin-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, so 711719577294 and 0711719577294 become 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-7 and 9780306406157 are 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+12 has lost its digits (re-export the column as text), and an ISBN-10 must be converted to ISBN-13 first.
Terminal-theme flow: a mixed code column is normalised, checked and deduplicated, then sent one code per request through a paced thread pool, with results cached and mapped back to every row
Only the unique, valid codes reach the API; every input row still gets its answer.

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.

Limits that decide batch throughput, and how the script below stays inside them.
LimitAnswerCostScript setting
Concurrent requests per keywaits up to 20 s, then 429 with Retry-After: 2free8 threads
600 a minute per client IP429, no Retry-Afterfree540 attempts a minute
15,000 an hour per client IP429, no Retry-Afterfreelower the pace above 15,000
Data briefly busy503 with Retry-After1 creditget_with_retry waits
Many codes fail after retriesoutage, not bad codes1 per attemptstop 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.

Terminal-theme table of the per-key and per-IP limits, busy and failing answers, duplicates, typos and uncovered marketplaces: what each costs and the script setting that avoids it
Rate-limit answers are free; a busy 503, failing retries, duplicates and typos are not.

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.jsonl to 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 eanList does 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.

Terminal-theme bar chart of the 19 HTTP attempts in the example run by answer: 10 OK, one each of 404, 429, 500, 502 and 503, and 4 without an answer
19 attempts for 12 unique codes: 13 billed, 1 refunded, 1 free, 4 unknown.

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 eanList and productTitle, 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, productTitle and listedSince can 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.

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