Connections and listings
Connection, Listing, variant binding and where each published field comes from.
The channels say where the Seller sells. A Connection is a marketplace account. Inside it, the content of an item (title, photos, attributes) is a MarketplaceProduct, and the offer (price and limits) is a Listing. Neither keeps a product or stock: the lines of the offer, the bindings, point to Variants of the catalog.
The resources and their main fields
| Resource | In one sentence | Endpoints |
|---|---|---|
| Link session | The page where the Seller grants access to a marketplace account. | Create, get |
| Connection | One of the Seller's marketplace accounts, with the credential we keep. | List, capabilities |
| MarketplaceProduct | The content of an item on the Connection, shared between offers. | Get, update, variations |
| Listing | The offer on a Connection: price and limits, on top of a MarketplaceProduct. | Create, update, publish |
| Variant binding | One line of the offer: the content variation the Listing sells, with its id there. | List, replace, update, remove |
Connection: the Seller's account on a marketplace
A Connection is born when the Seller grants access in a link session. You do not create Connections through the API.
- One account, one Connection. Its identity is the Seller, the marketplace, the account there (
externalAccountId) and the shop (externalShopId, when the marketplace has one). If the Seller grants access to the same account again, it is the same Connection: theiddoes not change and it goes back toactive. - One link session can create many Connections, one per marketplace granted. They show up in the session's
connectionIds. - Each Connection says what it can do. Read the capabilities before you show a button to import, publish or send stock. An unsupported operation returns
422; one the Seller did not grant returns403. - Only an
activeConnection works. Creating a listing, importing or reconciling on a Connection in another state returns409withdetails.reason = "connection_inactive". - The import rule belongs to the Connection (
listingImportPolicy), because each account can be run differently. See Importing listings that already exist.
MarketplaceProduct: the content
The MarketplaceProduct is what the marketplace shows and shares between offers: title, description, bullets, brand, condition, category, attributes, media and the variation axes. Each marketplace has its own name for it: User Product on Mercado Livre, parent ASIN on Amazon, product group on Magalu. The ids there are kept in externalId (on Mercado Livre, the family_id, or the item_id of a legacy listing).
- It belongs to a Connection and has the same versions (
desired,observed) and apublicationState, like the Listing. The content you want to publish is indesired.content. - The variation (
MarketplaceProductVariant) links the content to a catalog Variant (variantId). It keeps the variation id there (externalVariantId) and what is only its own:desired.attributes,desired.mediaanddesired.gtin. The order of the map (position) is the order of the lastPUT. - You never create a MarketplaceProduct on its own. It is born with the Listing, through a clone or through an import. Changing the content is
PATCH /v1/marketplace-products/{id}; replacing the variations isPUT …/variants. Neither calls the marketplace. - The
catalogReference(the external catalog product the content is tied to) also lives here, not on the Listing.
Listing: an offer
The Listing is the offer: price, currency, reference price, purchase limit and minimum, quantity tiers, scheduled prices, audience, handling time, warranty and fulfillment channels (desired.offer), the sync policy and the status on the marketplace. It points to a MarketplaceProduct (marketplaceProductId) and has no content, attributes, fulfillment or regulatory: sending those fields returns 400. Creating a Listing (POST /v1/sellers/{sellerId}/listings) does not call the marketplace: the offer is born locally and only goes out when you ask to publish it.
One MarketplaceProduct can have many offers. That is how you test a price or the content on the same marketplace:
| I want to test | How | MarketplaceProduct |
|---|---|---|
| Price | create the Listing with marketplaceProductId, or clone with marketplaceProduct: "same" | the same |
| Content | create the Listing without marketplaceProductId, or clone with marketplaceProduct: "copy" | one for each |
- Without
marketplaceProductId, the Listing creates the MarketplaceProduct. With it, the Listing reuses the existing one (from the same Connection). - Some marketplaces accept only one offer per MarketplaceProduct (
listings.offersPerMarketplaceProductisonein the capabilities). There, the second offer returns422 CAPABILITY_UNSUPPORTED: use the content test. - Changing the title of a MarketplaceProduct changes all of its offers.
The same Variant can also be in offers of different MarketplaceProducts, or of another Connection. That is how you sell on many marketplaces with the same stock:
The three listings offer the same 8 units. When you record a sale (sale) from one of them, the Variant's balance drops for all of them. With stock sending turned on, the new balance goes on to the others. To create Listing B from A, clone it.
The three versions
The content and the offer each have their own versions, and each version has an owner:
| Version | Who writes it | What it is for | Where to read it |
|---|---|---|---|
desired | you, with PATCH | your intent: content on the MarketplaceProduct, offer on the Listing and the line | on the resource itself |
observed | the marketplace, through import and notifications | the last state confirmed out there. Read-only | …/observed of the Listing; observed of the MarketplaceProduct |
resolved | nobody: the API computes it on the spot | the final value of each field, per binding, with its source | …/resolved of the Listing; of the content |
Three rules follow:
PATCHnever publishes. It changesdesiredand marks the resource as changed (publicationState: "dirty"). Publishing is always your request:POST /v1/listings/{listingId}/publish.- The marketplace never changes your catalog. A change made out there goes to
observedand reaches you through thelisting.external_changedwebhook (offer) ormarketplace_product.updatedwithbehavior: "external_changed"(content). Product and Variant stay as they were. - Missing means "inherit". A field missing from
desiredcomes from the level below. It does not erase anything on the marketplace.
Where each published field comes from
For each binding, the API builds what would be published. The content is looked up in four levels, and the first level that has the field wins:
The offer has only two levels: the line (binding.desired.offer) wins over the Listing (desired.offer). Product and Variant have no selling price.
Not every field exists at every level:
| Field | MP variation | MarketplaceProduct | Variant | Product |
|---|---|---|---|---|
title, description, bullets | no | yes | yes | yes |
brand | no | yes | no | yes |
condition, categoryId, productType | no | yes | no | no |
attributes | yes | yes | yes | yes |
media | yes | yes | yes | yes |
gtin | yes | no | yes | no |
physical: weight and dimensions | no | no | yes | yes |
Details that change the result:
- Each key is decided on its own in
contentandattributes, and in the offer. The title can come from the MarketplaceProduct, and the description from the Product. mediaandphysicalcome whole from a single level. Photos from two levels are never mixed. An emptymedia: []counts as "no override" and inherits from the level below.physicalcomes from the Variant, otherwise from the Product.- Product and Variant keep text per language. Inheritance uses the first language stored.
- There is no
subtitlein the result.
The response of GET /v1/listings/{listingId}/resolved shows each value with its origin in source: binding, listing, marketplace_product_variant, marketplace_product, variant or product.
{
"binding": {
"id": "c41d…",
"marketplaceProductVariantId": "5b2d…",
"variantId": "6f1c…"
},
"resolved": {
"content": {
"title": {
"value": "iPhone 16 128 GB Black",
"source": "marketplace_product"
},
"description": { "value": "6.1-inch display…", "source": "product" },
"brand": { "value": "Apple", "source": "product" }
},
"offer": {
"priceInCents": { "value": 799900, "source": "listing" },
"currency": { "value": "BRL", "source": "listing" }
},
"attributes": {
"color": {
"value": { "type": "enum", "value": "black", "label": "Black" },
"source": "marketplace_product_variant"
}
},
"physical": { "value": { "weightInGrams": 170 }, "source": "variant" },
"media": null
}
}Variant binding: the line of the offer
The binding says: "this line of the offer sells this content variation". It points to the MarketplaceProduct variation (marketplaceProductVariantId), and the variation points to the catalog Variant. That path is how the offer knows what it sells, which balance to show and where to inherit content from.
| Field | What it is for |
|---|---|
marketplaceProductVariantId | the MarketplaceProduct variation this line sells |
variantId | the Variant of the variation. Read-only: it follows the variation |
externalId, externalSku | the id of the line on the marketplace (ML: the item; Shopee: the model_id) and the SKU, if known |
inventoryEligible | whether this line can receive stock sends |
desired.offer | price and limits only for this line: overrides the Listing's desired.offer |
linkSource | how the link was made: manual, imported_match or imported_create |
matchedBy | for imported_match, what matched: sku or gtin |
Rules:
PUT /v1/listings/{listingId}/variant-bindingsreplaces all lines at once, up to 100. Send[]to unlink the offer from the catalog.- Each line carries
marketplaceProductVariantIdorvariantId. WithvariantId, the API creates the missing variation on the MarketplaceProduct, marks the MarketplaceProductdirty, and announces it withmarketplace_product.updated(behavior: "variants_changed"). PATCHandDELETEon…/variant-bindings/{bindingId}touch one line, keeping its id. Both leave the Listingdirty.- A variation (and so a Variant) appears only once per offer. So does an external id. A repeat returns
400. - The Variant must belong to the offer's Seller and cannot be archived.
- With no line, the offer does not publish (
409,details.reason = "listing_unlinked"). - A variation that stays in the offer keeps its line and id. Its fields are updated. A line left out is deleted, and its id leaves
syncPolicy.inventory.eligibleBindingIdson its own.
Importing listings that already exist
When the Seller already has listings on the marketplace, the import brings each one in as a Listing, with what came from there in observed. For each listing, the import finds or creates the MarketplaceProduct by its external id, the variations (by external id; otherwise by the Variant's SKU or GTIN, never by title) and the Listing. The lines become bindings. What happens to the Variants depends on the Connection's listingImportPolicy:
| Policy | If a Variant matches | If none matches |
|---|---|---|
unlinked | does not try to match | the listing has no binding |
match_only | links (linkSource: "imported_match") | the variation has no binding |
match_or_create | links (linkSource: "imported_match") | creates one Product per listing and one minimal Variant per variation, and links them ("imported_create") |
- Matching means exact equality of SKU or GTIN with an active Variant of an active Product.
- If two Variants match, nothing is linked or created, under any policy. The listing becomes
linkState: "ambiguous"; pick the Variant with the bindingsPUT. - The created Product is minimal: a placeholder title,
metadata.imported: trueand Variants withdisabledstock. Fill in the record later. - Imported content arrives in the MarketplaceProduct's
observed, withpublicationState: "clean". Nothing becomesdesiredby itself. - An imported listing arrives with sending turned off (
syncPolicy.outbound.enabled: false). That way, nothing you change locally goes to the marketplace by accident. Turn it on when you will publish from here: while it is off, publishing returns409withdetails.reason = "outbound_disabled". - To fetch again what changed out there, use reconciliation. marketapi also reconciles on its own every 6 hours; the synchronization state shows when the last one ran and when the next is due.
The states
A Listing has three states (marketplaceStatus also comes in layers: buyable, discoverable, pendingRevision, and statusReasons), and each one answers a different question:
| Field | Question | Examples |
|---|---|---|
marketplaceStatus | How is the listing doing on the marketplace? | draft, active, paused, closed |
publicationState | Has what you changed here gone out there yet? | never_published, dirty, pending, clean |
linkState | Is the offer linked to the catalog? | linked, unlinked, ambiguous |
The MarketplaceProduct also has a publicationState, with the same values, for the content. Publishing the Listing of a MarketplaceProduct that was never published publishes the content too, and it becomes clean. If the content was already published and you change it, it becomes dirty. To publish only the content, without an offer, use this route.
How each one changes is in States and events.