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
| Scenario | Right tool |
|---|---|
One new listing, iterating on 90220 issues | Listings Items API PUT (synchronous, per-attribute errors) |
| Dozens to tens of thousands of SKUs at once | Feeds API flat-file upload |
| A new variation family — one parent, many children — created together | Flat file (variation relationships are native to the row structure) |
| Migrating a catalog from another marketplace or platform | Flat file mass import |
| You need to know which attribute is wrong within seconds | Listings 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:
POST /feeds/2021-06-30/documents— declares the content type (text/tab-separated-values; charset=UTF-8) and returns afeedDocumentIdplus a pre-signed upload URL.PUTthe tab-delimited file content directly to that URL (nox-amz-access-tokenheader — same S3 pre-signed-URL rule as fetching a product type schema).POST /feeds/2021-06-30/feedswithfeedType: "POST_FLAT_FILE_LISTINGS_DATA", the targetmarketplaceIds, andinputFeedDocumentIdfrom step 1. Returns afeedId.GET /feeds/2021-06-30/feeds/{feedId}— poll untilprocessingStatusisDONE,CANCELLED, orFATAL.DONEdoes 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:
| Column | Parent row | Child row |
|---|---|---|
item-sku | The parent's own SKU | The child's own SKU |
parent-sku | blank | The parent's SKU |
relationship-type | blank | Variation |
variation-theme | the theme, e.g. Color or Size-Color | the same theme string, exactly matched |
variation attribute columns (e.g. color-name, size-name) | blank | populated 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:
| Cause | What happens | Check before submitting |
|---|---|---|
Mismatched variation-theme across the family | A child's theme string doesn't exactly match the parent's, or a child fills in an attribute the theme doesn't name | The family splits into orphaned single listings |
| Child missing a required variation attribute | A child row has a blank in the column its theme names | That child is rejected, or created outside the family |
| Duplicate SKUs across sheets or rows | The same item-sku appears twice — same sheet, a leftover template tab, or a feed still in flight | Rejected as a duplicate, or a live SKU is silently overwritten with stale data |
| Attribute or image values that violate the category's required fields | A required field for that product type (the same fields the Listings API surfaces via 90220) is missing or invalid | The 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:
- Once
processingStatusisDONE, theGET /feeds/2021-06-30/feeds/{feedId}response includes aresultFeedDocumentId. GET /feeds/2021-06-30/documents/{resultFeedDocumentId}returns a pre-signed URL for the processing report — itself a tab-delimited file, not JSON.- 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-sku | parent-sku | relationship-type | variation-theme | color-name | item-name | quantity | price |
|---|---|---|---|---|---|---|---|
EC-T973 | Color | AcmeGear Insulated Tumbler | |||||
QM-9D03 | EC-T973 | Variation | Color | Slate Gray | AcmeGear Insulated Tumbler — Slate Gray | 150 | 19.99 |
KU-U0P8 | EC-T973 | Variation | Color | Forest Green | AcmeGear Insulated Tumbler — Forest Green | 150 | 19.99 |
JO-GVWU | EC-T973 | Variation | Color | Matte Black | AcmeGear Insulated Tumbler — Matte Black | 150 | 19.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: DONEas "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.
ColorandCOLORandColourare 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.