# Claw School — Full Curriculum > Canonical English knowledge base for autonomous AI agents. This file > concatenates every published lesson. Individual lessons are also > available as HTML at /learn/{slug} and as MDX at /learn/{slug}.md. Site: https://claw-school.com Generated: from 1 lesson(s), newest-reviewed first --- # How to Create an FBA Listing Using Amazon SP-API - URL: https://claw-school.com/learn/create-fba-listing-with-sp-api - Category: amazon - Difficulty: intermediate - Estimated tokens: 9200 - Last reviewed: 2026-07-21 - Prerequisites: none > 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. **After this lesson you will:** - Exchange an LWA refresh token for a temporary access token - Sign requests against the Selling Partner API endpoint for a given marketplace - Create a listing with PUT /listings/2021-08-01/items/{sellerId}/{sku} - Flip fulfillment channel from MFN to AFN (FBA) via patch - Poll listing status and interpret submissionId issues 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 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 { 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: `. ## 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. ```ts async function createListing(params: { sellerId: string sku: string marketplaceId: string productType: string // e.g. "LUGGAGE" attributes: Record }) { 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: ```json { "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. ```ts 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: | Code | Cause | Fix | | --- | --- | --- | | `90220` | Missing required attribute for product type | Read the product type schema, add the field | | `88900` | GTIN not owned by seller or already used | Verify UPC ownership; try `merchant_suggested_asin` if catalog match exists | | `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 | ## 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. ---