---
slug: create-amazon-listing-with-sp-api
title: "How to Create an Amazon Listing (FBM or FBA) Using SP-API"
summary: "The shared foundation for creating any Amazon listing via the Selling Partner API — LWA auth, picking a productType, the product-identifier gate, building the attributes payload, the 90220 iterate loop, and the 100521 catalog-review state. End with a one-attribute fork: FBM (DEFAULT + quantity) or FBA (AMAZON_NA)."
category: amazon
difficulty: intermediate
estimatedTokens: 12500
lastReviewed: "2026-08-04"
prerequisites:
  []
learningOutcomes:
  - "Exchange an LWA refresh token for a temporary access token"
  - "Discover the correct productType for an item and fetch its definition schema"
  - "Satisfy the product-identifier gate (GTIN, GTIN exemption, or ASIN match)"
  - "Create a listing with PUT /listings/2021-08-01/items/{sellerId}/{sku}"
  - "Iterate on 90220 issues until ACCEPTED, then handle the 100521 catalog-review gate"
  - "Choose fulfillment channel — FBM (DEFAULT + quantity) or FBA (AMAZON_NA)"
  - "Split a title into item_name (≤75) + title_differentiation (≤125) under the 2026 modular title rule"
---

Creating an Amazon listing has two parts an agent must not conflate:

1. **A catalog listing** — the SKU exists in Amazon's catalog, has a title, images, price, and belongs to the seller. This is what buyers see.
2. **A fulfillment channel** — who stores the inventory and ships the order: **you (FBM/MFN)** or **Amazon (FBA)**.

The catalog-listing half is identical for FBM and FBA, and that is what this lesson covers. The fulfillment-channel half is **one attribute, `fulfillment_availability`, set at creation time** — and it is the only thing that differs between the two. At the end of this lesson you pick one:

- → [Create an **FBM** Listing](/learn/create-fbm-listing-with-sp-api) — `DEFAULT` + `quantity` + a shipping template. Buyable once review passes.
- → [Create an **FBA** Listing](/learn/create-fba-listing-with-sp-api) — `AMAZON_NA`, no quantity. Buyable once inbound stock reaches an Amazon FC.

**The shared path:** authenticate → pick a `productType` → read its definition schema → build the `attributes` payload (including the product identifier) → PUT → iterate on `90220` issues until `ACCEPTED` → wait out the `100521` catalog-review gate.

## Prerequisites

Before an agent can call SP-API on behalf of a seller, three artifacts must exist:

- **A Selling Partner developer profile**, registered under the seller's Amazon developer account, with the roles needed for the calls below (`Product Listing`, `Inventory and Order Tracking`).
- **A refresh token** — issued once when the seller authorized the app via the LWA consent flow (`https://sellercentral.amazon.com/apps/authorize/consent?application_id=...`). This is durable; store it encrypted per seller.
- **LWA client credentials** — `LWA_APP_ID` and `LWA_CLIENT_SECRET` from the developer profile. These belong to the app, not the seller.

Regional facts an agent must not hard-code and forget:

| Region | Endpoint | Token endpoint | Example marketplace |
| --- | --- | --- | --- |
| North America | `https://sellingpartnerapi-na.amazon.com` | `https://api.amazon.com/auth/o2/token` | US = `ATVPDKIKX0DER` |
| Europe | `https://sellingpartnerapi-eu.amazon.com` | `https://api.amazon.com/auth/o2/token` | UK = `A1F83G8C2ARO7P` |
| Far East | `https://sellingpartnerapi-fe.amazon.com` | `https://api.amazon.com/auth/o2/token` | JP = `A1VC38T7YXB528` |

Marketplace IDs are constants — cache them, don't look them up per request.

## Step 1 — Exchange the refresh token for an access token

LWA access tokens live for **one hour**. Cache them per seller until roughly 5 minutes before expiry, then refresh. Never call the token endpoint on every SP-API request; you will get rate-limited and the seller will get billed for slow agents.

```ts
type LwaToken = { access_token: string; expires_at: number }

async function getAccessToken(sellerId: string): Promise<string> {
  const cached = await tokenCache.get(sellerId)
  if (cached && cached.expires_at > Date.now() + 5 * 60_000) {
    return cached.access_token
  }

  const res = await fetch("https://api.amazon.com/auth/o2/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type:    "refresh_token",
      refresh_token: await getSellerRefreshToken(sellerId),
      client_id:     process.env.LWA_APP_ID!,
      client_secret: process.env.LWA_CLIENT_SECRET!,
    }),
  })

  if (!res.ok) {
    throw new Error(`LWA token exchange failed: ${res.status} ${await res.text()}`)
  }

  const json = (await res.json()) as { access_token: string; expires_in: number }
  const token: LwaToken = {
    access_token: json.access_token,
    expires_at:   Date.now() + json.expires_in * 1000,
  }
  await tokenCache.set(sellerId, token)
  return token.access_token
}
```

Every SP-API call from here on carries `x-amz-access-token: <access_token>`.

## Step 2 — Pick the productType and read its definition schema

The payload must match the **product type definition** for the item's category (`DRAIN_STRAINER`, `LUGGAGE`, `SHIRT`, …). Two sub-steps:

**Find the productType.** Either list what the marketplace accepts — `GET /definitions/2020-09-01/productTypes?marketplaceIds=<id>` returns ~1,800 names, grep for the category noun — or look up a reference ASIN's type via `GET /catalog/2022-04-01/items/{asin}?marketplaceIds=<id>&includedData=summaries` and read `summaries[].productType`. The catalog lookup of a competitor ASIN is the fastest path when you already know a similar product.

**Fetch the definition.** `GET /definitions/2020-09-01/productTypes/{productType}?marketplaceIds=<id>&requirements=LISTING`. The response does **not** inline the schema — it returns `schema.link.resource`, a **pre-signed S3 URL**. Fetch that URL with a plain GET and **no** `x-amz-access-token` header (S3 rejects the extra header with 403); the signature is self-contained. Cache the result; definitions change monthly, not daily.

> ⚠️ The schema's top-level `required` array is **not** the complete set of mandatory fields. Category-conditional fields (see Step 5) only surface when you PUT and read the `90220` issues. Treat the schema as a shape reference, not a final checklist.

## Step 3 — Build the `attributes` payload

Two things in the payload trip up every agent the first time: the **product-identifier gate**, and the fact that **every value is an array of marketplace-scoped objects**.

### The product-identifier gate (the #1 real-world blocker)

Amazon will not create a new catalog entry unless the item is identified one of three ways. Pick one:

| Strategy | Attribute | When to use |
| --- | --- | --- |
| **GTIN/UPC** | `externally_assigned_product_identifier` with `type` (`upc`/`ean`/`gtin`) + `value` | You have a real, owned barcode. The honest default. |
| **GTIN exemption** | `supplier_declared_has_product_identifier_exemption: [{ value: true }]` | The seller's brand has an approved exemption for this category. **This is how most private-label / sourced-from-supplier SKUs get created with no barcode.** |
| **ASIN match** | `merchant_suggested_asin: [{ value: "B0..." }]` | An identical catalog entry already exists; you add your own offer/price rather than creating a new product. |

Do **not** fabricate a UPC. A guessed barcode either fails `88900` (not owned / already used) or, worse, silently binds your listing to someone else's catalog entry. If the seller has a brand-level GTIN exemption, use it — that is the clean path for items sourced without barcodes.

### Every value is an array of marketplace-scoped objects

Text attributes (`item_name`, `brand`, `bullet_point`, …) carry `language_tag` + `marketplace_id`; structured ones (`condition_type`, `fulfillment_availability`, …) carry `marketplace_id`. A value without `marketplace_id` validates but silently applies to none.

### Enums are enforced

When a field has an enum, send the exact token. `required_product_compliance_certificate` for one type is `["California Air Review Board (CARB)", "Not Applicable"]`; `special_feature` accepts tokens like `Rust Resistant` / `Anti-Odor` / `Reusable`. Read the enum from the schema, don't paraphrase.

### A base payload (channel-agnostic)

This is everything except the fulfillment channel. The `fulfillment_availability` line is added in Step 4 (or in the FBM/FBA deep dives):

```json
{
  "item_name": [{ "language_tag": "en_US", "value": "AcmeGear 24-inch Hardside Suitcase", "marketplace_id": "ATVPDKIKX0DER" }],
  "brand": [{ "language_tag": "en_US", "value": "AcmeGear", "marketplace_id": "ATVPDKIKX0DER" }],
  "bullet_point": [{ "language_tag": "en_US", "value": "...", "marketplace_id": "ATVPDKIKX0DER" }],
  "product_description": [{ "language_tag": "en_US", "value": "...", "marketplace_id": "ATVPDKIKX0DER" }],
  "condition_type": [{ "value": "new_new", "marketplace_id": "ATVPDKIKX0DER" }],
  "country_of_origin": [{ "value": "CN", "marketplace_id": "ATVPDKIKX0DER" }],
  "supplier_declared_dg_hz_regulation": [{ "value": "not_applicable", "marketplace_id": "ATVPDKIKX0DER" }],
  "supplier_declared_has_product_identifier_exemption": [{ "value": true, "marketplace_id": "ATVPDKIKX0DER" }]
}
```

## The 2026 title rule: `item_name` (≤75) + `title_differentiation` (≤125)

Amazon split the listing title into two fields. A single long `item_name` is no longer the whole story on most categories — the always-shown title is capped, and a second "Item Highlight" field carries the rest.

| Field | API attribute | Limit | Role |
| --- | --- | --- | --- |
| Title | `item_name` | **≤75 chars** | The headline shown in search results + on mobile. Brand + core keyword + product type + the one spec that disambiguates. |
| Item Highlight | `title_differentiation` | **≤125 chars** | Secondary keywords, scenario, audience, differentiating selling points. Shown on the PDP, indexed for search. |

### Business — how to split it well

The split is a relevance-vs-clarity trade-off, not a copy-paste:

- **`item_name` is the only thing a mobile shopper sees in search.** Reserve it for the highest-intent terms: brand, the core product noun, the variant-defining spec (size / color / count). Anything that doesn't move click-through on a 6-inch screen loses its seat.
- **`title_differentiation` is where long-tail SEO lives.** Move scenario and audience phrases here — "for back-to-school", "travel-friendly", "gift for nurses". It's indexed, so you keep discoverability without bloating the mobile title.
- **Cost.** Rewriting an existing catalog is the real expense: an agent pass over *N* SKUs, plus a short-term ranking flux while Amazon re-indexes the new title signals. Run it on top sellers first; batch the long tail.
- **Effect.** Cleaner mobile titles (higher CTR where most buyers actually are) and a dedicated SEO surface that stops you cramming 200 chars into the headline. In competitive categories the modular structure is already table stakes.

Rule of thumb: if a phrase would make the mobile title wrap to two lines, it belongs in `title_differentiation`.

### Technical — send both fields

Same Listings Items API, same array-of-`{ marketplace_id, language_tag, value }` shape — just two attributes instead of one. On **PUT** (create), include both:

```json
"item_name": [
  { "value": "AcmeGear 24-inch Hardside Suitcase", "language_tag": "en_US", "marketplace_id": "ATVPDKIKX0DER" }
],
"title_differentiation": [
  { "value": "Lightweight checked luggage for international travel — 4 spinner wheels, TSA lock", "language_tag": "en_US", "marketplace_id": "ATVPDKIKX0DER" }
]
```

On **PATCH** (update either field), two ops:

```json
"patches": [
  { "op": "replace", "path": "/attributes/item_name",
    "value": [{ "value": "AcmeGear 24-inch Hardside Suitcase", "language_tag": "en_US" }] },
  { "op": "replace", "path": "/attributes/title_differentiation",
    "value": [{ "value": "Lightweight checked luggage for international travel…", "language_tag": "en_US" }] }
]
```

Agent implementation notes — mirror the read / build / diff loop you already run for `item_name`:

- **Read**: a GET returns both fields — read `title_differentiation` alongside `item_name`.
- **Build patches**: if your optimized payload carries `title_differentiation`, emit a second patch for `/attributes/title_differentiation`; otherwise omit it.
- **Diff**: compare both fields, not just `item_name`, or highlight changes silently slip through.
- **Backward compatible**: send only `item_name` and the listing behaves as a single-line title exactly as before; send both and you get the modular structure. Nothing else in the payload changes.

## Step 4 — PUT the listing

Use the **Listings Items API 2021-08-01**, not the legacy `JSON_LISTINGS_FEED` via the Feeds API. The modern PUT endpoint is synchronous-per-SKU, returns a `submissionId` you can poll, and gives per-attribute error messages. The Feeds API is still fine for batch imports of 10,000+ SKUs but is overkill for a single new listing.

```ts
async function putListing(params: {
  sellerId:      string
  sku:           string
  marketplaceId: string
  productType:   string            // e.g. "LUGGAGE"
  attributes:    Record<string, unknown>   // base payload + fulfillment_availability
}) {
  const token = await getAccessToken(params.sellerId)
  const url   = new URL(
    `/listings/2021-08-01/items/${params.sellerId}/${encodeURIComponent(params.sku)}`,
    "https://sellingpartnerapi-na.amazon.com",
  )
  url.searchParams.set("marketplaceIds", params.marketplaceId)

  const res = await fetch(url, {
    method: "PUT",
    headers: { "x-amz-access-token": token, "Content-Type": "application/json" },
    body: JSON.stringify({
      productType:  params.productType,
      requirements: "LISTING",
      attributes:   params.attributes,
    }),
  })

  if (!res.ok) throw new Error(`PUT listing failed: ${res.status} ${await res.text()}`)
  return res.json() as Promise<{ submissionId: string; status: "ACCEPTED" | "INVALID" | "IN_PROGRESS"; issues?: unknown[] }>
}
```

## Step 5 — Iterate on `90220` until `ACCEPTED`

Every write returns a `submissionId` and a `status`. The states:

- **`ACCEPTED`** — the payload validated and the listing is stored. Move on (then see Step 6).
- **`INVALID`** — validation failed. **No ASIN is created**, so iterating is safe and free. The `issues` array carries `code`, `message`, `severity`, and `attributeNames`. Add the named fields and re-PUT the same SKU (the operation is idempotent).
- **`IN_PROGRESS`** — Amazon is still processing. Poll `GET /listings/2021-08-01/items/{sellerId}/{sku}?marketplaceIds={id}&includedData=issues` every 30s. Don't poll faster; the write and read APIs share a rate bucket.

The `INVALID` you will hit most is **`90220` — "X is required but missing"** — and the fields it names are the category-conditional ones the schema's top-level `required` did not list. This is normal; expect one or two rounds. A real first PUT came back with eight missing fields; adding them with valid values and re-PUTting returned `ACCEPTED` with an empty `issues` array. That loop — PUT → read `90220` → fix → re-PUT — is the core creation workflow, not an edge case.

Other `INVALID` codes:

| Code | Cause | Fix |
| --- | --- | --- |
| `90220` | Missing required attribute for product type | Add the named field; re-PUT. Repeat until clean. |
| `88900` | GTIN not owned by seller or already used | Verify barcode ownership; switch to GTIN exemption or `merchant_suggested_asin`. |
| `100730` | Identical listing already being processed (duplicate) | Don't create two SKUs with identical product data; bind the second to the first's ASIN via `merchant_suggested_asin`. |
| `4000004` | Marketplace not eligible for product type | Check `productTypes` availability in target marketplace. |
| `18027` | Price outside marketplace min/max | Adjust `our_price`; some categories have Amazon-set floors. |

## Step 6 — Handle the catalog-review gate (`100521`)

`ACCEPTED` means your payload is stored and correct — but for a **new** catalog entry (especially one created via GTIN exemption), Amazon runs a catalog review before the listing goes live. The GET then returns issue `100521`:

> "We are reviewing this listing to determine if any additional information is required. Please allow up to 48 hours… otherwise the listing will be published."

Distinguish two things an agent often conflates:

- **Listing data stored correctly** — verifiable immediately via GET: the attributes you sent are all present. Creation succeeded.
- **ASIN assigned / buyable** — lags by minutes to ~48h while review runs. `summaries[].asin` is populated once the ASIN is assigned; `summaries[].status` moves `DISCOVERABLE` (searchable, not purchasable) → `BUYABLE` (purchasable). Poll the GET; only treat `BUYABLE` as "live."

Two post-submission states an agent must not misread:

- **Seller Central lags the API by ~1–2 hours.** A brand-new listing routinely shows as the wrong channel, "Missing offer", or quantity 0 in Seller Central even when the API already reports the correct `fulfillment_availability` channel and quantity. **Judge channel and inventory from the API (`GET .../items/{sellerId}/{sku}?includedData=attributes`), not from SC display in the first ~2 hours** — and do not "fix" a channel that the API says is already correct.
- **`18320 SEARCH_SUPPRESSED` — no main image.** A listing without `main_product_image_locator` can be `BUYABLE` (purchasable via a direct link) but is suppressed from search results. Add at least one image to make it discoverable.

## Step 7 — Choose your fulfillment channel

Set the channel in the same PUT (Step 4) — it is one attribute, `fulfillment_availability`:

- **FBM / MFN** — `fulfillment_availability: [{ "fulfillment_channel_code": "DEFAULT", "quantity": N }]` plus `merchant_shipping_group` (your shipping-template UUID). You hold and ship the stock. → Full walkthrough: [Create an FBM Listing Using SP-API](/learn/create-fbm-listing-with-sp-api).
- **FBA / AFN** — `fulfillment_availability: [{ "fulfillment_channel_code": "AMAZON_NA" }]`, no quantity. FBA also conditionally requires package dimensions, package weight, and a battery declaration (FCs need them) — fields FBM does not require. Amazon holds and ships the stock once you send an inbound shipment. → Full walkthrough: [Create an FBA Listing Using SP-API](/learn/create-fba-listing-with-sp-api).

> ⚠️ **Do not create two listings with identical product data.** Amazon's catalog dedup rejects the second with `100730` ("identical listing … is being processed") and suppresses it with `13013`. If you need a second SKU on the same product (e.g. an FBA offer alongside an FBM one), create the first, wait for its ASIN to appear, then create the second with `merchant_suggested_asin` set to that ASIN — it binds to the same catalog item instead of duplicating it.

## What to build on top

Once creation works reliably for one SKU, the same shapes cover:

- **Bulk creation** — same PUT body, driven from a spreadsheet or agent-generated batch. Rate limit is ~5 requests per second per seller; use a queue.
- **Multi-marketplace listings** — the same SKU can list in multiple marketplaces by putting more entries in each attribute's array. One PUT, many storefronts.

Do not build custom retry logic on 429 responses without reading the `x-amzn-RateLimit-Limit` header. SP-API uses a token bucket per operation, and the header tells you the current bucket size — build your rate limiter around that number, not around a wall-clock delay.
