This is the FBM (Merchant-Fulfilled / MFN) deep dive. The shared machinery β LWA auth, picking a productType, reading its definition schema, the product-identifier gate (GTIN / GTIN exemption / ASIN match), the 90220 iterate-until-ACCEPTED loop, the post-submission 100521 catalog-review state, and the title rule β lives in Create an Amazon Listing (FBM or FBA) Using SP-API. This page covers only what is specific to FBM.
What makes FBM different
With FBM you store and ship the inventory yourself, so the listing must declare how much you have and how you ship. Both live in the same fulfillment_availability attribute plus one more attribute, merchant_shipping_group:
| Channel | fulfillment_availability | quantity? | Extra required attr | Buyable when |
|---|---|---|---|---|
| FBM (this page) | [{ "fulfillment_channel_code": "DEFAULT", "quantity": N }] | yes | merchant_shipping_group (shipping-template UUID) | catalog review passes with quantity > 0 |
| FBA | [{ "fulfillment_channel_code": "AMAZON_NA" }] | no | β | inbound stock arrives at an Amazon FC |
DEFAULT is the channel code for merchant-fulfilled. It is also the default a listing gets if you omit the attribute entirely β but omitting it leaves quantity at 0, so an explicit DEFAULT + quantity is how you make the listing actually buyable.
Prerequisites specific to FBM
SP-API can reference these but cannot create them. They must already exist in Seller Central:
- A shipping template (Settings β Shipping Settings) β defines your ship-from address and rate rules. You need its UUID for
merchant_shipping_group. Find it in the URL or network panel when editing the template. - A return address (Settings β Return Settings) β a valid return address is required for a buyable FBM offer.
Step 1 β PUT the listing with DEFAULT + quantity + shipping template
Same endpoint and auth as any listing creation. The FBM specifics are the last three attributes:
async function createFbmListing(params: {
sellerId: string
sku: string
marketplaceId: string
productType: string // e.g. "DRAIN_STRAINER"
attributes: Record<string, unknown>
quantity: number // your live merchant stock
shippingGroupId: string // shipping-template UUID
}) {
const token = await getAccessToken(params.sellerId) // from the hub lesson
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[] }>
}
The FBM payload (the channel-specific part)
Build the base attributes as in the hub lesson β then set the three FBM fields. This is the exact shape that returned ACCEPTED for a DEFAULT-channel, quantity-2 listing:
"fulfillment_availability": [{ "fulfillment_channel_code": "DEFAULT", "quantity": 2 }],
"purchasable_offer": [{
"currency": "USD", "audience": "ALL", "marketplace_id": "ATVPDKIKX0DER",
"our_price": [{ "schedule": [{ "value_with_tax": 6.99 }] }]
}],
"list_price": [{ "currency": "USD", "value": 9.99, "marketplace_id": "ATVPDKIKX0DER" }],
"merchant_shipping_group": [{ "value": "<shipping-template-uuid>", "marketplace_id": "ATVPDKIKX0DER" }]
Optional but useful for FBM: lead_time_to_ship_max_days (handling time) rides inside the same fulfillment_availability object β { "fulfillment_channel_code": "DEFAULT", "quantity": 2, "lead_time_to_ship_max_days": 2 }. It is only valid on the DEFAULT channel.
Every value is still an array of marketplace-scoped objects; the title still follows the 2026 item_name (β€75) + title_differentiation (β€125) rule β both in the hub lesson.
Step 2 β Sync MFN inventory by updating quantity
Unlike FBA (where Amazon counts inbound stock), FBM quantity is yours to maintain. Whenever merchant stock changes, re-declare it. A targeted PATCH is cheaper than a full PUT:
{
"productType": "DRAIN_STRAINER",
"patches": [{
"op": "replace",
"path": "/attributes/fulfillment_availability",
"value": [{ "fulfillment_channel_code": "DEFAULT", "quantity": 17, "marketplace_id": "ATVPDKIKX0DER" }]
}]
}
Set quantity: 0 (or let it lapse) and the offer goes inactive β useful for pausing a listing without deleting it.
Step 3 β ACCEPTED β buyable: the DISCOVERABLE β BUYABLE transition
After ACCEPTED, a new listing typically sits in DISCOVERABLE (searchable, not purchasable) until the 100521 catalog review completes and the offer propagates. Two things to verify, and not to conflate:
- Listing data stored correctly β check immediately via GET:
fulfillment_availabilityshowsDEFAULT+ your quantity, price and shipping template present. Creation succeeded. - Status
BUYABLEβ lags by minutes to ~48h. PollGET /listings/2021-08-01/items/{sellerId}/{sku}?includedData=summaries;summaries[].statusmoves fromDISCOVERABLEtoBUYABLE. Only then can a customer actually purchase.
Do not treat DISCOVERABLE as a failure, but do not treat it as done either β keep polling until BUYABLE.
Common FBM pitfalls
- Omitting
quantity. The listing is created but stays at 0 stock β not buyable.DEFAULTwithoutquantityis the most common silent failure. - Omitting
merchant_shipping_group. Without a shipping template the offer cannot quote shipping and stays non-buyable. - Setting
quantityon what should be FBA. Wrong channel code β see the FBA lesson. - Treating
ACCEPTEDasBUYABLE. It only means the payload validated. ConfirmBUYABLEstatus separately.
For the shared foundation β auth, product types, the identifier gate, the 90220 loop, the title rule, rate limiting β see the hub lesson.