---
slug: amazon-flat-file-bulk-upload-variation-errors
title: "Bulk-Uploading Amazon SKUs via Flat File and Avoiding Variation-Family Errors"
summary: "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."
category: amazon
difficulty: advanced
estimatedTokens: 1850
lastReviewed: "2026-09-07"
prerequisites:
  - create-amazon-listing-with-sp-api
learningOutcomes:
  - "Decide when flat-file bulk upload is the right tool instead of the single-SKU Listings Items API PUT"
  - "Structure a parent/child variation family in a flat file with a consistent variation_theme"
  - "Check a family for the four most common pre-submission rejection causes before sending it"
  - "Submit a flat-file feed through the Feeds API and poll it to completion"
  - "Read a flat-file processing report to find the specific row and reason a SKU failed"
---

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](/learn/create-amazon-listing-with-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:

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).

```ts
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**:

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.

```ts
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: 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](/learn/create-amazon-listing-with-sp-api).
