Home / Blog / Amazon Product Data in Airtable: Sync Title, BSR and Price per ASIN

Amazon Product Data in Airtable: Sync Title, BSR and Price per ASIN

Pull title, Best Sellers Rank and price for a list of ASINs from the SellerMagnet API and upsert one row per ASIN into an Airtable base - batched up to 10 records per call, throttled to stay under Airtable's 5-requests-per-second-per-base limit, and keyed on the ASIN so re-running the sync updates existing rows instead of duplicating them.

October 18, 2026
4 min read
SellerMagnet Team
Share & Bookmark
Code editor syncing Amazon product data into an Airtable base via two APIs

To sync Amazon product data into Airtable, look up each ASIN on SellerMagnet's /amazon-product-lookup endpoint for title, price and Best Sellers Rank, then batch-upsert the results into an Airtable table with performUpsert keyed on the ASIN - up to 10 records per call, spaced to stay under Airtable's 5-requests-per-second-per-base rate limit. Keying on the ASIN means re-running the same sync updates existing rows instead of creating duplicates.

Key Takeaways

  • Airtable's Web API allows 5 requests per second per base (50/s per personal access token); exceeding it returns 429 and a 30-second wait before the next request succeeds.
  • Batch writes up to 10 records per create/update call - at 5 requests/second that's up to 50 records written per second, not one API call per row.
  • performUpsert with fieldsToMergeOn finds-or-creates in one call, keyed on a field you pick (the ASIN); no separate read-then-decide step is needed.
  • A SellerMagnet 404 is a billed, real answer - report it, don't write an empty or placeholder Airtable row for it.
  • Free and Team Airtable plans also cap total monthly calls (1,000 and 100,000); Business and Enterprise have no monthly cap but keep the same per-second limit.

What are Airtable's actual rate and batch limits?

Airtable's own documentation (support.airtable.com, checked 2026-10-17) states a flat 5 requests per second per base across every pricing tier, plus a separate 50 requests per second ceiling per personal access token across all the bases it can reach. A 429 means the limit was hit and the next request will not succeed for 30 seconds - Airtable's own troubleshooting guide recommends exponential backoff with jitter, not a fixed short retry. Monthly call caps are separate and plan-specific: the Free plan allows 1,000 calls a month with a 30-day grace period after that, the Team plan allows 100,000 before throttling to 2 requests/second, and Business/Enterprise have no monthly cap at all.

Airtable's documented rate and call limits (support.airtable.com, checked 2026-10-17).
LimitValueBehaviour when hit
Per base5 requests/second429, wait 30 s before the next request succeeds
Per personal access token50 requests/secondacross every base that token can reach
Free plan1,000 calls/month30-day grace period, then blocked until the month resets
Team plan100,000 calls/monththrottled to 2 requests/second after the limit, not blocked
Business / Enterpriseno monthly call capstill subject to the 5 req/s per-base limit

How do I batch the sync without tripping the rate limit?

The script below looks up every ASIN on SellerMagnet first (via get_with_retry from the Python quickstart, so a 502/503 from SellerMagnet doesn't abort the batch), then slices the successful rows into groups of 10 and sends each group as one performUpsert call, spaced just over 200 ms apart to stay under 5 requests/second with margin. An Airtable 429 triggers a roughly 30-second wait and a retry of that same batch, matching Airtable's documented recovery time.

airtable_sync.py - look up on SellerMagnet, batch-upsert into Airtable

"""airtable_sync.py - pull title/BSR/price for a list of ASINs from SellerMagnet
and upsert them into an Airtable table, one row per ASIN.

Respects Airtable's documented limits (support.airtable.com/articles/7735693959
and 2111090641, retrieved 2026-10-17): 5 requests/second per base (50/s per
personal access token), batched create/update up to 10 records per request, and
a 429 means "wait 30 seconds before the next request succeeds" - exponential
backoff with jitter on 429, not a fixed sleep. performUpsert lets one PATCH
both create and update by a key field (here: ASIN) in a single batched call.
"""
from __future__ import annotations

import os
import random
import time

import requests

from retry import get_with_retry
from sellermagnet import new_session

AIRTABLE_API = "https://api.airtable.com/v0"
AIRTABLE_BATCH = 10          # Airtable's max records per create/update request
AIRTABLE_MIN_INTERVAL = 0.21  # keep requests under the 5 req/s per-base cap with margin
MARKETPLACE = "ATVPDKIKX0DER"  # amazon.com


def _airtable_session() -> requests.Session:
    s = requests.Session()
    s.headers["Authorization"] = f"Bearer {os.environ['AIRTABLE_TOKEN']}"
    s.headers["Content-Type"] = "application/json"
    return s


def _airtable_upsert_batch(session: requests.Session, base_id: str, table: str,
                            records: list[dict], attempts: int = 4) -> dict:
    """performUpsert on one batch of <=10 records, keyed by the Asin field.
    429 means wait ~30s and retry with backoff; other 4xx are not retried."""
    url = f"{AIRTABLE_API}/{base_id}/{table}"
    body = {"performUpsert": {"fieldsToMergeOn": ["Asin"]},
            "records": [{"fields": r} for r in records]}
    for attempt in range(1, attempts + 1):
        resp = session.patch(url, json=body, timeout=30)
        if resp.status_code == 200:
            return resp.json()
        if resp.status_code == 429 and attempt < attempts:
            time.sleep(30 + random.uniform(0, 2))  # Airtable: wait 30s after a 429
            continue
        resp.raise_for_status()
    raise RuntimeError(f"Airtable upsert failed after {attempts} attempts")


def sync_asins_to_airtable(sm_session, airtable_session, base_id: str, table: str,
                            asins: list[str]) -> dict:
    """Look up every ASIN on SellerMagnet, then upsert rows into Airtable in
    batches of 10, spaced to stay under 5 requests/second per base."""
    rows, failed = [], []
    for asin in asins:
        try:
            data = sm_lookup(sm_session, asin)
            rows.append({"Asin": data["asin"], "Title": data["title"], "Price": data["price"],
                         "Currency": data["currency"], "Bsr": data["rank"]})
        except Exception as exc:  # noqa: BLE001 - report per-row, never stop the batch
            failed.append({"asin": asin, "error": str(exc)})

    written = 0
    last_call = 0.0
    for i in range(0, len(rows), AIRTABLE_BATCH):
        wait = AIRTABLE_MIN_INTERVAL - (time.time() - last_call)
        if wait > 0:
            time.sleep(wait)
        batch = rows[i:i + AIRTABLE_BATCH]
        _airtable_upsert_batch(airtable_session, base_id, table, batch)
        last_call = time.time()
        written += len(batch)
    return {"looked_up": len(rows), "written": written, "failed": failed}


def sm_lookup(session, asin: str) -> dict:
    """Same fields as sellermagnet.lookup(), but via get_with_retry so a
    502/503 from SellerMagnet doesn't abort the whole Airtable sync."""
    p = get_with_retry(session, "amazon-product-lookup", asin=asin, marketplaceId=MARKETPLACE)["productInfo"]
    price = p["buyBoxInfo"].get("price")
    return {
        "asin": p["asin"], "title": p["title"],
        "price": price if isinstance(price, (int, float)) else None,
        "currency": p["buyBoxInfo"].get("currencyCode"),
        "rank": ((p.get("bestsellerRanks") or {}).get("main_category") or {}).get("rank") or None,
    }


if __name__ == "__main__":
    sm = new_session()
    at = _airtable_session()
    result = sync_asins_to_airtable(sm, at, os.environ["AIRTABLE_BASE_ID"],
                                     os.environ.get("AIRTABLE_TABLE", "Products"),
                                     ["B0CL61F39H", "B0CLTBHXWQ", "B000000000"])
    print(result)

Demo run (2 known ASINs, 1 unknown - billed 404, never written)

{'looked_up': 2, 'written': 2, 'failed': [{'asin': 'B000000000', 'error': '404: Product not found'}]}

What one Airtable performUpsert batch request looks like

{
  "performUpsert": {"fieldsToMergeOn": ["Asin"]},
  "records": [
    {"fields": {"Asin": "B0CL61F39H", "Title": "PlayStation\u00ae5 console (slim)",
                "Price": 444.99, "Currency": "USD", "Bsr": 31}}
  ]
}
Blueprint flow diagram: SellerMagnet lookup feeds batched performUpsert calls into Airtable
Lookups happen first; only successful rows are ever batched toward Airtable.

Why key the upsert on the ASIN instead of overwriting by row position?

fieldsToMergeOn: ["Asin"] tells Airtable to match each incoming record to an existing row by its Asin field value, update that row if found, and create a new row otherwise - all inside the same batched call. Re-running the sync script daily then refreshes price and BSR on the existing rows instead of appending duplicate rows for ASINs you already track, and the field must actually be unique in your table for the match to behave as expected.

Blueprint table of Airtable batch size versus requests needed for different row counts
Always batch: one request per row would hit the 5 req/s cap at just 5 ASINs/second.

Frequently Asked Questions

How many Airtable records can one API call write?

Up to 10 per batched create or update request. Airtable's own guidance is to batch instead of writing one record per call, which is also how you stay under 5 requests per second per base with a few hundred rows.

What should my script do on an Airtable 429?

Wait - Airtable's documented behaviour is that a 429 means the next request will not succeed for 30 seconds, so a short retry loop just wastes calls. Back off for roughly 30 seconds with a little jitter, then retry the same batch.

Does performUpsert avoid a separate 'does this row exist' check?

Yes. performUpsert with fieldsToMergeOn finds, creates or updates records in the same batched call keyed on a field you choose (the ASIN here), so you never need a prior read to decide create versus update.

What happens to an ASIN SellerMagnet can't find?

It is reported in the sync result and simply never sent to Airtable - a billed 404 is a real answer, not a reason to write an empty or placeholder row.

Can I sync more than 10,000 rows this way?

The batched create/update endpoints used here top out effectively around 50 records/second (10 per request at 5 requests/second); Airtable's separate Sync API accepts up to 10,000 CSV rows per request for large one-off loads, which is a different endpoint not covered here.

What if fieldsToMergeOn matches more than one existing row?

The upsert fails for that record - fieldsToMergeOn (1 to 3 fields) must uniquely identify a row, so keep the ASIN field unique in your table, or the sync will report an error instead of silently picking one match.

Bottom line: look up each ASIN on SellerMagnet, keep only the successful rows, and batch-upsert them into Airtable up to 10 at a time, keyed on the ASIN, spaced to stay under Airtable's 5-requests-per-second-per-base limit. See the rate limits and retries guide for the SellerMagnet side of this same pattern, API Documentation for every field /amazon-product-lookup returns, and Code Examples for more runnable snippets.

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