How to Create an FBA Listing Using Amazon SP-API

End-to-end walkthrough for an autonomous agent to authenticate against Amazon Selling Partner API, create a listing via the Listings Items API, and mark it Fulfilled by Amazon.

intermediate~9k tokensreviewed 2026-07-21raw .md →

Creating an FBA listing on Amazon 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. An FBA fulfillment channel — inventory for that SKU is stored in Amazon's fulfillment centers and shipped by Amazon, not the seller.

An MFN listing is a live buyable SKU that the seller ships themselves. An FBA listing is the same buyable SKU with its fulfillment channel switched to AMAZON_NA (or the regional equivalent) and inventory sent to Amazon. The SP-API path is: authenticate → create the listing MFN-style → patch fulfillment channel → send inbound shipment. This lesson covers the first three steps. Inbound shipments (the fulfillmentInbound_2024-03-20 API) are their own lesson.

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 credentialsLWA_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:

RegionEndpointToken endpointExample marketplace
North Americahttps://sellingpartnerapi-na.amazon.comhttps://api.amazon.com/auth/o2/tokenUS = ATVPDKIKX0DER
Europehttps://sellingpartnerapi-eu.amazon.comhttps://api.amazon.com/auth/o2/tokenUK = A1F83G8C2ARO7P
Far Easthttps://sellingpartnerapi-fe.amazon.comhttps://api.amazon.com/auth/o2/tokenJP = 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.

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 — Create the listing (MFN by default)

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.

The body follows the product type definition for the chosen category. Get the schema for your product type first (call GET /definitions/2020-09-01/productTypes/{productType} — cache it, they change monthly, not daily) and shape your payload to match.

async function createListing(params: {
  sellerId:      string
  sku:           string
  marketplaceId: string
  productType:   string     // e.g. "LUGGAGE"
  attributes:    Record<string, unknown>
}) {
  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(`Create listing failed: ${res.status} ${await res.text()}`)
  }
  return res.json() as Promise<{ submissionId: string; status: string; issues?: unknown[] }>
}

A minimal attributes payload for a LUGGAGE product type in the US marketplace looks like:

{
  "condition_type":  [{ "value": "new_new",       "marketplace_id": "ATVPDKIKX0DER" }],
  "item_name":       [{ "value": "24-inch Hardshell Rolling Suitcase",
                        "language_tag": "en_US", "marketplace_id": "ATVPDKIKX0DER" }],
  "brand":           [{ "value": "AcmeGear",     "marketplace_id": "ATVPDKIKX0DER" }],
  "externally_assigned_product_identifier": [
    { "type": "upc", "value": "812345678901", "marketplace_id": "ATVPDKIKX0DER" }
  ],
  "purchasable_offer": [{
    "currency": "USD",
    "our_price": [{ "schedule": [{ "value_with_tax": 89.99 }] }],
    "marketplace_id": "ATVPDKIKX0DER"
  }]
}

Every value on the modern Listings API is an array of marketplace-scoped objects. This is not optional — a value without marketplace_id will validate but silently apply to none.

Step 3 — Switch fulfillment channel to FBA

Newly created listings default to Merchant Fulfilled (MFN). To promote the SKU to FBA, patch the fulfillment_availability attribute. This tells Amazon "inventory for this SKU is at fulfillment center X, ship it yourselves." The Buy Box will not switch to Amazon-fulfilled until inventory actually arrives at an FC, but the listing is now FBA-eligible.

async function switchToFba(params: {
  sellerId:      string
  sku:           string
  marketplaceId: string
}) {
  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: "PATCH",
    headers: {
      "x-amz-access-token": token,
      "Content-Type":       "application/json",
    },
    body: JSON.stringify({
      productType: "LUGGAGE",
      patches: [{
        op:    "replace",
        path:  "/attributes/fulfillment_availability",
        value: [{
          fulfillment_channel_code: "AMAZON_NA",
          marketplace_id:           params.marketplaceId,
        }],
      }],
    }),
  })

  if (!res.ok) {
    throw new Error(`FBA switch failed: ${res.status} ${await res.text()}`)
  }
  return res.json() as Promise<{ submissionId: string; status: string }>
}

Interpreting the response

Every write returns a submissionId and a status. The three states an agent must handle:

  • ACCEPTED — the payload validated. The listing is live (or updated). Move on.
  • INVALID — validation failed. The issues array contains per-attribute messages with code, message, and attributeNames. Re-read the product type schema, fix the payload, retry with the same SKU (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 common INVALID codes an agent will encounter first:

CodeCauseFix
90220Missing required attribute for product typeRead the product type schema, add the field
88900GTIN not owned by seller or already usedVerify UPC ownership; try merchant_suggested_asin if catalog match exists
4000004Marketplace not eligible for product typeCheck productTypes availability in target marketplace
18027Price outside marketplace min/maxAdjust our_price; some categories have Amazon-set floors

What to build on top

Once these three calls work 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.
  • Inventory sync — the fulfillment_availability patch is where you also declare quantity for MFN SKUs. FBA quantity is separately governed by inbound shipments and is read via the FBA Inventory API.
  • 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.

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 →