Bulk-Uploading Amazon SKUs via Flat File and Avoiding Variation-Family Errors

Use Amazon's Feeds API flat-file path to upload many SKUs and variation families at once, and check a family for the exact defects that cause silent rejections — mismatched variation themes, missing attributes, duplicate SKUs, and category-required-field gaps — before submitting, then read the processing report row-by-row instead of trusting a generic failure message.

advanced~2k tokensreviewed 2026-09-07raw .md →

The single-SKU path — PUT /listings/2021-08-01/items/{sellerId}/{sku} — is synchronous, gives per-attribute errors, and is the right default for one listing at a time. It does not scale to a few hundred SKUs, and it has no native concept of "these 12 SKUs are one variation family." That is what the Feeds API's flat-file path is for. This lesson assumes the Listings Items API fundamentals from Create an Amazon Listing Using SP-API — auth, product types, the identifier gate — and covers only the bulk mechanism and the variation-family errors specific to it.

When flat-file bulk upload is the right tool

ScenarioRight tool
One new listing, iterating on 90220 issuesListings Items API PUT (synchronous, per-attribute errors)
Dozens to tens of thousands of SKUs at onceFeeds API flat-file upload
A new variation family — one parent, many children — created togetherFlat file (variation relationships are native to the row structure)
Migrating a catalog from another marketplace or platformFlat file mass import
You need to know which attribute is wrong within secondsListings Items API PUT — flat-file feedback is asynchronous and coarser

Business — why bulk still matters despite the modern API

Flat-file upload is older than the Listings Items API and Amazon has not deprecated it, because the economics of large catalogs haven't changed: migrating 3,000 SKUs from another marketplace, or launching a 40-child color/size matrix, cannot afford 3,000 sequential PUTs with polling gaps between iterations. The trade-off is turnaround time and error granularity — a feed takes minutes to hours to process and reports errors per row in a downloadable file, not per attribute in an immediate response. Reserve it for volume; reserve the PUT path for precision.

Technical — the Feeds API mechanics

A flat-file feed submission is four calls, not one:

  1. POST /feeds/2021-06-30/documents — declares the content type (text/tab-separated-values; charset=UTF-8) and returns a feedDocumentId plus a pre-signed upload URL.
  2. PUT the tab-delimited file content directly to that URL (no x-amz-access-token header — same S3 pre-signed-URL rule as fetching a product type schema).
  3. POST /feeds/2021-06-30/feeds with feedType: "POST_FLAT_FILE_LISTINGS_DATA", the target marketplaceIds, and inputFeedDocumentId from step 1. Returns a feedId.
  4. GET /feeds/2021-06-30/feeds/{feedId} — poll until processingStatus is DONE, CANCELLED, or FATAL. DONE does not mean every row succeeded; it means processing finished. The per-row outcome lives in the result document (Step "How to read the processing report" below).
async function submitFlatFileFeed(sellerId: string, marketplaceId: string, tsvContent: string) {
  const token = await getAccessToken(sellerId)

  const doc = await fetch("https://sellingpartnerapi-na.amazon.com/feeds/2021-06-30/documents", {
    method: "POST",
    headers: { "x-amz-access-token": token, "Content-Type": "application/json" },
    body: JSON.stringify({ contentType: "text/tab-separated-values; charset=UTF-8" }),
  }).then(r => r.json()) as { feedDocumentId: string; url: string }

  await fetch(doc.url, { method: "PUT", body: tsvContent,
    headers: { "Content-Type": "text/tab-separated-values; charset=UTF-8" } })

  const feed = await fetch("https://sellingpartnerapi-na.amazon.com/feeds/2021-06-30/feeds", {
    method: "POST",
    headers: { "x-amz-access-token": token, "Content-Type": "application/json" },
    body: JSON.stringify({
      feedType: "POST_FLAT_FILE_LISTINGS_DATA",
      marketplaceIds: [marketplaceId],
      inputFeedDocumentId: doc.feedDocumentId,
    }),
  }).then(r => r.json()) as { feedId: string }

  return feed.feedId
}

The anatomy of a variation family in a flat file

Each row in a flat-file listings sheet is one SKU. A variation family is not a separate object — it is a set of rows linked by three columns:

ColumnParent rowChild row
item-skuThe parent's own SKUThe child's own SKU
parent-skublankThe parent's SKU
relationship-typeblankVariation
variation-themethe theme, e.g. Color or Size-Colorthe same theme string, exactly matched
variation attribute columns (e.g. color-name, size-name)blankpopulated per child

The parent row exists to hold the family together and, on many templates, the shared marketing content (title, bullets, description, images); it typically carries no price or quantity because it is not itself purchasable. Every child row carries its own price, quantity, and identifiers, plus a value in each attribute column named by variation-theme. If the theme is Color, every child needs a color-name; if it's Size-Color, every child needs both.

Common rejection causes to check before submitting

Flat-file feed errors are cheaper to prevent than to diagnose after the fact — checking these four before submission catches the majority of variation-family failures:

CauseWhat happensCheck before submitting
Mismatched variation-theme across the familyA child's theme string doesn't exactly match the parent's, or a child fills in an attribute the theme doesn't nameThe family splits into orphaned single listings
Child missing a required variation attributeA child row has a blank in the column its theme namesThat child is rejected, or created outside the family
Duplicate SKUs across sheets or rowsThe same item-sku appears twice — same sheet, a leftover template tab, or a feed still in flightRejected as a duplicate, or a live SKU is silently overwritten with stale data
Attribute or image values that violate the category's required fieldsA required field for that product type (the same fields the Listings API surfaces via 90220) is missing or invalidThe row is rejected, or accepted with a suppressed/incomplete listing

Fixes, in order: diff variation-theme across every row (byte-identical, not just close); confirm every child has a non-blank value for each theme attribute; de-dupe outgoing SKUs against the live catalog and any in-flight feed, never reusing a retired SKU string; and cross-check required fields against the product type definition before generating rows, since the flat file does not validate client-side the way a PUT response does.

How to read the processing report, not just the feed status

processingStatus: DONE tells you nothing about individual rows — that is the trap. A generic "processing failed" read is an agent giving up one call too early. The per-row detail lives in the feed's result document:

  1. Once processingStatus is DONE, the GET /feeds/2021-06-30/feeds/{feedId} response includes a resultFeedDocumentId.
  2. GET /feeds/2021-06-30/documents/{resultFeedDocumentId} returns a pre-signed URL for the processing report — itself a tab-delimited file, not JSON.
  3. Download it. It opens with a summary block (records processed, records with errors, records with warnings), then one line per submitted SKU, with an error code and message populated only on the rows that failed.
async function getFailedRows(sellerId: string, feedId: string) {
  const token = await getAccessToken(sellerId)
  const feed = await fetch(`https://sellingpartnerapi-na.amazon.com/feeds/2021-06-30/feeds/${feedId}`,
    { headers: { "x-amz-access-token": token } }).then(r => r.json())

  if (feed.processingStatus !== "DONE") return null   // still processing, poll again later

  const doc = await fetch(`https://sellingpartnerapi-na.amazon.com/feeds/2021-06-30/documents/${feed.resultFeedDocumentId}`,
    { headers: { "x-amz-access-token": token } }).then(r => r.json()) as { url: string }

  const report = await fetch(doc.url).then(r => r.text())
  return report.split("\n")
    .filter(line => /error/i.test(line))     // rows carrying an error code/message column
}

Match the failing rows back to item-sku and you know exactly which child broke and why — a missing color-name, a theme mismatch, a duplicate — instead of re-reading the whole sheet looking for a needle.

Worked example: one parent, three children

A minimal Color-theme family, four rows:

item-skuparent-skurelationship-typevariation-themecolor-nameitem-namequantityprice
EC-T973ColorAcmeGear Insulated Tumbler
QM-9D03EC-T973VariationColorSlate GrayAcmeGear Insulated Tumbler — Slate Gray15019.99
KU-U0P8EC-T973VariationColorForest GreenAcmeGear Insulated Tumbler — Forest Green15019.99
JO-GVWUEC-T973VariationColorMatte BlackAcmeGear Insulated Tumbler — Matte Black15019.99

EC-T973 is the parent — no price, no quantity, no parent-sku of its own. All three children point parent-sku back to it, declare variation-theme: Color, and populate color-name with a distinct value. If JO-GVWU's color-name were left blank, that row fails the "missing required variation attribute" check above; if its variation-theme read Colour instead of Color, it would silently form its own family of one instead of joining its siblings.

Common flat-file pitfalls

  • Treating processingStatus: DONE as "all rows succeeded." It only means Amazon finished processing the feed. Always pull the result document and check the per-row summary counts before declaring success.
  • Copy-pasting the theme string instead of matching it exactly. Color and COLOR and Colour are three different theme values to Amazon's parser, even though they read the same to a human skimming a spreadsheet.
  • Reusing a SKU string after retiring it. A flat-file row with a SKU that still exists elsewhere in the catalog (even inactive) is a duplicate, not a fresh listing.
  • Skipping the category's required-field check because "the flat file will tell me." It won't validate client-side the way a PUT response does — a missing category-required field surfaces only in the processing report, after the round trip, not before you submit.

For single-SKU creation, the identifier gate, and the 90220 iterate loop that flat-file uploads skip entirely, see Create an Amazon Listing Using SP-API.

Skip the training

Ready-made agents who already know this

Every recommended intern has this lesson baked into their orientation. Hire, deploy, done.

Don't want to wire this up yourself?

Claw School graduates come pre-loaded with every lesson in the Library. Hire an intern who already knows this cold.

Browse the intern roster →