# marketapi documentation

> marketapi is a B2B API that puts many marketplaces (Mercado Livre, Magalu, Amazon, TikTok Shop, Shopee) behind one API. Your company (the account) integrates once; each of your sellers connects their marketplace accounts through a hosted page.

- Every page in Markdown, in one file: https://marketapi.shop/llms-full.txt?lang=en
- Every field of every endpoint (OpenAPI 3): https://api.marketapi.shop/openapi.json
- Esta documentação em português: https://marketapi.shop/llms.txt?lang=pt

---

# Overview

URL: https://marketapi.shop/docs

> What marketapi does, the phases of an integration and what each part of the API covers.

marketapi is an API for selling on several marketplaces at once: Mercado Livre, Magalu, Amazon, TikTok Shop and Shopee. You integrate once. Each of your sellers connects their accounts, and your system lists, sells and serves buyers through a single API.

## Who is who

| Who               | What it is                                                                                                  |
| ----------------- | ----------------------------------------------------------------------------------------------------------- |
| **You (account)** | The company that integrates. Has the dashboard login and the API keys.                                      |
| **Seller**        | Your customer: the one who sells. Has no login here. Connects their marketplace accounts on a page we host. |
| **Connection**    | One marketplace account of the seller, such as their Mercado Livre store.                                   |

## The phases

```mermaid
flowchart TB
  accTitle: The phases of an integration
  subgraph build["Build"]
    direction LR
    A["1. Set up"] --> B["2. Connect"] --> C["3. Catalog"] --> D["4. Publish"]
  end
  subgraph run["Run"]
    direction LR
    E["5. Stay in sync"] --> F["6. Sell"] --> G["7. Serve"] --> H["8. Grow"]
  end
  build --> run
```

| Phase                                                 | What you do                                                                             | API parts                                                                                                                                                                                         |
| ----------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [1. Set up](/docs/how-it-works/setup-and-connect)     | Create the account, the API key and the endpoint that gets webhooks.                    | [Authentication](/docs/authentication) · [Webhooks](/docs/webhooks/overview)                                                                                                                      |
| [2. Connect](/docs/how-it-works/setup-and-connect)    | Create the seller and send them the link to connect a marketplace.                      | [Sellers](/docs/sellers/overview) · [Link sessions](/docs/link-sessions/overview) · [Connections](/docs/connections/overview)                                                                     |
| [3. Catalog](/docs/how-it-works/catalog-and-stock)    | Register products, variants, kits, photos, warehouses and stock.                        | [Products](/docs/products/overview) · [Assets and media](/docs/assets/overview) · [Warehouses](/docs/warehouses/overview) · [Inventory](/docs/inventory/overview)                                 |
| [4. Publish](/docs/how-it-works/publishing)           | Build each marketplace's listing and publish it. Or import the ones that already exist. | [Marketplace products](/docs/marketplace-products/overview) · [Listings](/docs/listings/overview)                                                                                                 |
| [5. Stay in sync](/docs/how-it-works/staying-in-sync) | Keep stock, price and status the same on both sides.                                    | [Listings](/docs/listings/overview) · [Inventory](/docs/inventory/overview) · [Fulfillment inbound shipments](/docs/fulfillment-inbound-shipments/overview)                                       |
| [6. Sell](/docs/how-it-works/order-lifecycle)         | Get the order, send the NF-e, ship it and handle returns.                               | [Orders](/docs/orders/overview) · [Invoices](/docs/invoices/overview) · [Shipments](/docs/shipments/overview) · [Returns](/docs/returns/overview) · [Disputes](/docs/disputes/overview)           |
| [7. Serve](/docs/how-it-works/customer-service)       | Answer questions, messages and reviews.                                                 | [Questions](/docs/questions/overview) · [Conversations](/docs/conversations/overview) · [Reviews](/docs/reviews/overview)                                                                         |
| [8. Grow](/docs/how-it-works/commerce)                | Run promotions, tune prices, follow Ads, metrics and payouts.                           | [Promotions](/docs/promotions/overview) · [Pricing and fees](/docs/pricing/overview) · [Ads](/docs/ad-campaigns/overview) · [Metrics](/docs/metrics/overview) · [Finance](/docs/finance/overview) |

## Four rules that apply everywhere

1. **Whatever talks to the marketplace runs in the background.** The API answers right away with a [job](/docs/tracking-jobs), and the work goes on after.
2. **Every change becomes a webhook.** You find out without polling. See [Receiving webhooks](/docs/receiving-webhooks).
3. **The catalog belongs to the seller.** The marketplace never changes a product, variant or stock. What changes out there is kept on the listing, apart.
4. **Each marketplace does different things.** Before an action, the Connection tells you whether it can. See [Capabilities](/docs/how-it-works/capabilities).

## An example: one sale

Acme Store has 100 units of the black iPhone 16 and lists it on Mercado Livre, Amazon and Shopee. A sale comes in on Mercado Livre:

1. marketapi gets the order and tells you with the `order.created` webhook.
2. The order is paid, and 1 unit is reserved. 99 remain available.
3. marketapi sends 99 to every listing of that iPhone, on its own.
4. You send the NF-e, print the label and ship. The reservation becomes a sale.

The full path, step by step: [The order lifecycle](/docs/how-it-works/order-lifecycle).

## Where to go next

<Cards>
  <Card title="Getting started" href="/docs/getting-started">
    From the API key to the first seller, in five commands.
  </Card>

  <Card title="How it works" href="/docs/how-it-works">
    Each phase in detail: what happens, what to call and which webhooks arrive.
  </Card>

  <Card title="Data model" href="/docs/data-model">
    Every resource and how they link, with diagrams.
  </Card>

  <Card title="API reference" href="/docs/sellers/overview">
    One page per endpoint, with fields, examples and a playground.
  </Card>
</Cards>

The API lives at your environment's address, under `/v1`.

<Callout title="Integrating with an AI assistant?">
  The whole documentation is in plain text, in one file, at
  [`/llms-full.txt`](/llms-full.txt). The index is at [`/llms.txt`](/llms.txt).
</Callout>

---

# Getting started

URL: https://marketapi.shop/docs/getting-started

> From API key to your first seller.

The API lives at `https://api.marketapi.shop`. Keep the address in a variable for the examples:

```bash
export BACKEND_URL=https://api.marketapi.shop
```

## 1. Create your account

Open the [dashboard](/dashboard). On first login you see the **What is your company called?** screen: fill in **Company name** and click **Continue**.

## 2. Create an API key

Under *API keys*, create an API key and copy it. It is shown **only once**. See [Authentication](/docs/authentication).

## 3. Check that the API responds

```bash
curl $BACKEND_URL/health
# { "status": "ok", "mode": "api" }
```

`mode` depends on the environment (`api`, `worker` or `all`).

## 4. Create a seller

```bash
curl -X POST $BACKEND_URL/v1/sellers \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"name":"Acme Store","externalId":"customer-123"}'
```

`201` response:

```json
{
  "id": "0f6c1d6e-3b8e-4c3e-9a57-2f1f4d0c9b11",
  "name": "Acme Store",
  "externalId": "customer-123",
  "createdAt": "2026-10-06T14:30:00.000Z",
  "updatedAt": "2026-10-06T14:30:00.000Z"
}
```

The `Idempotency-Key` header lets you repeat the request without creating another seller. See [Idempotency and limits](/docs/idempotency-and-limits).

## 5. List your sellers

```bash
curl "$BACKEND_URL/v1/sellers?page=1&pageSize=20" \
  -H "Authorization: Bearer mk_your_api_key"
```

```json
{
  "data": [
    {
      "id": "0f6c1d6e-3b8e-4c3e-9a57-2f1f4d0c9b11",
      "name": "Acme Store",
      "externalId": "customer-123",
      "createdAt": "2026-10-06T14:30:00.000Z",
      "updatedAt": "2026-10-06T14:30:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "pageSize": 20,
    "totalItems": 1,
    "totalPages": 1,
    "hasNext": false,
    "hasPrev": false
  }
}
```

More about this resource in [Sellers](/docs/sellers/overview).

## What's next

Connect the seller's marketplaces: [Connecting a seller](/docs/connecting-a-seller). To know right away when they connect, register a webhook: [Receiving webhooks](/docs/receiving-webhooks).

Every endpoint has a page in the **API** section, with its fields, request and response examples, and a playground to try it.

---

# How it works

URL: https://marketapi.shop/docs/how-it-works

> The whole journey, from the first seller to the delivered order, and what each part does.

This section follows Acme Store from start to end. Each page is one phase: what happens, what you call and which webhooks arrive. To see the resources and how they link, use the [Data model](/docs/data-model).

## The three ends

```mermaid
flowchart LR
  accTitle: Your system, marketapi and the marketplaces
  S["Your system<br/>(ERP, app, panel)"] -->|"REST calls /v1"| M["marketapi"]
  M -->|"signed webhooks"| S
  M -->|"publishes, answers, ships"| K["Marketplaces<br/>(Mercado Livre, Amazon…)"]
  K -->|"notifications and reads"| M
```

* **Your system** only talks to marketapi. It calls the API and gets webhooks.
* **marketapi** keeps the seller's catalog, talks to each marketplace and turns everything into one format. Money is always in cents, dates have a time zone, and statuses have the same names on every marketplace.
* **The marketplaces** get what marketapi publishes and tell it when something changes there: an order, a question, a price changed in their panel.

## The phases

1. **[Set up and connect](/docs/how-it-works/setup-and-connect).** You create the account, the API key and the webhook endpoint. Then you create the seller and send a link. The seller authorizes the marketplace, and a Connection is born.
2. **[Catalog and stock](/docs/how-it-works/catalog-and-stock).** You register what the seller sells: products, variants, kits, photos and tax data. You say where the stock is and who owns it.
3. **[Publish listings](/docs/how-it-works/publishing).** For each marketplace, you build the content and the offer, validate and publish. If the seller already sells there, you import the listings.
4. **[Stay in sync](/docs/how-it-works/staying-in-sync).** Stock goes out to the listings on its own. What changes on the marketplace comes back to marketapi. Mismatches become alerts.
5. **[Sell: the order lifecycle](/docs/how-it-works/order-lifecycle).** The order arrives, stock is reserved, you send the NF-e, the label comes out and the order ships. Returns and disputes follow the same path.
6. **[Serve buyers](/docs/how-it-works/customer-service).** Questions before the purchase, messages after it, reviews and account health.
7. **[Grow and track](/docs/how-it-works/commerce).** Promotions, fee estimates, competitive price, Ads, metrics and finance.
8. **[Capabilities](/docs/how-it-works/capabilities).** How to know what each Connection can do before you ask.

## Around everything

Five rules apply in every phase:

| Rule                                                                     | Where to read                                          |
| ------------------------------------------------------------------------ | ------------------------------------------------------ |
| Whatever talks to the marketplace becomes a **job** (`202`).             | [Tracking jobs](/docs/tracking-jobs)                   |
| Every change becomes a **webhook**, with ids only.                       | [Receiving webhooks](/docs/receiving-webhooks)         |
| Every `POST` accepts an **`Idempotency-Key`**.                           | [Idempotency and limits](/docs/idempotency-and-limits) |
| Errors have a stable **`code`**.                                         | [Errors](/docs/errors)                                 |
| The **account isolates everything**: another account's id answers `404`. | [Data model](/docs/data-model)                         |

**Your system never waits for the marketplace.** A publication, an NF-e or an answer to a question goes into the queue and answers right away with the `jobId`. The result arrives as `job.completed`, `job.partially_completed` or `job.failed`. If the marketplace limits calls, the job waits for the quota to come back and goes on. It does not fail because of that.

---

# Set up and connect

URL: https://marketapi.shop/docs/how-it-works/setup-and-connect

> Account, API key, webhooks, the seller and their marketplace account.

This phase happens once per account and once per seller. At the end of it, Acme Store has an active Connection with Mercado Livre.

## 1. Account and API key

Sign in to the [dashboard](/dashboard), give the company name and activate the account. Then create an API key. It shows up **once**: keep it in your secrets vault.

Every call carries the key in the `Authorization: Bearer mk_…` header. One key works for the whole account, with every seller. See [Authentication](/docs/authentication) and [Billing](/docs/billing).

## 2. Webhook endpoint

Register a URL of yours to get the events. You pick the event types or use `*` for all. The signing secret shows up only at creation. Check the signature on every delivery.

If your URL goes down, marketapi retries for up to 24 hours. You can also redeliver one delivery. See [Receiving webhooks](/docs/receiving-webhooks).

## 3. The seller

Create one seller per customer of yours. Use `externalId` with their id in your system and `metadata` for anything else you want to keep. The seller has no login on marketapi: you are the one who talks to the API.

## 4. Connecting the marketplace

The seller authorizes access to their account on a page we host. You only create the session and send the link.

```mermaid
sequenceDiagram
  accTitle: How a seller connects a marketplace
  participant S as Your system
  participant M as marketapi
  participant V as Seller
  participant K as Marketplace
  S->>M: POST /v1/sellers/{id}/link-sessions
  M-->>S: url and connectUrls (valid for 5 minutes)
  S->>V: sends the link
  V->>K: authorizes access
  K-->>M: returns the authorization
  M-->>S: webhook connection.created
  Note over M: connection.sync job reads the account
  V->>M: clicks Done
  M-->>S: webhook link_session.completed
```

* **Two ways to connect.** The seller uses our page, which shows every marketplace. Or you put one of the `connectUrls` behind your own button, and they go straight to the marketplace screen.
* **Right after connecting**, a `connection.sync` job reads the account. Its id arrives in `connection.created`, in `data.syncJobId`.
* **The token is on us.** marketapi renews access on its own. If the seller or the marketplace revokes it, `connection.revoked` arrives, and a new link session reconnects the same account.
* **Your own app (BYOK).** You can use your own marketplace app instead of ours. See [BYOK](/docs/own-apps).

The step by step with `curl` is in [Connecting a seller](/docs/connecting-a-seller).

## 5. What the Connection tells you

An active Connection answers three questions:

| Question                          | Where                                          |
| --------------------------------- | ---------------------------------------------- |
| What can I do on this account?    | [Capabilities](/docs/connections/capabilities) |
| Is each resource up to date?      | [Sync state](/docs/connections/sync-state)     |
| Is the account's reputation good? | [Account health](/docs/connections/health)     |

## Endpoints in this phase

| To                           | Endpoint                                                                                                                                           |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Get events                   | [Create endpoint](/docs/webhooks/create-endpoint) · [Event types](/docs/webhooks/list-event-types)                                                 |
| Register the seller          | [Create](/docs/sellers/create) · [List](/docs/sellers/list) · [Update](/docs/sellers/update)                                                       |
| Make the connect link        | [Create link session](/docs/link-sessions/create) · [Get](/docs/link-sessions/get)                                                                 |
| See the connected accounts   | [List connections](/docs/connections/list) · [Get](/docs/connections/get) · [Disconnect](/docs/connections/disconnect)                             |
| Know what the account allows | [Capabilities](/docs/connections/capabilities) · [Health](/docs/connections/health) · [Marketplace options](/docs/connections/marketplace-options) |

**Webhooks in this phase:** `seller.created`, `seller.updated`, `seller.deleted`, `connection.created`, `connection.updated`, `connection.revoked`, `link_session.completed` and `connection.health_changed`.

---

# Catalog and stock

URL: https://marketapi.shop/docs/how-it-works/catalog-and-stock

> Products, variants, kits, media, warehouses and who owns the balance.

The catalog is the seller's base. It says **what** the seller sells, with no price and no marketplace. Listings use this base but never change it. The full model, with diagrams: [Catalog and inventory](/docs/data-model/catalog-and-inventory).

## 1. Product and variant

* **Product** is the item: title, brand, description, attributes, tax and compliance data.
* **Variant** is the combination that sells: the **black** iPhone 16 · 128 GB. It has its own SKU, GTIN, weight and size, for the item and for the package.
* Every Product is born with a default Variant. You create the others later.

Tax data is typed: `ncm`, `cest`, `origin`, CFOP, CST/CSOSN and PIS/COFINS. So are attributes: each one has a `key` and a typed value (text, number with unit, list, yes/no…). That way the same attribute works for every marketplace.

Weight is always in grams and size in millimeters, as integers: `weightInGrams: 170`, `dimensionsInMm: { length: 147, width: 72, height: 8 }`.

## 2. Kits

A kit is a Product whose Variant uses up others. Acme Store's charging kit takes 2 cables and 1 charger. You save the recipe in `components`. The kit's balance is computed: with 10 cables and 3 chargers in the same warehouse, there are 3 kits. A kit is not assembled from parts that sit in two places, so the calculation is done warehouse by warehouse.

## 3. Photos and videos

A file goes in through the API in two steps:

1. You ask for an upload URL (valid for 15 minutes) and send the file to it.
2. You confirm. marketapi checks the type by the content and only then accepts the file.

You can also bring a file from a public URL, in a job. Then you link the media to the Product or the Variant. To take that media to listings already created, ask for a **propagation**: it never happens on its own.

Documents with personal data, such as the NF-e XML and the shipping label, are **confidential**: they never have a URL and only come out through the API, with your key. See [Assets and media](/docs/assets/overview).

## 4. Warehouses

Every seller has a default warehouse, created on its own. If they keep stock in more than one place, create others. The balance is per Variant in each warehouse, and the Variant's balance is the sum.

* A movement without `warehouseId` goes to the default warehouse.
* A warehouse is never deleted: it is **archived**. The default one, and a warehouse that still has balance, cannot be archived.

## 5. Who owns the stock

Choose, on each Variant, where the truth of the balance comes from:

| `inventoryAuthority` | Who decides      | What marketapi does                          |
| -------------------- | ---------------- | -------------------------------------------- |
| `marketapi`          | marketapi        | keeps the movements and computes the balance |
| `managed_externally` | your ERP or WMS  | takes no movement; your system publishes     |
| `derived`            | the kit's recipe | computes the balance from the components     |
| `disabled`           | nobody           | publishes no quantity                        |

Pick a single owner. If the ERP and marketapi both count the same sale, the balance drifts.

## 6. Movements and balance

With `marketapi`, stock only changes through a **movement**: `adjustment`, `reservation`, `release`, `sale`, `return` or `transfer`. A movement never changes after it is created. The balance has three numbers:

| Field       | Means                                   |
| ----------- | --------------------------------------- |
| `onHand`    | what is physically in the warehouse     |
| `reserved`  | what was sold but has not left yet      |
| `available` | `onHand − reserved`: what can be listed |

Example: Acme Store receives 100 black iPhones (`adjustment` +100). A paid order reserves 1: `onHand` 100, `reserved` 1, `available` 99. When the order ships, the reservation becomes a `sale`: `onHand` 99, `reserved` 0.

To take units from one warehouse to another, use `transfer`: it writes an outbound at the source and an inbound at the destination, with the same `transferId`. The Variant total does not change. If the warehouse does not have enough stock, the API responds `409` (`inventory_underflow`) and writes nothing.

With orders turned on, marketapi creates the reservation, the sale and the release on its own. See [The order lifecycle](/docs/how-it-works/order-lifecycle).

## Endpoints in this phase

| To                | Endpoint                                                                                                                                    |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Register products | [Create](/docs/products/create) · [List](/docs/products/list) · [Update](/docs/products/update) · [Archive](/docs/products/archive)         |
| Register variants | [Create](/docs/products/create-variant) · [List](/docs/products/list-variants) · [Update](/docs/products/update-variant)                    |
| Build kits        | [See recipe](/docs/products/list-components) · [Replace recipe](/docs/products/replace-components)                                          |
| Upload files      | [Ask for upload](/docs/assets/create-upload) · [Confirm](/docs/assets/complete-upload) · [Bring from a URL](/docs/assets/ingest)            |
| Link media        | [See media](/docs/products/get-media) · [Replace media](/docs/products/replace-media) · [Propagate](/docs/products/propagate)               |
| Manage warehouses | [Create](/docs/warehouses/create) · [List](/docs/warehouses/list) · [Update](/docs/warehouses/update) · [Archive](/docs/warehouses/archive) |
| Move stock        | [Record movement](/docs/inventory/create-movement) · [Balance](/docs/inventory/balance) · [Movements](/docs/inventory/list-movements)       |

**Webhooks in this phase:** `product.created`, `product.updated`, `product.archived`, `variant.created`, `variant.updated`, `variant.archived`, `inventory.movement_created`, `warehouse.created`, `warehouse.updated`, `warehouse.archived`, `asset.ready` and `asset.failed`.

---

# Publish listings

URL: https://marketapi.shop/docs/how-it-works/publishing

> From the catalog to the published listing, or from the existing listing to the catalog.

A listing has two parts. The **content** (MarketplaceProduct) is what the buyer sees: title, description, photos, attributes and variations. The **offer** (Listing) is what they pay: price, handling time, warranty and quantity. One line of the offer, the **binding**, links each variation to a Variant of the catalog. The full model: [Connections and listings](/docs/data-model/connections-and-listings).

```mermaid
flowchart LR
  accTitle: The parts of a listing
  V["Variant<br/>(catalog)"] -. "variation" .- MP["MarketplaceProduct<br/>(content)"]
  MP --> L["Listing<br/>(offer)"]
  L --> B["Binding<br/>(offer line)"]
  B -. "sells" .-> V
```

Keeping the two parts apart lets you test price with two offers on the same content, or test a title with two contents.

## 1. Find out what the marketplace asks for

Each marketplace has its own categories and asks for different fields. Before building the listing, ask the Connection for:

* the right **category** and its **requirements**: which attributes are required, and when;
* the **brands**, **product types**, **size chart** and **shipping options** it accepts;
* whether a marketplace **catalog product** already exists for the item. Linking to it makes the listing stronger, and some fields start coming from the catalog.

These reads answer right away, with no job.

## 2. Build the listing

1. **Create the offer.** The Listing starts as a local draft and creates the content with it. Nothing goes to the marketplace yet.
2. **Fill in the content** on the MarketplaceProduct. What you leave empty is inherited from the Product and the Variant.
3. **Link the variations** to the Variants: the black and the white iPhone become two lines of the same offer.
4. **Set the offer:** `priceInCents`, reference price, quantity tiers, scheduled prices, handling time and warranty. A line can have its own price.
5. **Check the result.** `resolved` shows the final value of each field and where it came from.

Options only one marketplace understands go in `marketplaceOptions`, kept per marketplace.

## 3. Validate and publish

* **Validate** asks the marketplace whether the listing would pass, without publishing. Pending items show up in `issues`.
* **Publish** sends the content and the offer. If the content was never published, it goes along.
* **Publish in batch** sends many listings in one job, one item per listing.

All of these answer `202` with a job. When publishing takes several calls, the job item keeps each step, and a new attempt goes on from where it stopped. At the end, the Listing keeps the marketplace ids and `observed` with what is there.

## 4. Already selling there? Import

If Acme Store already has listings on Mercado Livre, import them instead of building them again. The Connection's import policy decides what to do with each listing:

| Policy            | What happens                                                                 |
| ----------------- | ---------------------------------------------------------------------------- |
| `unlinked`        | brings the content and the offer, without linking to the catalog             |
| `match_only`      | links when the SKU or the GTIN finds **one** Variant                         |
| `match_or_create` | links when it finds one; if not, creates a minimal Product for you to review |

Matching never uses the title. If two Variants match, nothing is linked, and the listing stays `ambiguous` until you fix the map.

## 5. After publishing

* **Marketplace actions:** pause, activate, close or relist. Relisting creates a new Listing, linked to the old one.
* **Remove:** `publication=delete` takes the listing off the marketplace; `keep` deletes it here only.
* **Clone:** copies an offer as a draft, to test price or content.

## Endpoints in this phase

| To                          | Endpoint                                                                                                                                                                                                     |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Find requirements           | [Categories](/docs/connections/listing-categories) · [Requirements](/docs/connections/listing-requirements) · [Brands](/docs/connections/brands) · [Shipping options](/docs/connections/fulfillment-options) |
| Use the marketplace catalog | [Search the catalog](/docs/connections/catalog-products) · [Candidates](/docs/marketplace-products/catalog-candidates) · [Link](/docs/marketplace-products/replace-catalog-reference)                        |
| Build the listing           | [Create offer](/docs/listings/create) · [Content](/docs/marketplace-products/update) · [Variations](/docs/marketplace-products/replace-variants) · [Bindings](/docs/listings/replace-bindings)               |
| Check first                 | [Resolved](/docs/listings/resolved) · [Validate](/docs/listings/validate) · [Issues](/docs/listings/issues)                                                                                                  |
| Publish                     | [Publish](/docs/listings/publish) · [Content only](/docs/marketplace-products/publish) · [In batch](/docs/listings/publish-batch)                                                                            |
| Import                      | [Policy](/docs/connections/update-listing-import-policy) · [Import](/docs/connections/import-listings)                                                                                                       |
| Act on the listing          | [Actions](/docs/listings/marketplace-actions) · [Clone](/docs/listings/clone) · [Remove](/docs/listings/remove)                                                                                              |

**Webhooks in this phase:** `listing.created`, `listing.updated`, `listing.deleted`, `listing.status_changed`, `marketplace_product.created`, `marketplace_product.updated` and `job.completed`.

---

# Stay in sync

URL: https://marketapi.shop/docs/how-it-works/staying-in-sync

> Stock that goes out on its own, changes that come back from the marketplace and how to settle differences.

Once published, a listing lives in two places: on marketapi and on the marketplace. This phase keeps both the same. What you want lives in `desired`. What the marketplace confirmed lives in `observed`. The seller's catalog never changes because of the marketplace.

```mermaid
flowchart LR
  accTitle: The two directions of sync
  D["desired<br/>(what you want)"] -->|"publication and stock"| K["Marketplace"]
  K -->|"notifications and reconciliation"| O["observed<br/>(what is there)"]
  O -. "difference" .-> C["diff, conflict<br/>or discrepancy"]
```

## 1. The sync policy

Each Listing has a policy with three parts:

| Part        | Values                     | What it decides                                                                   |
| ----------- | -------------------------- | --------------------------------------------------------------------------------- |
| `outbound`  | `enabled: true` or `false` | whether publish commands may call the marketplace                                 |
| `inbound`   | `active` or `disabled`     | whether changes made out there are saved in `observed`                            |
| `inventory` | `automatic` or `manual`    | whether each balance change publishes the quantity on its own, and on which lines |

## 2. Automatic stock

With `inventory: automatic`, every balance change publishes `available` on each chosen line of the offer. It does not matter where the change came from: a movement of yours, an order's reservation, a return.

At Acme Store: the black iPhone's balance drops from 100 to 99. marketapi creates a job and sends 99 to every listing of that iPhone, on Mercado Livre, Amazon and Shopee. At the end, `inventory.published` arrives.

* **Kits follow.** Selling a cable changes the kit's balance, and the kit is published too.
* **Only quantity goes out on its own.** Price, title and photos only go to the marketplace when you publish.
* **One call for shared stock.** Where the marketplace shares stock between offers, marketapi publishes once. The Connection says so in `inventory.scope`.
* With `manual`, you choose when to resend.

## 3. What changes on the marketplace

The seller may change a price right in the Mercado Livre panel. marketapi finds out in two ways:

* **Notification:** the marketplace tells it right away.
* **Reconciliation:** every 6 hours, marketapi compares everything. This catches what a notification missed.

The change goes to `observed` and `listing.external_changed` arrives. When the status changes (paused, under review, out of stock), `listing.status_changed` arrives with the reason.

A **conflict** is when both sides changed the same field: you changed the price here and have not published yet, and the seller changed it there. marketapi does not choose. `listing.conflict_created` arrives, and you [resolve](/docs/listings/resolve-conflict) it: `keep_local` (your value stays), `accept_external` (theirs stays), `inherit` (your override goes away) or `manual` (you send the value). Resolving never publishes.

To understand a listing, use:

* [`diff`](/docs/listings/diff): what changed here and what changed there since the last publication;
* [`issues`](/docs/listings/issues): what blocks publishing or selling;
* [`activity`](/docs/listings/activity): the listing's history;
* [`publish-attempts`](/docs/listings/publish-attempts): each publish attempt, with the error.

## 4. Stock in the marketplace's warehouse

Some marketplaces keep the product in their own warehouse, such as Mercado Livre Full and Amazon FBA. There, the marketplace counts the stock:

* The Connection's **stock locations** say what belongs to the seller (marketapi publishes) and what belongs to the marketplace (read only).
* The **fulfillment map** says which of the seller's warehouses feed each location.
* A **fulfillment inbound shipment** records units sent to the marketplace's warehouse.

When what marketapi published and what the marketplace says it has do not match, a **discrepancy** is born, with the `inventory.discrepancy_detected` event. You check and settle it.

## 5. Is everything up to date?

The Connection's sync state shows, per resource (listings, orders, questions…), the last full read, the last notification and the next reconciliation. To force a read, ask for a reconciliation.

## Endpoints in this phase

| To                       | Endpoint                                                                                                                                                                                                                       |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Set up sync              | [See policy](/docs/listings/sync-policy) · [Change policy](/docs/listings/update-sync-policy)                                                                                                                                  |
| Resend stock             | [Resend stock](/docs/inventory/publish)                                                                                                                                                                                        |
| See what is there        | [Observed](/docs/listings/observed) · [Differences](/docs/listings/diff) · [Issues](/docs/listings/issues) · [History](/docs/listings/activity)                                                                                |
| Settle conflicts         | [List conflicts](/docs/listings/conflicts) · [Resolve](/docs/listings/resolve-conflict)                                                                                                                                        |
| Force a read             | [Reconcile listings](/docs/connections/reconcile-listings) · [Refresh one listing](/docs/listings/refresh)                                                                                                                     |
| Stock at the marketplace | [Locations](/docs/connections/stock-locations) · [Map](/docs/connections/update-fulfillment-mapping) · [Discrepancies](/docs/inventory/list-discrepancies) · [Inbound shipments](/docs/fulfillment-inbound-shipments/overview) |
| See the state            | [Sync state](/docs/connections/sync-state)                                                                                                                                                                                     |

**Webhooks in this phase:** `inventory.published`, `listing.external_changed`, `listing.status_changed`, `listing.conflict_created`, `listing.conflict_resolved`, `inventory.discrepancy_detected` and `fulfillment_inbound_shipment.updated`.

---

# Sell: the order lifecycle

URL: https://marketapi.shop/docs/how-it-works/order-lifecycle

> From the incoming order to the delivered package, with stock, NF-e, shipping, returns and disputes.

This page follows one Acme Store sale from start to end. Acme Store has 100 black iPhone 16 units, listed on Mercado Livre, Amazon and Shopee. A buyer buys 1 on Mercado Livre.

## The sale, step by step

```mermaid
sequenceDiagram
  accTitle: One Acme Store sale, from order to dispatch
  participant K as Mercado Livre
  participant M as marketapi
  participant S as Your system
  participant O as Other listings
  K->>M: new order
  M-->>S: order.created
  K->>M: payment approved
  Note over M: reserves 1 · 99 available
  M-->>S: order.updated
  M->>O: publishes 99 (job)
  M-->>S: inventory.published
  S->>M: sends the NF-e XML
  M->>K: delivers the NF-e (job)
  M-->>S: invoice.accepted
  S->>M: asks for the label
  M-->>S: shipment.label_ready
  S->>M: dispatches
  Note over M: reservation becomes a sale · 99 on hand
  M-->>S: shipment.updated
```

## 1. The order arrives

The marketplace tells marketapi, which creates the order and sends `order.created`. If a notice gets lost, reconciliation brings the order anyway. You can also import older orders.

An order has its status on separate axes, so you know everything at once:

| Axis                | Examples                            |
| ------------------- | ----------------------------------- |
| `status`            | `open`, `completed`, `cancelled`    |
| `paymentStatus`     | pending, paid, refunded             |
| `fulfillmentStatus` | waiting to ship, shipped, delivered |
| `invoiceStatus`     | no NF-e, sent, accepted, rejected   |

Amounts come in parts: subtotal, shipping paid by the buyer, discounts (and who paid each one), taxes, fees and the net. All in cents. marketapi recalculates nothing: if the sum does not match the marketplace total, the difference shows up in `roundingAdjustmentInCents`.

Each order line points to the listing and the Variant sold. If a line has no link yet (a listing made outside marketapi, for example), it stays `unlinked` with the marketplace SKU. It links on its own once the link exists.

## 2. Stock reacts

The Connection's order policy decides what happens to the balance. It applies to Variants with `inventoryAuthority: marketapi`:

| When the order… | Stock…                                       | At Acme Store              |
| --------------- | -------------------------------------------- | -------------------------- |
| is paid         | reserves                                     | 99 available, 100 on hand  |
| ships           | is deducted (the reservation becomes a sale) | 99 available, 99 on hand   |
| is cancelled    | releases the reservation                     | 100 available, 100 on hand |

Each balance change triggers [automatic publication](/docs/how-it-works/staying-in-sync#2-automatic-stock): the other listings get 99 without you doing anything. For a kit, the reservation and the sale go to the components: selling 1 kit reserves 2 cables and 1 charger.

The policy can be turned off. Then you record the movements through the API, as you would with your ERP.

## 3. The NF-e

marketapi **does not issue** NF-e. You issue it in your ERP and send the XML:

1. Upload the XML as a file (it is confidential: it never has a URL).
2. Create the order's NF-e with the file id.
3. marketapi checks the key, the amount and the issuer's CNPJ against the order. If something does not match, the answer says why.
4. A job delivers the NF-e to the marketplace. `invoice.accepted` or `invoice.rejected` arrives.

Need to change the invoice? Send a new one that replaces the old one.

## 4. Shipping

A shipment is a package. One order can turn into several packages, and one package can carry items from several orders of the same buyer. Each shipment has a mode:

| `mode`                    | Who carries it                                              |
| ------------------------- | ----------------------------------------------------------- |
| `seller_ship`             | the seller, with their own carrier                          |
| `marketplace_logistics`   | the marketplace's logistics, with a label                   |
| `marketplace_fulfillment` | the marketplace's warehouse (Full, FBA): you only follow it |
| `pickup`                  | the buyer picks it up                                       |

The usual flow: see the dispatch options, ask for the label (`shipment.label_ready` arrives), download the file and dispatch. The deadline is in `dispatchDueAt`. Each change arrives as `shipment.updated`, and tracking shows the delivery events.

**The NF-e locks the shipment.** While the shipment shows `invoice_required` in `statusReasons`, asking for a label or dispatching answers `409`. Send the NF-e first.

## 5. Buyer data

Name, tax id, phone and address are kept apart, encrypted. They never go in a webhook or in the regular order read. To see them, call the buyer route: each access is logged.

The data is deleted after the retention period: usually 90 days after the order closes, or less if the marketplace requires it. The order stays, with those fields marked `purged`.

## 6. When something goes wrong

* **Cancellation.** The order goes to `cancelled`, `order.cancelled` arrives and the reservation is released.
* **Return.** The buyer sends the product back. You follow the return tracking and check what arrived. When the unit arrives in good shape, it goes back to stock, once. Events `return.created` and `return.updated`.
* **Dispute.** A claim, mediation, cancellation request or chargeback. The dispute has the deadline (`deadlineAt`) and the allowed actions (`allowedActions`): refund, accept the return, send evidence. You read the dispute's messages through the API. Events `dispute.created`, `dispute.updated` and `dispute.message_received`.
* **Order actions.** Cancel, or another action the marketplace allows, in a job.

## Endpoints in this phase

| To              | Endpoint                                                                                                                                                                   |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Set up stock    | [See order policy](/docs/connections/order-policy) · [Change](/docs/connections/update-order-policy)                                                                       |
| Bring orders    | [Import](/docs/connections/import-orders) · [Reconcile](/docs/connections/reconcile-orders)                                                                                |
| Read orders     | [List](/docs/orders/list) · [Get](/docs/orders/get) · [Buyer](/docs/orders/buyer) · [Actions](/docs/orders/actions)                                                        |
| Send the NF-e   | [Upload the XML](/docs/assets/create-upload) · [Create NF-e](/docs/invoices/create) · [Get](/docs/invoices/get) · [Download XML](/docs/invoices/xml)                       |
| Ship            | [Options](/docs/shipments/dispatch-options) · [Ask for label](/docs/shipments/request-label) · [Dispatch](/docs/shipments/dispatch) · [Tracking](/docs/shipments/tracking) |
| Handle problems | [Returns](/docs/returns/overview) · [Disputes](/docs/disputes/overview) · [Dispute messages](/docs/disputes/send-message)                                                  |

**Webhooks in this phase:** `order.created`, `order.updated`, `order.cancelled`, `inventory.published`, `invoice.accepted`, `invoice.rejected`, `shipment.updated`, `shipment.label_ready`, `return.created`, `return.updated`, `dispute.created`, `dispute.updated` and `dispute.message_received`.

---

# Serve buyers

URL: https://marketapi.shop/docs/how-it-works/customer-service

> Questions before the purchase, messages after it, reviews and account health.

Good service weighs on the seller's reputation, and reputation weighs on sales. marketapi brings service from every marketplace into one format.

| Channel          | When it happens     | Who sees it                    | Resource                                      |
| ---------------- | ------------------- | ------------------------------ | --------------------------------------------- |
| **Question**     | before the purchase | everyone, on the listing       | [Questions](/docs/questions/overview)         |
| **Conversation** | after the purchase  | only the buyer and the seller  | [Conversations](/docs/conversations/overview) |
| **Review**       | after delivery      | everyone                       | [Reviews](/docs/reviews/overview)             |
| **Health**       | all the time        | the seller and the marketplace | [Account health](/docs/connections/health)    |

## The webhook does not carry the text

Questions, messages and reviews are free text and may hold personal data. So the webhook carries only ids. When you get the event, fetch the item through the API and read the text there.

```mermaid
sequenceDiagram
  accTitle: How to answer a question
  participant K as Marketplace
  participant M as marketapi
  participant S as Your system
  K->>M: new question on the listing
  M-->>S: question.received (ids only)
  S->>M: GET /v1/questions/{questionId}
  M-->>S: the question text
  S->>M: POST …/answers
  M->>K: publishes the answer (job)
```

## Questions

A buyer asks on the iPhone listing whether it comes with a charger. `question.received` arrives. You read it, answer, and marketapi publishes the answer in a job. The question goes from `unanswered` to `answered`.

You can also delete a question or block who asked, if the marketplace allows it.

## Conversations

Every conversation is born with the order. There is no "new conversation": to talk to the buyer, use the order's conversation. Each new message arrives as `message.received`.

Before sending, look at the conversation's `sendability`. Marketplaces limit what the seller can send: text length, number of attachments, whether they can start the conversation. If the message does not pass, the answer is `409 message_not_sendable` with the reason, and no job. Attachments go up as confidential files.

A claim opened by the buyer is not a conversation: it is a [dispute](/docs/disputes/overview).

## Reviews

A review can be about the product, the sale or the seller (`subject`). The rating comes in `ratingBps`, from 0 to 10000, so you can compare marketplaces with different scales. The original rating stays alongside. `review.received` arrives, and you can reply when the marketplace accepts it.

## Account health

Each marketplace measures the seller: delays, cancellations, claims. The Connection sums this up in `level`: `good`, `warning`, `at_risk`, `restricted` or `unknown`. With it come the metrics, the thresholds and penalties with a deadline. The reading refreshes every 6 hours. When the level changes, `connection.health_changed` arrives.

## Endpoints in this phase

| To               | Endpoint                                                                                                                          |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Answer questions | [List](/docs/questions/list) · [Get](/docs/questions/get) · [Answer](/docs/questions/answer) · [Actions](/docs/questions/actions) |
| Talk             | [List](/docs/conversations/list) · [Messages](/docs/conversations/list-messages) · [Send](/docs/conversations/send-message)       |
| Reply to reviews | [List](/docs/reviews/list) · [Get](/docs/reviews/get) · [Reply](/docs/reviews/reply)                                              |
| See health       | [Account health](/docs/connections/health)                                                                                        |

**Webhooks in this phase:** `question.received`, `message.received`, `review.received` and `connection.health_changed`.

---

# Grow and track

URL: https://marketapi.shop/docs/how-it-works/commerce

> Promotions, price, fees, Ads, metrics and finance.

With the catalog published and sales going, the seller wants to sell more and know how much they made. This phase covers both.

## 1. Promotions

A promotion is a discount with a deadline on a Connection. It comes from two places:

* **From the seller** (`origin: seller`): you create it through the API. Discount, coupon, buy-more-pay-less, flash sale.
* **From the marketplace** (`origin: marketplace`): a campaign invitation. You only decide whether to join, until `joinDeadlineAt`.

Each offer joins the promotion through an **entry**, with its promotional price, the discount in bps and, if you want, a stock limit. Before adding an offer, look at its **eligibilities**: they say which promotions the offer can join and why it cannot join the others.

`sellerShareBps` says how much of the discount the seller pays. `5000` means half.

**Renewal.** No marketplace renews a promotion on its own. With a renewal rule, marketapi creates the promotion again before it ends, with the same entries. `promotion.renewed` arrives.

## 2. Price and fees

* **Estimate fees:** send a price and get, in cents, the commission, the fixed fee, shipping and the net. It is a job. Use it before publishing a new price.
* **Competitive price:** where several sellers sell the same product, see whether the offer is winning, the price to win and the suggested price.

## 3. Ads

The sponsored ad campaigns of each Connection show up in one format: impressions, clicks, spend (`spendInCents`), CTR and ROAS, both in bps. You can pause, activate or change the budget, when the marketplace allows it. marketapi syncs the numbers every day, and you can ask for a sync of a period.

The marketplace revises the numbers of the last few days. So a recent day may change.

## 4. Metrics

Per product and per offer: visits, units sold, revenue (`revenueInCents`) and conversion (`conversionBps`). Every series has `period`, `granularity` (day, week or month) and `timezone`. Sales are computed from the imported orders, without cancelled orders.

`null` is not zero: it means the marketplace does not report that number.

## 5. Finance

What the marketplace paid and charged the seller:

| Resource       | What it shows                                                      |
| -------------- | ------------------------------------------------------------------ |
| Transactions   | each credit and debit, with marketapi's type and the marketplace's |
| Settlements    | the money that landed in the seller's account                      |
| Fee statements | the fees charged per period                                        |
| Balances       | the available and pending balance at the marketplace               |

The data arrives once a day. Use it to **reconcile**, not to decide on the spot: an entry from today may only show up tomorrow. A credit is positive, a debit is negative.

## Endpoints in this phase

| To                          | Endpoint                                                                                                                                                                     |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Create promotions           | [List](/docs/promotions/list) · [Create](/docs/promotions/create) · [Replace entries](/docs/promotions/replace-entries)                                                      |
| Put an offer in a promotion | [Eligibilities](/docs/promotions/list-eligibilities) · [Offer's promotions](/docs/promotions/list-for-listing) · [Create for the offer](/docs/promotions/create-for-listing) |
| Renew                       | [Renewal rules](/docs/promotion-renewals/overview) · [Create rule](/docs/promotion-renewals/create)                                                                          |
| Price                       | [Estimate fees](/docs/pricing/fee-estimates) · [Competitive price](/docs/pricing/competitive-prices)                                                                         |
| Ads                         | [Campaigns](/docs/ad-campaigns/list) · [Metrics](/docs/ad-campaigns/metrics) · [Actions](/docs/ad-campaigns/actions) · [Sync](/docs/ad-campaigns/metric-syncs)               |
| Metrics                     | [Product](/docs/metrics/product-metrics) · [Offer](/docs/metrics/listing-metrics) · [Visits](/docs/metrics/listing-visits)                                                   |
| Finance                     | [Transactions](/docs/finance/transactions) · [Settlements](/docs/finance/settlements) · [Fees](/docs/finance/fee-statements) · [Balances](/docs/finance/balances)            |

**Webhooks in this phase:** `promotion.created`, `promotion.updated`, `promotion.entry_updated` and `promotion.renewed`. Ads, metrics and finance have no webhook: read them when you need them.

---

# Capabilities

URL: https://marketapi.shop/docs/how-it-works/capabilities

> How to know what each Connection can do, and what happens when it cannot.

Each marketplace does different things. One takes coupons, another does not. One lets the seller start a conversation, another only lets them reply. marketapi does not hide this: each Connection tells you, operation by operation, what it can do.

## One capability per operation

Every route that talks to the marketplace depends on a **capability**: `listings.publish`, `inventory.publish`, `orders.import`, `invoices.submit`, `questions.answer`, `promotions.manage` and so on. Read the Connection's capabilities before showing an action in your app:

| `status`      | Means                                                  | What the route answers       |
| ------------- | ------------------------------------------------------ | ---------------------------- |
| `available`   | the Connection can                                     | goes on as usual             |
| `unsupported` | the marketplace does not offer it                      | `422 CAPABILITY_UNSUPPORTED` |
| `not_granted` | the marketplace offers it, but a permission is missing | `403 CAPABILITY_NOT_GRANTED` |

The error comes **before** the job, with `details.marketplace` and `details.capability`. On a batch route that touches several Connections, the job is created and only the item of the Connection without the operation fails.

With `not_granted`, ask the seller to connect again, granting the missing permission.

## Marketplace facts

Besides operations, the Connection carries `declarations`: facts that change how an operation behaves.

| Declaration                               | What it says                                                     |
| ----------------------------------------- | ---------------------------------------------------------------- |
| `inventory.scope`                         | whether stock belongs to each offer line or is shared by offers  |
| `inventory.kitStockComputedByMarketplace` | whether the marketplace computes a kit's stock on its own        |
| `listings.offersPerMarketplaceProduct`    | whether the same content takes one offer or many                 |
| `invoices.scope`                          | at which level the NF-e is delivered: order, package or delivery |
| `timezone`                                | the time zone of dates the marketplace sends without one         |
| `pii.retentionMaxDays`                    | how long buyer data may be kept                                  |

## What only one marketplace has

Part of each marketplace has no match in the others: its own shipping type, a price automation. For that there are two paths, both safe:

* **`marketplaceOptions`:** options of one marketplace, kept per marketplace, on the listing and on the Connection. They go along with the publication.
* **Native operations:** the Connection lists the marketplace's own operations that marketapi exposes, with the effect and permission of each. You run them in a job. It is not a proxy: only what is on the list exists.

## Call limits

Each marketplace limits how many calls per minute an app can make. marketapi respects that limit for you. When the quota runs out, the job waits and goes on later. It does not fail because of that.

## In the documentation

Every endpoint page that talks to the marketplace shows, at the top, the support of each marketplace. The full table is in [Marketplace support](/docs/marketplace-support). It is the product overview. The decision for a specific Connection is always the answer of its capabilities.

## Endpoints in this phase

| To                          | Endpoint                                                                                                                    |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| See what the account can do | [Capabilities](/docs/connections/capabilities)                                                                              |
| Marketplace options         | [On the Connection](/docs/connections/marketplace-options) · [On the listing](/docs/listings/update-marketplace-options)    |
| Native operations           | [List](/docs/connections/list-operations) · [Get](/docs/connections/get-operation) · [Run](/docs/connections/run-operation) |

---

# Data model

URL: https://marketapi.shop/docs/data-model

> Every resource in the API, how they connect and the rules that apply to all of them.

This section shows the whole data model: what each resource represents, who owns it and how it connects to the others. The detail of each field and endpoint is on each resource's pages, in the **API** group of the sidebar.

## The map

```mermaid
erDiagram
  accTitle: Data model map
  Account ||--o{ Seller : "has"
  Seller ||--o{ Connection : "connects"
  Seller ||--o{ Product : "catalogs"
  Seller ||--o{ Warehouse : "has"
  Seller ||--o{ LinkSession : "opens"
  Connection }o--o{ LinkSession : "created or renewed in"
  Connection ||--o{ MarketplaceProduct : "has the content"
  Connection ||--o{ Listing : "lists"
  MarketplaceProduct ||--o{ MarketplaceProductVariant : "has"
  MarketplaceProduct ||--o{ Listing : "is offered in"
  Product ||--|{ Variant : "has"
  Product ||--o{ ProductComponent : "kit: recipe"
  Listing ||--o{ VariantBinding : "has lines"
  MarketplaceProductVariant ||--o{ VariantBinding : "is sold in"
  Variant ||--o{ MarketplaceProductVariant : "appears in"
  Variant ||--o{ ProductComponent : "goes into"
  Variant ||--o{ InventoryMovement : "records"
  Variant ||--o{ InventoryBalance : "has a balance in"
  Warehouse ||--o{ InventoryBalance : "holds"
```

**How to read the lines.** Each line connects two resources, and its end says how many exist on that side:

| End of the line            | Means        |
| -------------------------- | ------------ |
| two bars                   | exactly one  |
| a circle and a bar         | zero or one  |
| a circle and a crow's foot | zero or more |
| a bar and a crow's foot    | one or more  |

Example: between `Product` and `Variant`, the end on the Variant side has a bar and a crow's foot. Read "a Product has one or more Variants". The Product side has two bars: "each Variant belongs to exactly one Product".

## The resources in one sentence

| Resource                                                                      | What it is                                                                                             | Belongs to                                         | Who creates it                                                                        |
| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | ------------------------------------------------------------------------------------- |
| **Account**                                                                   | Your company. It has the dashboard login and the API keys.                                             | —                                                  | you, in the dashboard                                                                 |
| [**Seller**](/docs/sellers/overview)                                          | Your customer: the merchant. They have no marketapi login.                                             | Account                                            | `POST /v1/sellers`                                                                    |
| [**Link session**](/docs/link-sessions/overview)                              | The page where the Seller connects a marketplace. It lasts up to 5 minutes.                            | Seller                                             | `POST /v1/sellers/{id}/link-sessions`                                                 |
| [**Connection**](/docs/connections/overview)                                  | One of the Seller's marketplace accounts, such as a Mercado Livre account.                             | Seller                                             | the Seller, by granting access in the link session                                    |
| [**Product**](/docs/products/overview)                                        | The base record: what the Seller sells. No price and no marketplace.                                   | Seller                                             | `POST /v1/sellers/{sellerId}/products`                                                |
| [**Variant**](/docs/products/overview)                                        | A sellable combination of the Product (color, size). It has its own SKU, GTIN and stock.               | Product                                            | the API creates the first one; the others, `POST /v1/products/{productId}/variants`   |
| [**Component**](/docs/products/list-components)                               | One line of a kit's recipe: which Variant and how many units.                                          | Product (the kit)                                  | `PUT /v1/products/{productId}/components`                                             |
| [**Warehouse**](/docs/warehouses/overview)                                    | Where the Seller keeps the goods. Every Seller starts with a default warehouse.                        | Seller                                             | the API creates the default one; the others, `POST /v1/sellers/{sellerId}/warehouses` |
| [**Inventory movement**](/docs/inventory/overview)                            | One stock entry or exit in a warehouse. It never changes after it is created.                          | Variant and warehouse                              | `POST /v1/sellers/{sellerId}/inventory-movements`                                     |
| [**Inventory balance**](/docs/inventory/balance)                              | The Variant's balance in each warehouse, computed from the movements. The Variant sums the warehouses. | Variant and warehouse                              | the API                                                                               |
| [**MarketplaceProduct**](/docs/marketplace-products/overview)                 | The content of an item on the Connection: title, attributes, media. Shared between offers.             | Connection                                         | `POST /v1/sellers/{sellerId}/listings` (creates it), a clone or an import             |
| [**Marketplace product variation**](/docs/marketplace-products/list-variants) | The variation of the content: links the content to a Variant and keeps the variation id there.         | MarketplaceProduct                                 | `PUT /v1/marketplace-products/{id}/variants`, the binding or an import                |
| [**Listing**](/docs/listings/overview)                                        | The offer on a Connection: price and limits, on top of a MarketplaceProduct.                           | Connection                                         | `POST /v1/sellers/{sellerId}/listings` or an import                                   |
| [**Variant binding**](/docs/listings/list-bindings)                           | One line of the offer: it sells a variation of the content, with the line id there.                    | Listing                                            | `PUT /v1/listings/{listingId}/variant-bindings` or an import                          |
| [**Job**](/docs/jobs/overview)                                                | Background work: publishing, importing, sending stock.                                                 | Account (and the Seller, when the work is for one) | the API, when the operation talks to the marketplace                                  |
| [**Webhook endpoint**](/docs/webhooks/overview)                               | A URL of yours that receives the events.                                                               | Account                                            | `POST /v1/webhooks/endpoints`                                                         |
| [**Webhook delivery**](/docs/webhooks/list-deliveries)                        | One delivery of an event to an endpoint, with its attempts.                                            | Webhook endpoint                                   | the API, for every event                                                              |

## Two sides that meet at the offer

The model has two sides. The **catalog** says *what* the Seller sells. The **channels** say *where* they sell it. The channel side keeps no product and no stock: the content (MarketplaceProduct) points to the catalog's Variants through its variations, and the offer (Listing) sells those variations through the **variant binding**.

```mermaid
flowchart TB
  accTitle: Catalog and channels
  subgraph catalog["Catalog: what the Seller sells"]
    P["Product"] --> V["Variant"]
    V --> E["Stock<br/>(warehouses, movements and balance)"]
  end
  subgraph channels["Channels: where the Seller sells"]
    C["Connection"] --> MP["MarketplaceProduct<br/>(content)"]
    MP --> L["Listing<br/>(offer)"]
  end
  V ---|"variation"| MP
```

That is why:

* **One Product can be in many listings**, on many marketplaces, or in two offers of the same content to test a price.
* **There is one stock.** It belongs to the Variant, not to the offer, and sits in the Seller's warehouses. Every movement you record changes the same balance, whichever offer the sale came from.
* **The marketplace never changes the catalog.** What changes out there is kept in the MarketplaceProduct and in the Listing (`observed`), and the Product stays as you left it.

## Around it: jobs and webhooks

Two pieces follow everything else and belong to the Account:

```mermaid
erDiagram
  accTitle: Jobs and webhooks
  direction LR
  Account ||--o{ Job : "tracks"
  Job ||--o{ JobItem : "has"
  Account ||--o{ WebhookEndpoint : "registers"
  WebhookEndpoint ||--o{ WebhookDelivery : "receives"
  Event ||--o{ WebhookDelivery : "becomes"
```

* **Job:** every operation that talks to the marketplace answers right away with a Job and goes on in the background. Each Job has items, one per resource. See [Tracking jobs](/docs/tracking-jobs).
* **Event and delivery:** every change that matters to you becomes an event. Each event becomes one delivery for each endpoint subscribed to it. See [Receiving webhooks](/docs/receiving-webhooks).

## Rules that apply to everything

**The Account isolates everything.** An id from another Account returns `404`, the same as an id that does not exist.

**The Seller is the owner.** Products, Variants, warehouses, MarketplaceProducts, Listings, Connections, link sessions and inventory movements belong to a Seller. A resource only connects to another one of the same Seller: a listing never points to another Seller's Variant.

**Where the Seller goes in the request:**

| Operation                               | Where the Seller goes                 | Example                                          |
| --------------------------------------- | ------------------------------------- | ------------------------------------------------ |
| Create something for the Seller         | in the path                           | `POST /v1/sellers/{sellerId}/products`           |
| List connections or inventory movements | in the `Seller-Id` header             | `GET /v1/connections` with `Seller-Id: …`        |
| Read, change or delete by id            | nowhere: the id is enough             | `GET /v1/listings/{listingId}`                   |
| List products, listings or jobs         | nowhere: the whole Account comes back | `GET /v1/products`; each item has its `sellerId` |

**Ids.** Every resource's `id` is a UUID generated by the API. The Seller also takes an `externalId`: their id in your system. Marketplace ids arrive in their own fields: `externalAccountId` on the Connection, `externalId` on the MarketplaceProduct, `externalVariantId` on the variation, `externalId` and `externalSku` on the binding, `observed.marketplaceId` on the Listing.

**Units.** Money is always in cents, as an integer, next to the currency: `priceInCents: 799900` with `currency: "BRL"` is R$ 7,999.00. Weight in grams (`weightInGrams`) and dimensions in millimeters (`dimensionsInMm`), also integers. Dates in ISO 8601 with a time zone.

**Delete or archive.**

* **Seller:** `DELETE` removes it for good, and only when it has no active connection (otherwise `409` with `details.reason = "active_connections"`). There is no trash.
* **Product and Variant:** `DELETE` archives. The history and the inventory movements stay.
* **Warehouse:** `DELETE` archives. It does not archive the default one or a warehouse with balance (`409`, `details.reason = "warehouse_in_use"`).
* **MarketplaceProduct:** it has no `DELETE`. Without an offer, it keeps existing.
* **Listing:** `DELETE` takes `publication`, which decides whether the listing also leaves the marketplace.

## The example in this guide

The next pages always use the same case. **Acme Store** is one of your Sellers. It connected a Mercado Livre account and sells:

* an **iPhone 16** in two colors, black and white, both with 128 GB;
* a **USB-C cable** and a **20W charger**, each sold on its own;
* a **charging kit**, made of 2 cables and 1 charger.

**The catalog.** Four Products. The iPhone has two Variants. The others only have the default Variant. The kit consumes Variants of the other Products:

```mermaid
flowchart LR
  accTitle: Acme Store catalog
  P1["Product<br/>iPhone 16"] --> V1["Variant<br/>black · 128 GB"]
  P1 --> V2["Variant<br/>white · 128 GB"]
  P2["Product<br/>USB-C cable"] --> V3["Variant<br/>default"]
  P3["Product<br/>20W charger"] --> V4["Variant<br/>default"]
  PK["Product<br/>Charging kit"] --> VK["Variant<br/>kit default"]
  PK -. "component: 2 ×" .-> V3
  PK -. "component: 1 ×" .-> V4
```

**The listings.** Two MarketplaceProducts and two Listings on the Mercado Livre Connection. Each content points, through its variations, to the Variants it sells, and each Listing is the offer of its content:

```mermaid
flowchart LR
  accTitle: Acme Store listings
  C["Connection<br/>Mercado Livre"] --> MA["MarketplaceProduct<br/>iPhone 16"]
  C --> MK["MarketplaceProduct<br/>Charging kit"]
  MA --> LA["Listing<br/>iPhone 16 128 GB"]
  MK --> LK["Listing<br/>Charging kit"]
  MA -. variation .-> V1["Variant<br/>black · 128 GB"]
  MA -. variation .-> V2["Variant<br/>white · 128 GB"]
  MK -. variation .-> VK["Variant<br/>kit default"]
```

In both drawings, a solid line means "has". A dashed line points to a Variant: a variation of the content (the lines of the offer, the bindings, sell those variations), or a component of the kit with how many units it consumes.

## Where to go next

<Cards>
  <Card title="Catalog and inventory" href="/docs/data-model/catalog-and-inventory">
    Product, Variant, kit and how the balance is computed.
  </Card>

  <Card title="Connections and listings" href="/docs/data-model/connections-and-listings">
    Connection, MarketplaceProduct, Listing, binding and where each published
    field comes from.
  </Card>

  <Card title="States and events" href="/docs/data-model/states-and-events">
    The lifecycle of each resource and the webhook for each change.
  </Card>
</Cards>

<Callout title="Integrating with an AI assistant?">
  The whole documentation is in plain text, in one file, at
  [`/llms-full.txt`](/llms-full.txt), with the diagrams in Mermaid format. The
  index is at [`/llms.txt`](/llms.txt).
</Callout>

---

# Catalog and inventory

URL: https://marketapi.shop/docs/data-model/catalog-and-inventory

> Product, Variant, kit and how each Variant's balance is computed.

The catalog says **what** the Seller sells. It knows nothing about marketplaces: nothing here has a selling price, a listing or an external account. Those live in the [listings](/docs/data-model/connections-and-listings).

## The resources and their main fields

```mermaid
erDiagram
  accTitle: Catalog and inventory resources
  Product ||--|{ Variant : "has"
  Product ||--o{ ProductComponent : "kit: recipe"
  Variant ||--o{ ProductComponent : "goes into"
  Variant ||--o{ InventoryMovement : "records"
  Variant ||--o{ InventoryBalance : "has a balance in"
  Warehouse ||--o{ InventoryBalance : "holds"
  Warehouse ||--o{ InventoryMovement : "receives"
  Product {
    uuid id
    uuid sellerId
    enum status "active, archived"
    object content "title, description"
    object identity "brand, model"
    object physical "grams and mm"
  }
  Variant {
    uuid id
    uuid productId
    string sku "may be null"
    string gtin "may be null"
    object options "unique in the Product"
    enum inventoryAuthority
    boolean allowBackorder
    int costInCents "cost, not price"
  }
  ProductComponent {
    uuid componentVariantId
    int quantity "units per kit"
  }
  Warehouse {
    uuid id
    uuid sellerId
    string name
    string externalId "may be null"
    boolean isDefault "one per seller"
    enum status "active, archived"
  }
  InventoryMovement {
    uuid id
    uuid variantId
    uuid warehouseId
    enum type
    int quantity
    uuid transferId "transfers only"
    string reason
  }
  InventoryBalance {
    uuid variantId
    uuid warehouseId
    int onHand
    int reserved
    int available
  }
```

| Resource              | In one sentence                                                               | Endpoints                                                                                     |
| --------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| **Product**           | What the Seller sells, with the data shared by every variation.               | [Create](/docs/products/create), [list](/docs/products/list), [update](/docs/products/update) |
| **Variant**           | A sellable combination. It is the one with the SKU, GTIN and stock.           | [Create](/docs/products/create-variant), [list](/docs/products/list-variants)                 |
| **ProductComponent**  | One line of a kit's recipe (BOM).                                             | [Read](/docs/products/list-components), [replace](/docs/products/replace-components)          |
| **Warehouse**         | Where the Seller keeps the goods. Every Seller starts with a default one.     | [Create](/docs/warehouses/create), [list](/docs/warehouses/list)                              |
| **InventoryMovement** | One stock entry or exit in a warehouse. It never changes after it is created. | [Record](/docs/inventory/create-movement), [list](/docs/inventory/list-movements)             |
| **InventoryBalance**  | The Variant's balance in each warehouse, computed by the API.                 | [Read the balance](/docs/inventory/balance)                                                   |

## Product: what exists

The Product keeps what is the same across every variation: title and description, brand and model, typed attributes, item and package measurements, Brazilian fiscal data, compliance declarations and media. Texts are per language, such as `{ "en-US": "iPhone 16" }`.

The Product does **not** keep a selling price or stock. The price belongs to the listing, and the stock belongs to the Variant.

## Variant: what is sold

The Variant is the unit the buyer receives: the **black** 128 GB iPhone 16. It has its own SKU, GTIN and balance. It can also override the Product's content, attributes, dimensions and media, only where they differ.

* **Every Product is born with a default Variant**, with `options: {}`, no SKU and `inventoryAuthority: "disabled"`. A product with no variations only uses that one. To record stock on it, set `inventoryAuthority` to `marketapi` ([update Variant](/docs/products/update-variant)).
* **`options` is unique within the Product.** Two Variants cannot have the same combination (`409`, `details.reason = "variant_options_taken"`), and key order does not matter. Since the default one already takes `{}`, a product with colors works like this: give `options` to the default one (`PATCH`) and create the others.
* **`sku` and `gtin` are used to match imported listings** to your catalog. The SKU does not have to be unique in the API, but a repeated SKU gets in the way of that match.
* **`costInCents` is cost, not price.** It goes together with `costCurrency`.

```json
{
  "id": "6f1c…",
  "productId": "2b7e…",
  "sku": "IP16-BLK-128",
  "gtin": "0195949000000",
  "options": { "color": "black", "storage": "128 GB" },
  "inventoryAuthority": "marketapi",
  "allowBackorder": false,
  "costInCents": 520000,
  "costCurrency": "BRL"
}
```

## Kit: a Product made of other Variants

A kit is a regular Product with a **recipe** (BOM): the list of Variants it consumes and how many units of each. The recipe belongs to the Product. For the kit's balance to come from it, the kit's Variant uses `inventoryAuthority: "derived"`.

```mermaid
flowchart TB
  accTitle: Charging kit balance
  K["Charging kit<br/>Variant derived<br/>available = 3"]
  K -- "2 per kit" --> C["USB-C cable<br/>available = 10<br/>enough for 5 kits"]
  K -- "1 per kit" --> R["20W charger<br/>available = 3<br/>enough for 3 kits"]
```

The kit's balance is **the smallest number of kits each component can build**: `min(⌊10 ÷ 2⌋, ⌊3 ÷ 1⌋) = min(5, 3) = 3`. The charger is the bottleneck.

The calculation is done **per warehouse**, and the warehouses are summed at the end. A kit is not assembled from parts that sit in two places: if the 3 chargers are in another warehouse, with no cables, the kit is worth 0 in both.

Recipe rules:

* [`PUT /v1/products/{productId}/components`](/docs/products/replace-components) replaces the whole recipe at once, with up to 100 lines. Each Variant appears only once.
* Components are Variants of the **same Seller**.
* A kit can contain another kit, but it cannot contain itself, not even through an indirect path (`409`, `details.reason = "component_cycle"`).
* The kit **takes no movement of its own**. It changes when its components change.
* An archived component, or one whose stock is neither `marketapi` nor `derived`, counts as zero.

## Inventory: who owns the balance

Each Variant chooses, in `inventoryAuthority`, who is the source of truth for its balance:

| `inventoryAuthority` | Who owns the balance             | Takes movements? | Sellable quantity            |
| -------------------- | -------------------------------- | ---------------- | ---------------------------- |
| `marketapi`          | marketapi, through the movements | yes              | `onHand − reserved`          |
| `derived`            | the kit's recipe                 | no               | computed from the components |
| `managed_externally` | your ERP or WMS                  | no               | not kept in marketapi        |
| `disabled`           | nobody                           | no               | none                         |

A movement on a Variant that is not `marketapi` returns `409` with `details.reason = "inventory_authority"`.

## Inventory: warehouses

The balance is not one number per Variant: it is one per **warehouse**. A warehouse is the place where the Seller keeps the goods, such as headquarters and a branch.

* **Every Seller starts with a default warehouse** (`isDefault: true`), created together with it. Only one warehouse is the default. To change it, mark another one with `isDefault: true` ([update warehouse](/docs/warehouses/update)).
* **A movement without `warehouseId` goes to the default warehouse.** With `warehouseId`, it goes to the chosen warehouse, which must belong to the same Seller as the Variant (otherwise `404`).
* **`DELETE` archives.** An archived warehouse takes no movement (`409`, `details.reason = "warehouse_archived"`) and leaves the balance list. The API refuses to archive the default or a warehouse with balance (`409`, `details.reason = "warehouse_in_use"`, with `details.uses`).
* **The Variant balance is the sum of the warehouses.** `GET /v1/variants/{variantId}/inventory` returns the totals and the `warehouses` list, one line per active warehouse.

## Inventory: movements and balance

With `marketapi`, the balance comes from a **ledger**: a list of movements that never change. Each movement changes one warehouse. The warehouse balance is the sum of its movements.

```mermaid
flowchart TB
  accTitle: From movement to marketplace
  M["InventoryMovement<br/>entries and exits"] -- "added up per warehouse" --> B["InventoryBalance<br/>onHand and reserved<br/>per warehouse"]
  B -- "available = onHand − reserved<br/>summed across warehouses" --> A["Sellable quantity"]
  A -- "through the bindings" --> L["Listings on the marketplace"]
```

Each movement type changes the balance in its own way:

| `type`        | `quantity`           | Effect on the balance                                   | What it is for                           |
| ------------- | -------------------- | ------------------------------------------------------- | ---------------------------------------- |
| `adjustment`  | positive or negative | `onHand` goes up or down                                | count, loss, correction                  |
| `transfer`    | positive             | `onHand` goes down at the source, up at the destination | move units from one warehouse to another |
| `return`      | positive             | `onHand` goes up                                        | customer return                          |
| `reservation` | positive             | `reserved` goes up                                      | open order, not paid yet                 |
| `release`     | positive             | `reserved` goes down                                    | order canceled before the sale           |
| `sale`        | positive             | `reserved` and `onHand` go down                         | confirmed sale of something reserved     |

Example with the black iPhone Variant, from zero:

| Movement         | `onHand` | `reserved` | `available` |
| ---------------- | -------- | ---------- | ----------- |
| `adjustment` +10 | 10       | 0          | 10          |
| `reservation` 2  | 10       | 2          | 8           |
| `sale` 2         | 8        | 0          | 8           |
| `return` 1       | 9        | 0          | 9           |

### Transfer between warehouses

A `transfer` is not a signed adjustment. It takes `warehouseId` (the source), `toWarehouseId` (the destination) and a positive quantity, and writes **two rows** in the ledger with the same `transferId`: an outbound at the source and an inbound at the destination. Either both are written or neither is.

Example: the black iPhone has 10 in the default warehouse and 0 in the branch. A transfer of 4:

| Movement              | Warehouse | `quantity` | Warehouse `onHand` | Variant `onHand` |
| --------------------- | --------- | ---------- | ------------------ | ---------------- |
| `transfer` (outbound) | default   | -4         | 6                  | 10               |
| `transfer` (inbound)  | branch    | +4         | 4                  | 10               |

The Variant total does not change, so a transfer **does not publish quantity by itself**. Whoever sends `publishTargets` still gets the job.

Balance rules:

* `available` is what can be sold. Do not use `onHand` for that.
* Each warehouse has its own balance, and the rules below hold in each one. Release (`release`) and take out (`sale`) in the same warehouse where you reserved.
* `reserved` never goes negative. That is why `sale` only takes out what was already reserved. For a sale with no earlier reservation, record `reservation` and then `sale`.
* `onHand` and `available` do not go negative, unless the Variant has `allowBackorder: true`.
* **Not enough stock in the warehouse, the API refuses:** `409` with `details.reason = "inventory_underflow"` and `details.warehouseId` (for a transfer, the source). It holds for every type. Nothing is written and no event is sent.
* A quantity with the wrong sign for the type (for example, a negative `sale`) is `400`.
* A movement can carry a `reference` (`{ "type": "order", "id": "ML-123" }`) so you can tie the change to the order or to your ERP.

## From the balance to the marketplace

The balance only reaches a listing through a **binding** (see [Connections and listings](/docs/data-model/connections-and-listings)). The quantity sent is the Variant's `available`, the sum of all warehouses. Sending it is always an `inventory.publish` Job, and it happens in three ways:

1. **In the movement itself:** `publishTargets` says which listings, bindings or Connections to send to.
2. **Automatic:** the listing is already published, has `syncPolicy.inventory.mode = "automatic"`, and the binding is in `eligibleBindingIds`. Every movement of the Variant triggers the send.
3. **On demand:** [`POST /v1/sellers/{sellerId}/inventory-publications`](/docs/inventory/publish) sends again whenever you want, by Variant (`variantIds`), listing, binding or Connection.

In all three, the binding must have `inventoryEligible: true`, and the Variant must be `marketapi` or `derived`: `managed_externally` and `disabled` never go out. When no binding qualifies, the movement returns `publicationJobId: null`, and the on-demand send returns `409` with `details.reason = "inventory_target_empty"`.

**Kits:** a movement on a component also counts for the kits that consume it, including a kit inside a kit. The kit's bindings go out by the same rules (automatic or `publishTargets`), with the computed balance. Publishing a kit's listing also sends that balance, unless the marketplace computes kit stock on its own.

## Archiving

`DELETE` on a Product, a Variant or a warehouse **archives** it: the resource goes out of use, and its history stays. The API refuses with `409` when archiving would break something:

| `details.reason`   | When                                                                                   | What to do                                               |
| ------------------ | -------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| `variant_in_use`   | the Variant is in a listing binding                                                    | remove the binding from the listing                      |
| `variant_in_kit`   | the Variant is a component of an active kit                                            | remove the Variant from the kit recipe                   |
| `product_in_use`   | a Variant of the Product is in a binding                                               | remove the bindings                                      |
| `product_in_kit`   | a Variant of the Product is a component of an active kit                               | remove the Variant from the recipe                       |
| `warehouse_in_use` | the warehouse is the Seller's default (`uses: default`) or has balance (`uses: stock`) | mark another one as the default, or move the balance out |

---

# Connections and listings

URL: https://marketapi.shop/docs/data-model/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](/docs/data-model/catalog-and-inventory).

## The resources and their main fields

```mermaid
erDiagram
  accTitle: Connection and listing resources
  Seller ||--o{ Connection : "connects"
  Seller ||--o{ LinkSession : "opens"
  Connection }o--o{ LinkSession : "created or renewed in"
  Connection ||--o{ MarketplaceProduct : "has the content"
  MarketplaceProduct ||--o{ MarketplaceProductVariant : "has"
  MarketplaceProduct ||--o{ Listing : "is offered in"
  Listing ||--o{ VariantBinding : "has lines"
  MarketplaceProductVariant ||--o{ VariantBinding : "is sold in"
  Variant ||--o{ MarketplaceProductVariant : "appears in"
  Connection {
    uuid id
    uuid sellerId
    slug marketplace "mercadolivre, …"
    string externalAccountId "account id there"
    enum status "active, error, revoked"
  }
  MarketplaceProduct {
    uuid id
    uuid connectionId
    object desired "content: title, attributes, media"
    object observed "what the marketplace confirmed"
    enum publicationState
    string externalId "family or parent ASIN"
  }
  MarketplaceProductVariant {
    uuid id
    uuid marketplaceProductId
    uuid variantId
    string externalVariantId "variation id there"
    int position
  }
  Listing {
    uuid id
    uuid connectionId
    uuid marketplaceProductId
    object desired "the offer: price and limits"
    object observed "what the marketplace confirmed"
    enum publicationState
    object syncPolicy
  }
  VariantBinding {
    uuid id
    uuid listingId
    uuid marketplaceProductVariantId
    uuid variantId "read-only"
    string externalId "line id there"
    boolean inventoryEligible
    object desired "offer override"
  }
```

| Resource               | In one sentence                                                                    | Endpoints                                                                                                                                                          |
| ---------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Link session**       | The page where the Seller grants access to a marketplace account.                  | [Create](/docs/link-sessions/create), [get](/docs/link-sessions/get)                                                                                               |
| **Connection**         | One of the Seller's marketplace accounts, with the credential we keep.             | [List](/docs/connections/list), [capabilities](/docs/connections/capabilities)                                                                                     |
| **MarketplaceProduct** | The content of an item on the Connection, shared between offers.                   | [Get](/docs/marketplace-products/get), [update](/docs/marketplace-products/update), [variations](/docs/marketplace-products/replace-variants)                      |
| **Listing**            | The offer on a Connection: price and limits, on top of a MarketplaceProduct.       | [Create](/docs/listings/create), [update](/docs/listings/update), [publish](/docs/listings/publish)                                                                |
| **Variant binding**    | One line of the offer: the content variation the Listing sells, with its id there. | [List](/docs/listings/list-bindings), [replace](/docs/listings/replace-bindings), [update](/docs/listings/update-binding), [remove](/docs/listings/remove-binding) |

## Connection: the Seller's account on a marketplace

A Connection is born when the Seller grants access in a [link session](/docs/connecting-a-seller). 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: the `id` does not change and it goes back to `active`.
* **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](/docs/connections/capabilities) before you show a button to import, publish or send stock. An unsupported operation returns `422`; one the Seller did not grant returns `403`.
* **Only an `active` Connection works.** Creating a listing, importing or reconciling on a Connection in another state returns `409` with `details.reason = "connection_inactive"`.
* **The import rule belongs to the Connection** (`listingImportPolicy`), because each account can be run differently. See [Importing listings that already exist](#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 a `publicationState`, like the Listing. The content you want to publish is in `desired.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.media` and `desired.gtin`. The order of the map (`position`) is the order of the last `PUT`.
* **You never create a MarketplaceProduct on its own.** It is born with the [Listing](/docs/listings/create), through a [clone](/docs/listings/clone) or through an import. Changing the content is [`PATCH /v1/marketplace-products/{id}`](/docs/marketplace-products/update); replacing the variations is [`PUT …/variants`](/docs/marketplace-products/replace-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`](/docs/listings/create)) **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.offersPerMarketplaceProduct` is `one` in the [capabilities](/docs/connections/capabilities)). There, the second offer returns `422 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:

```mermaid
flowchart LR
  accTitle: One Variant in three listings
  A["Listing A<br/>Mercado Livre<br/>R$ 7,999.00"] -. binding .-> V["Variant<br/>black · 128 GB<br/>available = 8"]
  B["Listing B<br/>Mercado Livre<br/>R$ 7,799.00"] -. binding .-> V
  C["Listing C<br/>another Connection"] -. binding .-> V
```

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](/docs/data-model/catalog-and-inventory#from-the-balance-to-the-marketplace) turned on, the new balance goes on to the others. To create Listing B from A, [clone](/docs/listings/clone) it.

## The three versions

The content and the offer each have their own versions, and each version has an owner:

```mermaid
flowchart TB
  accTitle: The three versions of a listing
  APP["Your application"] -- "PATCH" --> D["desired<br/>what you want to publish:<br/>content on the MarketplaceProduct,<br/>offer on the Listing"]
  MP["Marketplace"] -- "import, reconciliation<br/>and notifications" --> O["observed<br/>what the marketplace confirmed"]
  D --> R["resolved<br/>what would be sent now"]
  CAT["Variant and Product"] --> R
```

| 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`](/docs/listings/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`](/docs/listings/resolved) of the Listing; [of the content](/docs/marketplace-products/resolved) |

Three rules follow:

* **`PATCH` never publishes.** It changes `desired` and marks the resource as changed (`publicationState: "dirty"`). Publishing is always your request: [`POST /v1/listings/{listingId}/publish`](/docs/listings/publish).
* **The marketplace never changes your catalog.** A change made out there goes to `observed` and reaches you through the `listing.external_changed` webhook (offer) or `marketplace_product.updated` with `behavior: "external_changed"` (content). Product and Variant stay as they were.
* **Missing means "inherit".** A field missing from `desired` comes 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**:

```mermaid
flowchart TB
  accTitle: Inheritance order
  B["1 · MarketplaceProduct variation<br/>only this variation"]
  L["2 · MarketplaceProduct.desired.content<br/>every offer of this content"]
  V["3 · Variant<br/>this variation, on any marketplace"]
  P["4 · Product<br/>every variation"]
  B -- "does not have the field" --> L
  L -- "does not have the field" --> V
  V -- "does not have the field" --> P
```

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 `content` and `attributes`, and in the offer. The title can come from the MarketplaceProduct, and the description from the Product.
* **`media` and `physical` come whole** from a single level. Photos from two levels are never mixed. An empty `media: []` counts as "no override" and inherits from the level below.
* **`physical`** comes from the Variant, otherwise from the Product.
* **Product and Variant keep text per language.** Inheritance uses the first language stored.
* **There is no `subtitle`** in the result.

The response of [`GET /v1/listings/{listingId}/resolved`](/docs/listings/resolved) shows each value with its origin in `source`: `binding`, `listing`, `marketplace_product_variant`, `marketplace_product`, `variant` or `product`.

```json
{
  "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-bindings`](/docs/listings/replace-bindings) replaces **all** lines at once, up to 100. Send `[]` to unlink the offer from the catalog.
* Each line carries `marketplaceProductVariantId` **or** `variantId`. With `variantId`, the API creates the missing variation on the MarketplaceProduct, marks the MarketplaceProduct `dirty`, and announces it with `marketplace_product.updated` (`behavior: "variants_changed"`).
* [`PATCH`](/docs/listings/update-binding) and [`DELETE`](/docs/listings/remove-binding) on `…/variant-bindings/{bindingId}` touch **one** line, keeping its id. Both leave the Listing `dirty`.
* 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.eligibleBindingIds` on its own.

## Importing listings that already exist

When the Seller already has listings on the marketplace, the [import](/docs/connections/import-listings) 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 bindings `PUT`.
* **The created Product is minimal**: a placeholder title, `metadata.imported: true` and Variants with `disabled` stock. Fill in the record later.
* **Imported content arrives in the MarketplaceProduct's `observed`**, with `publicationState: "clean"`. Nothing becomes `desired` by 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 returns `409` with `details.reason = "outbound_disabled"`.
* To fetch again what changed out there, use [reconciliation](/docs/connections/reconcile-listings). marketapi also reconciles on its own every 6 hours; the [synchronization state](/docs/connections/sync-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](/docs/marketplace-products/publish).

How each one changes is in [States and events](/docs/data-model/states-and-events).

---

# States and events

URL: https://marketapi.shop/docs/data-model/states-and-events

> The lifecycle of each resource, what changes each state and the webhook for each change.

This page shows how each resource changes state. In each diagram, a box is a state and an arrow says what causes the change. The webhook that tells you comes in parentheses. The full list of events is in [Receiving webhooks](/docs/receiving-webhooks).

## Link session

```mermaid
stateDiagram-v2
  accTitle: Link session states
  [*] --> pending: POST /v1/sellers/{id}/link-sessions
  pending --> completed: the Seller clicks Finish (link_session.completed)
  pending --> revoked: DELETE /v1/link-sessions/{id}
  pending --> expired: expiresAt has passed
  completed --> [*]
  revoked --> [*]
  expired --> [*]
```

* The three final states never change again. To connect again, create another session.
* Only a `pending` session can be revoked. In the other states, `DELETE` returns `409`.
* The session lasts up to 5 minutes (`expiresAt`). A Connection granted before that keeps working after the session expires.

## Connection

```mermaid
stateDiagram-v2
  accTitle: Connection states
  [*] --> active: the Seller grants access (connection.created)
  active --> error: renewing the access failed (connection.updated)
  active --> revoked: DELETE, or the marketplace cut the access (connection.revoked)
  error --> revoked: DELETE (connection.revoked)
  error --> active: the Seller grants access again (connection.updated)
  revoked --> active: the Seller grants access again (connection.updated)
```

* **`active`:** working. The API renews the access on its own before it expires.
* **`error`:** renewing failed, and we do not know whether the access ended. Ask the Seller to grant access again in a link session.
* **`revoked`:** the access ended. `revokedReason` says who cut it: `client` (you) or `marketplace`. The credential is deleted.
* **Granting access again reuses the Connection.** The `id` is the same, and `connection.updated` brings the previous state in `previousStatus`.

## Listing: `publicationState`

The offer's `publicationState` answers: "has what changed here gone out to the marketplace yet?" The content has its own, right below.

```mermaid
stateDiagram-v2
  accTitle: Listing publication states
  [*] --> never_published: create or clone
  [*] --> clean: import
  never_published --> dirty: PATCH or PUT of bindings
  clean --> dirty: PATCH or PUT of bindings
  failed --> dirty: PATCH or PUT of bindings
  never_published --> pending: POST publish
  dirty --> pending: POST publish
  clean --> pending: POST publish, or DELETE with removal
  failed --> pending: POST publish, or DELETE with removal
  pending --> clean: the job finished well
  pending --> failed: the job failed (listing.updated publication_failed)
  pending --> dirty: PATCH during the job
  pending --> [*]: removal finished well (listing.deleted)
```

* **`never_published`:** the listing only exists here.
* **`dirty`:** there is a local change that has not been published yet.
* **`pending`:** a publish or removal job is running. When removal on the marketplace finishes well, the Listing is deleted and you get `listing.deleted`.
* **`clean`:** what is here is what was published or imported.
* **`failed`:** the last publish or removal job failed. You get `listing.updated` with `behavior: "publication_failed"` and the `jobId`; the error is on the Job item. Fix it and publish again.
* **`dirty` wins over the end of a job.** If you edit the listing while the job runs, it ends `dirty`, because the edit has not been published yet.
* **Reconciling** a listing that already exists keeps `dirty` and `pending`; in the other states, it becomes `clean`.

`marketplaceStatus` (`active`, `paused`, `closed`, `scheduled`, `suppressed`, `frozen`…) is something else: it comes from the marketplace and changes on publishing, import, reconciliation and notifications. With it come the layers `buyable`, `discoverable`, `pendingRevision` and `statusReasons`, on the Listing and on each binding. When one changes, you receive `listing.status_changed` with the `before` and `after` of the Listing and only of the lines that changed; `source` tells where it came from (`imported`, `inbound`, `reconciled`, `published`, or `inventory_published`). The first import does not emit this event: `listing.created` already covers it.

`linkState` is `linked` when the listing has at least one binding, and `unlinked` when it has none. It is `ambiguous` when an import found more than one Variant for some variation and linked none of them; fix it with the bindings `PUT`.

## MarketplaceProduct: `publicationState` and events

The content has the same states as the offer, but it moves on its own:

* **`never_published`:** the content only exists here. This is how it is born, when you create or clone a Listing.
* **`dirty`:** a [`PATCH`](/docs/marketplace-products/update) or a replacement of the [variations](/docs/marketplace-products/replace-variants) changed the content and it has not been published yet.
* **`clean`:** the content was published or imported.
* **Publishing the Listing publishes the content with it, if it was never published.** The content becomes `clean`. If it had been published and you changed it afterwards, it stays `dirty` even after you publish the Listing: publishing only the content takes the [content publication](/docs/marketplace-products/publish) route.
* The MarketplaceProduct has no `DELETE`. Only the publish route talks to the marketplace; it keeps the content `pending` until the Job ends.

Each change becomes a webhook, `marketplace_product.created` or `marketplace_product.updated`. The `behavior` of `updated` says what happened:

| `behavior`              | When                                                                                                                             |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `local_update`          | you changed the content with `PATCH`                                                                                             |
| `variants_changed`      | the variations changed: `PUT …/variants`, or a bindings `PUT` that created one                                                   |
| `published`             | publishing a Listing confirmed the content on the marketplace                                                                    |
| `imported`              | an import brought the content in again                                                                                           |
| `publication_requested` | you asked for the [content publication](/docs/marketplace-products/publish); the content becomes `pending` (carries the `jobId`) |
| `publication_failed`    | the content publication failed for good (carries the `jobId`)                                                                    |
| `external_changed`      | the marketplace changed the content and the change reached `observed`                                                            |

## Job

```mermaid
stateDiagram-v2
  accTitle: Job states
  [*] --> queued: the API creates the job
  queued --> running: a worker starts it
  running --> completed: every item worked (job.completed)
  running --> partial: some items worked (job.partially_completed)
  running --> failed: no item worked (job.failed)
  completed --> [*]
  partial --> [*]
  failed --> [*]
```

Each Job has items, one per resource, which go from `pending` to `succeeded` or `failed` only once. The Job types and who creates each one:

| `type`                        | Who creates it                                                           | One item per       |
| ----------------------------- | ------------------------------------------------------------------------ | ------------------ |
| `connection.sync`             | the API, when a Connection is born or access is granted again            | Connection         |
| `listing.publish`             | `POST /v1/listings/{listingId}/publish`                                  | Listing            |
| `listing.remove`              | a `DELETE /v1/listings/{listingId}` that removes it on the marketplace   | Listing            |
| `listing.import`              | `POST /v1/connections/{id}/listing-imports`                              | Connection         |
| `listing.reconcile`           | `POST /v1/connections/{id}/listing-reconciliations`                      | Connection         |
| `order.import`                | `POST /v1/connections/{id}/order-imports`                                | Connection         |
| `order.reconcile`             | `POST /v1/connections/{id}/order-reconciliations`                        | Connection         |
| `inventory.publish`           | an inventory movement, automatic sync or `POST …/inventory-publications` | binding            |
| `marketplace_product.publish` | `POST /v1/marketplace-products/{marketplaceProductId}/publish`           | MarketplaceProduct |

## Webhook delivery

```mermaid
stateDiagram-v2
  accTitle: Webhook delivery states
  [*] --> pending: an event for a subscribed endpoint
  pending --> delivered: the endpoint answered 2xx
  pending --> pending: failed, next attempt scheduled
  pending --> failed: the 8th attempt failed, or the endpoint is paused (enabled false)
  delivered --> [*]
  failed --> [*]
```

* Retries come after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h. Each attempt waits up to 10 seconds for the answer.
* [Redeliver](/docs/webhooks/redeliver) creates a **new** delivery of the same event. The original delivery does not change.
* The same event can arrive more than once. Use the event `id` to skip repeats.

## A publication, from the request to the webhook

The path from `POST /v1/listings/{listingId}/publish` to the published listing:

```mermaid
sequenceDiagram
  accTitle: Publication sequence
  participant App as Your application
  participant API as marketapi
  participant MP as Marketplace
  App->>API: POST /v1/listings/{id}/publish
  API-->>App: 202 with the Job (status queued)
  Note over API: Listing becomes pending
  API-)App: webhook listing.updated (publication_requested)
  API->>MP: creates or updates the listing
  MP-->>API: listing confirmed
  Note over API: Listing becomes clean, observed updated.<br/>The content goes along if never published
  API-)App: webhook marketplace_product.updated (published)
  API-)App: webhook listing.updated (published)
  API-)App: webhook job.completed
```

On the first publication, the content (the MarketplaceProduct) goes in the same Job as the offer. On later ones, only the offer and its lines change, but the `marketplace_product.updated` with `published` is sent just the same.

If the marketplace refuses, the Job ends `failed` and the listing becomes `failed`. You get `listing.updated` (`publication_failed`) and `job.failed`, and the error is on the Job item (`GET /v1/jobs/{id}/items`).

## An inventory movement with automatic sending

The path from a recorded sale to the new quantity on the marketplace:

```mermaid
sequenceDiagram
  accTitle: Inventory movement sequence
  participant App as Your application
  participant API as marketapi
  participant MP as Marketplace
  App->>API: POST /v1/sellers/{sellerId}/inventory-movements
  API-->>App: 201 with the movements, the balance and the publicationJobId
  API-)App: webhook inventory.movement_created
  API->>MP: sends the quantity of each binding
  MP-->>API: quantity confirmed
  API-)App: webhook listing.updated (inventory_published)
  API-)App: webhook inventory.published
  Note over App,API: these two webhooks come once per binding
  API-)App: webhook job.completed
```

With no automatic sending and no `publishTargets`, the movement only changes the balance: `publicationJobId` comes back `null`, and nothing goes to the marketplace. See [From the balance to the marketplace](/docs/data-model/catalog-and-inventory#from-the-balance-to-the-marketplace).

The `inventory.movement_created` webhook is sent once per ledger row, with the `warehouseId` and the Variant balance after the command. A transfer between warehouses writes two rows and sends two events, with the same `transferId`. Because the Variant total does not change, it does not trigger automatic sending.

---

# Authentication

URL: https://marketapi.shop/docs/authentication

> API key in the Authorization header.

Every call to `/v1` carries the API key as a bearer token:

```bash
curl $BACKEND_URL/v1/sellers \
  -H "Authorization: Bearer mk_your_api_key"
```

The `Bearer ` prefix (with a space) is required.

## The API key

<Card title="Generate an API key" href="/dashboard/api-keys" description="Opens the API keys screen in the dashboard. If you're not signed in, sign in first, then open API keys from the menu." />

* Created in the [dashboard](/dashboard), under *API keys*. Create as many as you need, one per system.
* Format: `mk_` followed by 32 characters, like `mk_AbCd1234…`. The first 8 of those 32 are the **prefix**, which identifies the API key in the dashboard list.
* **Shown once**, when created. If you lose it, create another.
* Can be **revoked** at any time. From then on it stops working.
* Identifies **your account**. You only see the sellers and data of your own account.

Treat the API key like a password: keep it out of source code and the front end. If it leaks, revoke it.

## Try it in the playground

Every endpoint page has a playground:

1. In the `Authorization` field, paste the API key after `Bearer `, like this: `Bearer mk_your_api_key`.
2. Fill in the other fields and send.

The playground calls the real API: a `POST` creates the resource in your account.

Your browser saves the API key and fills in the field on the other endpoint pages. On a shared computer, clear the field when you're done.

## Missing, invalid or revoked API key

```http
HTTP/1.1 401 Unauthorized
```

```json
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Missing, invalid or revoked API key."
  }
}
```

Full error format in [Errors](/docs/errors).

---

# Connecting a seller

URL: https://marketapi.shop/docs/connecting-a-seller

> From a created seller to a connected marketplace account, and how you find out.

The seller connects their accounts on **our page**. You create the session through the API, send them the URL and find out through the webhook — or by reading the session.

The examples use `Authorization: Bearer mk_your_api_key` and `SELLER_ID` with the id of a [seller](/docs/sellers/overview).

## 1. Create a link session

```bash
curl -X POST $BACKEND_URL/v1/sellers/$SELLER_ID/link-sessions \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"redirectUrl":"https://your-app.com/connected"}'
```

Without `allowedMarketplaces`, the page offers every marketplace of this environment. Asking for one that does not exist here (for example `amazon`, or `mock` in production) gives `400` with `details.unavailable`. In development, use `mock` (see the end of this page).

The `201` response carries the page `url` and, in `connectUrls`, one link per marketplace for your own button. They show up **only here** (and in the replay of the same `Idempotency-Key`) and last at most 5 minutes, so create the session at click time. Fields and errors are in [Create a link session](/docs/link-sessions/create).

## 2. The seller connects

Two paths, your choice:

* **Through our page.** Send the `url` to the seller. They pick the marketplace, authorize on the marketplace screen and come back to the page, which shows the connected account. They can connect more than one marketplace, and more than one account on the same one. **Finish** closes the session and takes them to your `redirectUrl`, if you sent one. The page has a language picker (Portuguese and English).
* **Through your app.** Put `connectUrls.mercadolivre` behind your "Connect Mercado Livre" button. The seller goes straight to the marketplace consent screen and comes back to your `redirectUrl` with `?connected=mercadolivre` (or `?error=<code>`), without ever seeing our page. Exchanging the code for a token and renewing it stays on us.

On Mercado Livre, only the **administrator** account can authorize; an operator or collaborator user gets an error.

## 3. You find out

Through the [webhook](/docs/receiving-webhooks): `connection.created` for each connected account and `link_session.completed` when the seller clicks **Finish**. Or read the session: [Get a link session](/docs/link-sessions/get) returns its `status` and `connectionIds`.

## 4. Use the connections

The list is per seller, with the `Seller-Id` header:

```bash
curl "$BACKEND_URL/v1/connections?status=active" \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Seller-Id: $SELLER_ID"
```

Each connection carries `status`: `active` works (we refresh the token on our own); `revoked` and `error` need a new link session (`revoked`: you or the marketplace ended the access; `error`: renewing failed in an unexpected way). Details in [Connections](/docs/connections/overview). To disconnect: [Disconnect](/docs/connections/disconnect).

## Development: the `mock` marketplace

Outside production there is the `mock` marketplace: authorization is instant, with no account anywhere. To simulate the marketplace revoking access to a `mock` connection (`CONNECTION_ID` is its `id`, as `GET /v1/connections` returns it):

```bash
curl -X POST $BACKEND_URL/inbound/mock/$CONNECTION_ID \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"topic":"deauthorization"}'
```

`connection.revoked` arrives within seconds, with `revokedReason: "marketplace"`. In production this route answers `404`.

---

# BYOK (your own app)

URL: https://marketapi.shop/docs/own-apps

> Bring Your Own Key. The seller authorizes your business's app, not marketapi's.

By default, the seller authorizes the marketapi app at the marketplace. With **BYOK** (*Bring Your Own Key*) you do the opposite: you register the credential of **your** app and the seller authorizes it.

## 1. What it is

The seller sees your business's app on the marketplace authorization screen, not marketapi's. You register your app's credential (Client ID and Client Secret) in the dashboard.

## 2. Why use it

* Your brand on the authorization screen.
* Calls count against your quota.
* Your app does not depend on ours.

## 3. Where it works

| Marketplace                            | Supports BYOK?                                        |
| -------------------------------------- | ----------------------------------------------------- |
| Mercado Livre                          | Yes. See the [tutorial](/docs/own-apps/mercadolivre). |
| Magalu, TikTok Shop, Amazon and Shopee | When the marketplace joins marketapi.                 |

Following the marketplace's terms with your app is your responsibility.

## 4. The rules

<Callout>
  1. Registering the credential does not touch existing connections: they stay on the marketapi app.

  2. After registering, every new connection uses your app, unless the link session asks for `app: "platform"`.

  3. Removing the credential revokes only the connections made with it, and you get `connection.revoked` for each one. Connections on the marketapi app stay.

  4. A connection uses the same app for its whole life. To change apps, the seller connects again.
</Callout>

## 5. Force the marketapi app

With a credential registered, the link session uses your app (`app: "auto"`, the default). To use the marketapi app anyway, send `app: "platform"`. `SELLER_ID` is the id of a [seller](/docs/sellers/overview).

```bash
curl -X POST $BACKEND_URL/v1/sellers/$SELLER_ID/link-sessions \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"app":"platform"}'
```

The fields are in [Create link session](/docs/link-sessions/create).

## 6. Know which app a connection uses

A connection has an `app` field: `platform` (ours) or `own` (yours). It also comes in the `connection.*` webhooks. Details in [Connections](/docs/connections/overview).

## 7. Move sellers to your app

1. Register the credential.
2. Create a link session for the seller.
3. Ask them to connect again.

If the seller connects the same marketplace account, the connection keeps its id and becomes `own`. You get `connection.updated`.

## 8. Register only in the dashboard

Go to **BYOK** and pick the marketplace. There is no API route for this. When you save, you tick a statement: the app is yours (or your company's), and using it with marketapi under the marketplace's terms is your responsibility. marketapi records who ticked it and when.

---

# Mercado Livre

URL: https://marketapi.shop/docs/own-apps/mercadolivre

> Step by step to create the app in the DevCenter and use it in marketapi.

Create an app in the Mercado Livre DevCenter and register its credential in marketapi. First, read [the rules](/docs/own-apps).

## Step by step

1. Sign in to the [DevCenter](https://developers.mercadolivre.com.br/devcenter) with the account that will own the integration (preferably a business account). In Brazil, each account creates **one** app.
2. Click "Criar uma aplicação". Name, short name, description and logo: **this is what the seller sees** on the authorization screen.
3. Under redirect URIs, register exactly <ConnectCallbackUrl marketplace="mercadolivre" />. It must be `https` and identical.
4. Turn on PKCE. marketapi always sends PKCE.
5. Scopes: read and write. If the form shows "offline access", check it: without it there is no refresh token.
6. Topics and notifications URL: leave blank for now.
7. Save and copy the **Client ID** and the **Client Secret**.
8. In the dashboard, go to **BYOK**, choose **Mercado Livre**, paste the credential, tick the responsibility statement and save. marketapi checks the credential with Mercado Livre right away.

## Change the secret later

In the DevCenter use "Programar renovação": both secrets stay valid until the date you choose (up to 7 days). Update the secret in the dashboard before then.

"Renove agora" invalidates the old secret immediately, and connections may fail until you update it.

<Callout>
  Remember the [rules](/docs/own-apps): existing connections stay on the
  marketapi app, and new ones use yours.
</Callout>

---

# Marketplace support

URL: https://marketapi.shop/docs/marketplace-support

> What every integration offers today and what is still planned.

This page shows the **static product capability catalog**. It answers what MarketAPI already offers for each marketplace and is generated from the same definition used by adapters. A capability change therefore does not rely on keeping a copied documentation table up to date.

Before an external action, read the Connection's [Connection capabilities](/docs/connections/capabilities). The matrix below is a product overview; that response accounts for the adapter that is actually connected and is the authority for importing, publishing, removing, or updating inventory on one specific Connection.

## Reading the states

* **Supported:** the capability is part of a production integration. It can still depend on an active Connection and the capability endpoint response.
* **Development only:** it exists only on Mock, which is used in development and e2e. It does not represent a production marketplace.
* **Not supported:** the marketplace is integrated, but that capability has not been implemented yet.
* **Planned:** the marketplace appears in the product catalog, but has no adapter and cannot be connected through the API.

<Callout type="warn">
  Amazon, Magalu, Shopee, and TikTok Shop are planned. Do not create a link
  session or present a connection button for them until their integration is
  available.
</Callout>

## Capability matrix

The table is generated by <code>MarketplaceSupportMatrix</code>; no data is copied into this guide.

<MarketplaceSupportMatrix />

## Next steps

Start with [connecting a seller](/docs/connecting-a-seller). Then use [Connection capabilities](/docs/connections/capabilities) as the precondition for each command, and read the [Listings overview](/docs/listings/overview) to understand the local flow before external publication.

---

# Receiving webhooks

URL: https://marketapi.shop/docs/receiving-webhooks

> Signed events, how to verify the signature, retries and deduplication.

We tell your system when something changes: an account connected, disconnected, a job finished. Each event arrives as a signed `POST` to the URL you register.

## 1. Register an endpoint

```bash
curl -X POST $BACKEND_URL/v1/webhooks/endpoints \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"url":"https://your-app.com/webhooks/marketapi","events":["*"]}'
```

The `201` response carries the `secret` (`whsec_...`) **only once**. Keep it: you verify the signature with it. URL and event rules are in [Register an endpoint](/docs/webhooks/create-endpoint).

## 2. What arrives

<WebhookEventsTable />

Three details of `data`: in `connection.updated`, `syncJobId` is `null` when the change started no sync. In `connection.revoked`, the reason is in `connection.revokedReason`. And each final status of a job has its own event: `job.completed` (every item succeeded), `job.partially_completed` (some failed) and `job.failed`. In all of them `data` carries the whole job. See [Tracking jobs](/docs/tracking-jobs).

For products and listings, `product.*` and `variant.*` describe local changes only; they do not publish to a marketplace. `listing.external_changed` always reports an external change: `behavior: "applied"` means only the listing's `observed` state was updated; `behavior: "observed_only"` means inbound was disabled and no record was changed. In both cases `data.observed` carries the normalized representation that was just fetched. An external deletion follows the same rule: with active inbound the observed status becomes `deleted`; it never deletes the local Listing.

`listing.conflict_created` and `listing.conflict_resolved` carry only ids, the group and the paths, never the values. Read both sides with [List conflicts](/docs/listings/conflicts).

For inventory, `inventory.movement_created` is sent **once per ledger row**. `data` carries the `movement` (with `warehouseId` and `transferId`) and the Variant `balance` after the command, in total and per warehouse. The two events of a transfer carry the same `balance`. `warehouse.created`, `warehouse.updated` and `warehouse.archived` carry the warehouse in `data.warehouse`. When the default warehouse changes, the previous one also gets `warehouse.updated`.

Headers of each delivery:

| Header                    | Value                                             |
| ------------------------- | ------------------------------------------------- |
| `X-Marketapi-Event`       | the `type`                                        |
| `X-Marketapi-Delivery-Id` | id of this delivery (changes on every redelivery) |
| `X-Marketapi-Signature`   | `t=<unix seconds>,v1=<HMAC-SHA256>`               |

```json
{
  "id": "e1d2c3b4-a5f6-4e7d-8c9b-0a1b2c3d4e5f",
  "type": "connection.created",
  "createdAt": "2026-10-06T14:32:00.000Z",
  "accountId": "8a2e5b90-1c44-4d7a-b3f2-5e6a7c8d9f02",
  "sellerId": "3d1c8a52-7f0e-4b0a-8a65-9c1b2f6d7e01",
  "data": {
    "connection": {
      "id": "9a1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
      "marketplace": "mercadolivre",
      "status": "active"
    },
    "linkSessionId": "5b2f0c1e-8d3a-4f6b-9c21-7e4d3a2b1c00",
    "syncJobId": "7c6b5a49-3827-4615-a0b9-c8d7e6f5a4b3"
  }
}
```

(The example trims the `connection` fields; the full shape is in [Get a connection](/docs/connections/get).)

Answer `2xx` within **10 seconds**. Anything else counts as a failure — redirects (`3xx`) included: register the final URL.

## 3. Verify the signature

`v1` is the HMAC-SHA256, in hex, of `<t>.<body>` with your `secret` (the whole value, `whsec_` included). Use the **raw body**, as it arrived: re-serialized JSON changes the bytes and the signature will not match. Reject a `t` more than 5 minutes off.

```js
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

const SECRET = process.env.MARKETAPI_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_S = 5 * 60;
const seen = new Set(); // event ids already handled; use your database in production

const isValid = (header, rawBody) => {
  const parts = Object.fromEntries(
    header.split(',').map((part) => part.split('=')),
  );
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > TOLERANCE_S) return false;
  const expected = Buffer.from(
    createHmac('sha256', SECRET).update(`${t}.${rawBody}`).digest('hex'),
  );
  const received = Buffer.from(parts.v1 ?? '');
  return (
    expected.length === received.length && timingSafeEqual(expected, received)
  );
};

const app = express();

app.post(
  '/webhooks/marketapi',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const rawBody = req.body.toString('utf8');
    if (!isValid(req.get('x-marketapi-signature') ?? '', rawBody))
      return res.sendStatus(400);
    const event = JSON.parse(rawBody);
    if (!seen.has(event.id)) {
      seen.add(event.id);
      // Act on event.type and event.data here.
    }
    res.sendStatus(200);
  },
);

app.listen(3000);
```

The example uses `express` and is ESM: `"type": "module"` in `package.json` (or save it as `.mjs`).

### Rotation

If the `secret` leaks or gets lost, [rotate it](/docs/webhooks/rotate-secret). The new one shows up only once, and the old one stops working at once, with no overlap period. Update your system right after.

## 4. Retries and deduplication

If a delivery fails, we try again after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h. After the 8th attempt the delivery is `failed`.

The same event can arrive more than once (you answered late, or asked for a redelivery). **Deduplicate on the event `id`**; `X-Marketapi-Delivery-Id` changes on every delivery.

## 5. Log and redelivery

The log shows each delivery with the outcome of its last attempt: [List deliveries](/docs/webhooks/list-deliveries). Fixed your endpoint? [Redeliver](/docs/webhooks/redeliver) sends the event again, as a new delivery. The dashboard has the same **Redeliver** button.

---

# Tracking jobs

URL: https://marketapi.shop/docs/tracking-jobs

> Background operations — track them with GET or through the webhook (job.completed, job.partially_completed, job.failed).

Everything that depends on the marketplace runs in the background, as a **job**. The API answers right away; the work goes on afterwards.

There are six job types (list in [States and events](/docs/data-model/states-and-events#job)). The most common is `connection.sync`, started for each connected account. Its `id` arrives in `data.syncJobId`: in the `connection.created` event, and in `connection.updated` when the seller connects the same account again.

## Two ways to track

1. **Through the webhook (recommended):** subscribe your endpoint to `job.completed`, `job.partially_completed` and `job.failed` (or `*`). Each final status has its own event: `job.completed` only arrives when every item succeeded. The event carries the whole job. See [Receiving webhooks](/docs/receiving-webhooks).
2. **By polling:** [Get a job](/docs/jobs/get) with the `id`. `JOB_ID` is the `data.syncJobId` of the `connection.created` event (or of the `connection.updated` of a reconnection). Wait a few seconds between calls.

```bash
curl $BACKEND_URL/v1/jobs/$JOB_ID \
  -H "Authorization: Bearer mk_your_api_key"
```

## Outcome

The final `status` tells how it went: `completed` (every item succeeded), `partial` (some failed) or `failed` (none succeeded). Each job has items with their own outcome: the failed ones are in [List items](/docs/jobs/items) with `status=failed`. States and rules are in [Jobs](/docs/jobs/overview).

A job can stay `running` longer when the marketplace limits how many calls we can make. The remaining items wait for the quota to come back, then go on. The job does not fail because of it.

---

# Idempotency and limits

URL: https://marketapi.shop/docs/idempotency-and-limits

> Retry POSTs safely and handle the rate limit.

## Idempotency

Networks fail. To retry a `POST` without duplicating its effect, send `Idempotency-Key` with a unique value per operation (a UUID works; 1 to 255 characters):

```bash
KEY=$(uuidgen)

curl -X POST $BACKEND_URL/v1/sellers \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d '{"name":"Acme Store","externalId":"customer-123"}'
```

Run the same command again with the same `KEY` and body: you get **the same response** (same status and body) with the header:

```http
Idempotency-Replayed: true
```

Rules:

* Only `POST /v1/*` honors the header. `PATCH` and `DELETE` ignore it: repeating a `DELETE` already done responds `404`.
* A replay returns the stored status (e.g. `201`).
* A key longer than 255 characters responds `400 VALIDATION_ERROR`.
* The same key with a **different body** responds `409 CONFLICT` (no `details`). Use a new key for a different operation.
* The key is kept for **24 hours**. After that, the same value counts as a new key.
* The key is scoped **per account**: two API keys of the same account share idempotency keys. Another account can use the same value without a clash.
* Only success responses are stored. If it failed, you can retry with the same key.
* Don't send two requests with the same key at the same time. Wait for the first response.

The `409` for a different body:

```json
{
  "error": {
    "code": "CONFLICT",
    "message": "Idempotency-Key already used with a different body."
  }
}
```

## Rate limit

The default is 300 requests per minute, per API key (configurable). The counter belongs to the API key, summed across all `/v1` routes. Every authenticated request to `/v1` counts, including `4xx` errors and idempotency replays. `401` does not count: authentication runs before the limit, and without a valid API key there is no counter to add to.

Authenticated `/v1` responses that are not `429` carry (`401` does not):

| Header                  | What it is                                     |
| ----------------------- | ---------------------------------------------- |
| `X-RateLimit-Limit`     | window limit                                   |
| `X-RateLimit-Remaining` | requests still allowed                         |
| `X-RateLimit-Reset`     | seconds until the window restarts (not a date) |

When you exceed it, the response is `429`, with the `Retry-After` header and this body:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 30
```

```json
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded. Try again after the time given in Retry-After."
  }
}
```

How to handle it:

1. Stop sending with that API key and **wait for `Retry-After`** before the next attempt.
2. Don't retry right away: it only uses up the next window.
3. If you often get close to the limit, watch `X-RateLimit-Remaining` and slow down before you hit a `429`.

When retrying a `POST` after a `429`, resend the same `Idempotency-Key`.

Error format in [Errors](/docs/errors).

---

# Billing

URL: https://marketapi.shop/docs/billing

> One plan in dollars, up to 5 sellers free, what counts toward the bill and what happens without a payment in good standing.

One plan, charged in **US dollars (USD)**.

| Sellers (period peak) | Price                                |
| --------------------- | ------------------------------------ |
| Up to 5               | Free                                 |
| 6 or more             | **US$ 9.90 per seller**, all of them |

Once you pass 5, **all** sellers count, not only the ones above 5. With 6 sellers: 6 × US$ 9.90 = US$ 59.40.

## What counts: the peak

The base is the **peak**: the highest number of sellers that existed at the same time in the period. Deleting sellers does not lower the peak.

| What you do in the period     | Peak | You pay    |
| ----------------------------- | ---- | ---------- |
| Create 10, delete 5           | 10   | 10 sellers |
| Create 10, delete 5, create 6 | 11   | 11 sellers |
| Create 10, create 6, delete 5 | 16   | 16 sellers |

A seller counts even with no marketplace connected.

## Period and charge

The period is the subscription's own: it starts on the day of the month you subscribed. You are charged **at the end of each period**, for that period's usage, in USD.

## Manage

The card is requested at signup, through Stripe Checkout. Later, in the dashboard, open **Billing** and use the Stripe portal to change the card, see invoices, edit billing details and cancel.

## Without a payment in good standing

Without a subscription in good standing, every `/v1` route answers `402 PAYMENT_REQUIRED` (see [Errors](/docs/errors)).

Still working:

* the `/connect` page of an already-issued link;
* marketplace notifications;
* token refresh;
* webhooks to you.

Nothing is deleted.

If a payment fails, everything keeps working while Stripe retries. When the retries run out, the subscription is canceled and the API answers `402` until you reactivate it, in **Billing**, in the dashboard.

---

# Errors

URL: https://marketapi.shop/docs/errors

> One format, with a stable code for your code to handle.

Every error has this body:

```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request.",
    "details": [
      {
        "origin": "string",
        "code": "too_small",
        "minimum": 1,
        "inclusive": true,
        "path": ["name"],
        "message": "Too small: expected string to have >=1 characters"
      }
    ]
  }
}
```

`details` comes in two shapes. On a body, query or `:id` `VALIDATION_ERROR`, it is the **list** of validation problems, one per field. In the cases of the table below, it is an **object** that says what clashed. Other errors have no `details`. The text of each `message` may change: use `code` and `details`.

| Case                                                                                                                                                               | HTTP | `details`                                                                                                                        |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---- | -------------------------------------------------------------------------------------------------------------------------------- |
| `Seller-Id` header missing or malformed                                                                                                                            | 400  | `{ "header": "Seller-Id" }`                                                                                                      |
| marketplace not available in this environment                                                                                                                      | 400  | `{ "field": "allowedMarketplaces", "unavailable": ["amazon"] }`                                                                  |
| `redirectUrl` that is not `https`                                                                                                                                  | 400  | `{ "field": "redirectUrl" }`                                                                                                     |
| webhook URL refused                                                                                                                                                | 400  | `{ "field": "url" }`                                                                                                             |
| `Idempotency-Key` longer than 255 characters                                                                                                                       | 400  | `{ "header": "Idempotency-Key" }`                                                                                                |
| marketplace options refused (another marketplace namespace, or a URL, header or credential inside)                                                                 | 400  | `{ "field": "marketplaceOptions" }`, or `"oneTimeMarketplaceOptions"` when publishing                                            |
| invalid binding: a Variant that does not exist or belongs to another seller, `bindingIds` that are not the listing's, or `eligibleBindingIds` from another listing | 400  | `{ "field": "bindings" }`, `{ "field": "bindingIds" }`, or `{ "field": "eligibleBindingIds" }`                                   |
| conflict resolution refused: `value` outside the conflict paths, or a result that is invalid for the group                                                         | 400  | `{ "field": "value" }` or `{ "field": "resolution", "issues": [...] }`                                                           |
| repeated `externalId`                                                                                                                                              | 409  | `{ "field": "externalId" }`                                                                                                      |
| seller with active connections                                                                                                                                     | 409  | `{ "reason": "active_connections" }`                                                                                             |
| link session that is not pending                                                                                                                                   | 409  | `{ "status": "completed" }` (the current status)                                                                                 |
| Connection that is not active (for example, revoked)                                                                                                               | 409  | `{ "reason": "connection_inactive" }`                                                                                            |
| publishing with outbound sync off, or a listing with no Variant binding                                                                                            | 409  | `{ "reason": "outbound_disabled" }` or `{ "reason": "listing_unlinked" }`                                                        |
| publishing content: already published (`create`), never published (`update`) or without a variation                                                                | 409  | `{ "reason": "marketplace_product_published" }`, `"marketplace_product_unpublished"` or `"marketplace_product_without_variants"` |
| archiving a Product or Variant that a listing binding still uses                                                                                                   | 409  | `{ "reason": "product_in_use" }` or `{ "reason": "variant_in_use" }`                                                             |
| kit BOM with a cycle                                                                                                                                               | 409  | `{ "reason": "component_cycle" }`                                                                                                |
| archiving a Product or Variant that is a component of an active kit                                                                                                | 409  | `{ "reason": "product_in_kit" }` or `{ "reason": "variant_in_kit" }`                                                             |
| BOM or listing binding with an archived Variant, or one active in an archived Product                                                                              | 409  | `{ "reason": "variant_archived" }` or `{ "reason": "product_archived" }`                                                         |
| inventory movement on a Variant that MarketAPI does not manage                                                                                                     | 409  | `{ "reason": "inventory_authority" }`                                                                                            |
| inventory movement in an archived warehouse                                                                                                                        | 409  | `{ "reason": "warehouse_archived", "warehouseId": "…" }`                                                                         |
| movement that would leave the warehouse without enough stock (for a transfer, the source)                                                                          | 409  | `{ "reason": "inventory_underflow", "warehouseId": "…" }`                                                                        |
| archiving the seller's default warehouse, or a warehouse with balance                                                                                              | 409  | `{ "reason": "warehouse_in_use", "uses": ["default", "stock"] }`                                                                 |
| quantity publication with no eligible target                                                                                                                       | 409  | `{ "reason": "inventory_target_empty" }`                                                                                         |
| resolving a listing conflict that was already resolved                                                                                                             | 409  | `{ "reason": "conflict_not_open" }`                                                                                              |
| Connection without the operation's scope                                                                                                                           | 403  | `{ "marketplace": "mercadolivre", "capability": "listings.publish" }`                                                            |
| operation the marketplace does not offer                                                                                                                           | 422  | `{ "marketplace": "mercadolivre", "capability": "listings.publish" }`                                                            |

| HTTP | `code`                    | When                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ---- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400  | `VALIDATION_ERROR`        | invalid body, query (`page`, `pageSize`, filters), `:id` that is not a UUID, or `Idempotency-Key`; `Seller-Id` header missing or malformed; marketplace not available; `redirectUrl` that is not `https`; webhook URL refused; marketplace options refused; invalid binding; request the marketplace itself refused (discovery routes)                                                                                                                |
| 401  | `UNAUTHORIZED`            | missing, invalid or revoked API key                                                                                                                                                                                                                                                                                                                                                                                                                   |
| 402  | `PAYMENT_REQUIRED`        | the account has no subscription in good standing; activate or reactivate it in Billing, in the dashboard                                                                                                                                                                                                                                                                                                                                              |
| 403  | `FORBIDDEN`               | dashboard session with no account created yet; any `/v1` route. Does not happen with an API key                                                                                                                                                                                                                                                                                                                                                       |
| 403  | `CAPABILITY_NOT_GRANTED`  | the marketplace offers the operation, but the Connection lacks a required scope; the seller authorizes again. In a job, also when the marketplace itself refuses the app the permission for the operation: enable the permission in the app and authorize again                                                                                                                                                                                       |
| 404  | `NOT_FOUND`               | unknown route, or a valid id that does not exist (or belongs to another account) — including the seller in the path or in `Seller-Id`, and an internal job. On the discovery routes, also a category, product type or catalog product the marketplace does not know                                                                                                                                                                                   |
| 409  | `CONFLICT`                | `externalId` already in use, `Idempotency-Key` reused with a different body, seller with active connections, link session that is not pending, inactive Connection, listing state, Product or Variant in use, BOM cycle, inventory MarketAPI does not manage, not enough stock in a warehouse, archived or in-use warehouse, archived Variant or kit component, listing conflict already resolved. Any `POST` can return the reused `Idempotency-Key` |
| 422  | `CAPABILITY_UNSUPPORTED`  | the Connection's marketplace does not offer the operation. See [Connection capabilities](/docs/connections/capabilities)                                                                                                                                                                                                                                                                                                                              |
| 429  | `RATE_LIMITED`            | API key limit exceeded; see `Retry-After`                                                                                                                                                                                                                                                                                                                                                                                                             |
| 500  | `INTERNAL_ERROR`          | our fault; already logged                                                                                                                                                                                                                                                                                                                                                                                                                             |
| 503  | `MARKETPLACE_UNAVAILABLE` | the marketplace did not answer a call of ours, refused the app or limited the requests; try again. `details.reason` tells a wait from an outage                                                                                                                                                                                                                                                                                                       |
| 503  | `STORAGE_UNAVAILABLE`     | file storage is not configured in this environment (asset routes)                                                                                                                                                                                                                                                                                                                                                                                     |

## Examples

`409 CONFLICT` has several causes (listed in the table above). Tell them apart by `details`; the reused `Idempotency-Key` is the only one without it.

Repeated `externalId`, with `details.field`:

```json
{
  "error": {
    "code": "CONFLICT",
    "message": "A seller with this externalId already exists.",
    "details": { "field": "externalId" }
  }
}
```

Seller with active connections, on `DELETE /v1/sellers/<id>`:

```json
{
  "error": {
    "code": "CONFLICT",
    "message": "Seller with active connections cannot be removed. Disconnect them first.",
    "details": { "reason": "active_connections" }
  }
}
```

Link session that is no longer pending, on `DELETE /v1/link-sessions/<id>`:

```json
{
  "error": {
    "code": "CONFLICT",
    "message": "Only a pending link session can be revoked.",
    "details": { "status": "completed" }
  }
}
```

`400` for a missing `Seller-Id`, on `GET /v1/connections`:

```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Seller-Id header is required.",
    "details": { "header": "Seller-Id" }
  }
}
```

`Idempotency-Key` reused with a different body, no `details` (see [Idempotency and limits](/docs/idempotency-and-limits)):

```json
{
  "error": {
    "code": "CONFLICT",
    "message": "Idempotency-Key already used with a different body."
  }
}
```

`422 CAPABILITY_UNSUPPORTED`, when publishing a listing on a Connection whose marketplace does not publish. `403 CAPABILITY_NOT_GRANTED` has the same `details`. In an inventory batch the error is not in the response: that Connection's item fails in the job, with the `code` in its message.

```json
{
  "error": {
    "code": "CAPABILITY_UNSUPPORTED",
    "message": "Marketplace mercadolivre does not support listings.publish.",
    "details": {
      "marketplace": "mercadolivre",
      "capability": "listings.publish"
    }
  }
}
```

`402 PAYMENT_REQUIRED` (no subscription in good standing; it does not count toward the rate limit):

```json
{
  "error": {
    "code": "PAYMENT_REQUIRED",
    "message": "Subscription required. Add a payment method in the dashboard."
  }
}
```

`429 RATE_LIMITED` comes with the `Retry-After` header (seconds):

```json
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded. Try again after the time given in Retry-After."
  }
}
```

`500 INTERNAL_ERROR`:

```json
{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "Internal error."
  }
}
```

`503 MARKETPLACE_UNAVAILABLE`, on the Connection [discovery and catalog routes](/docs/connections/listing-categories), which read the marketplace right away. `details.reason` tells a wait from an outage:

| `details.reason`           | What happened                                       | What to do                  |
| -------------------------- | --------------------------------------------------- | --------------------------- |
| missing                    | the marketplace did not answer, or refused the app  | try again later             |
| `marketplace_rate_limited` | the marketplace is limiting the requests of the app | wait a little and try again |
| `connection_refreshing`    | the Connection credential is being renewed          | try again in a moment       |

```json
{
  "error": {
    "code": "MARKETPLACE_UNAVAILABLE",
    "message": "The marketplace is limiting requests. Try again.",
    "details": { "reason": "marketplace_rate_limited" }
  }
}
```

If the marketplace ended the seller's access in the middle of the call, the answer is not `503`: it is `409 CONFLICT` with `{ "reason": "connection_inactive" }`, the same as any route of a Connection that is not active. Ask the seller to connect again.

Handle errors by `code`, not by `message`: the message may change, the code won't. The `message` is in English and is meant for logs and debugging.

Each endpoint page shows the errors it can return, with an example body for each status. For example: [Create a seller](/docs/sellers/create).

---

# Overview

URL: https://marketapi.shop/docs/sellers/overview

> What a seller is and the rules that apply to all of its endpoints.

A **seller** is your customer: the merchant who will connect marketplaces through you. They have no login on marketapi. Each seller belongs to one account, and only that account can see it.

How the Seller connects to every other resource, with diagrams: [Data model](/docs/data-model).

## Endpoints

| Method   | Path               | What it does                            |
| -------- | ------------------ | --------------------------------------- |
| `POST`   | `/v1/sellers`      | [Create a seller](/docs/sellers/create) |
| `GET`    | `/v1/sellers`      | [List sellers](/docs/sellers/list)      |
| `GET`    | `/v1/sellers/{id}` | [Get a seller](/docs/sellers/get)       |
| `PATCH`  | `/v1/sellers/{id}` | [Update a seller](/docs/sellers/update) |
| `DELETE` | `/v1/sellers/{id}` | [Delete a seller](/docs/sellers/delete) |

All of them require `Authorization: Bearer mk_your_api_key`. See [Authentication](/docs/authentication).

## A seller's connections

Every seller carries `connections`: the short list of the marketplace accounts they have connected and working (`status: active`), newest first, with `id`, `marketplace`, `marketplaceDisplayName` and `status`. Empty when there is none. It is enough to know **where** the seller is connected straight from the listing, with no second call.

```json
"connections": [
  { "id": "9a1b2c3d-…", "marketplace": "mercadolivre", "marketplaceDisplayName": "Mercado Livre", "status": "active" }
]
```

The full record (external account, scopes, token expiry) and the other states (`revoked`, `error`) live in [List connections](/docs/connections/list), with the seller's `id` in the `Seller-Id` header:

```bash
curl "$BACKEND_URL/v1/connections?status=active" \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Seller-Id: $SELLER_ID"
```

There is no `GET /v1/sellers/{id}/connections`: the seller goes in the path when the operation creates something of theirs (like `POST /v1/sellers/{id}/link-sessions`) and in the header when it lists by seller. To connect a new account, create a [link session](/docs/link-sessions/create).

## Two ids: `id` and `externalId`

* **`id`** is ours: a UUID generated at creation. It is the one that goes in the URL (`/v1/sellers/{id}`).
* **`externalId`** is yours, and optional. Use your customer's id in your system to find the record without storing ours.

`externalId` is unique **within your account**. Another account may use the same value. Repeating a value in your account returns `409 CONFLICT`.

## A seller from another account

An `id` that does not exist and an `id` that belongs to another account get the same answer: `404 NOT_FOUND`. This way nobody can discover ids of other accounts.

## Metadata

Store your own data on the seller (plan, region, CRM id, tags) and filter the list by it. See [Metadata](/docs/sellers/metadata).

## Pagination

The list returns one page at a time, from the newest seller to the oldest. You choose the page with `page` (starting at 1) and the size with `pageSize` (1 to 100, default 20). The `pagination` of the response has the totals (`totalItems`, `totalPages`) and tells if there is a page after (`hasNext`) and before (`hasPrev`). The step by step is in [List sellers](/docs/sellers/list).

## Deletion

Deleting a seller is **permanent**: there is no trash and no undo.

A seller with an active connection cannot be deleted: the response is `409 CONFLICT` with `details.reason = "active_connections"`. Disconnect their connections first ([Disconnect](/docs/connections/disconnect)).

---

# Metadata

URL: https://marketapi.shop/docs/sellers/metadata

> Store your own data on a seller (plan, region, CRM id, tags) and filter the list by it.

`metadata` is a space of **yours** inside the seller. You store what you need as keys and values, then list the sellers filtering by that data.

Examples: the customer's plan, the region, their id in your CRM, tags like `vip` and `b2b`.

```json
{
  "id": "6f0c…",
  "name": "Acme Store",
  "metadata": { "plan": "pro", "region": "south", "tags": ["vip", "b2b"] }
}
```

Every seller carries `metadata`. With nothing stored, it is `{}`. The value of each key can be any JSON: text, number, `true`/`false`, array or object.

## Write

### On create (`POST /v1/sellers`)

Send `metadata` in the body. Without `metadata`, the seller is born with `{}`.

**The `null` rule:** `null` at the top level means "no such key". The key is simply not stored.

| You send                | The seller ends up with |
| ----------------------- | ----------------------- |
| (no `metadata`)         | `{}`                    |
| `"metadata": null`      | `{}`                    |
| `{ "a": 1, "b": null }` | `{ "a": 1 }`            |

```bash
curl -X POST "$BACKEND_URL/v1/sellers" \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Acme Store", "metadata": { "plan": "pro", "region": null } }'
```

Response (`region` was not stored):

```json
{ "id": "6f0c…", "name": "Acme Store", "metadata": { "plan": "pro" } }
```

### On update (`PATCH /v1/sellers/{id}`)

`PATCH` **merges** at the top level. Only the keys you send change.

Before: `{ "plan": "free", "region": "south", "tags": ["vip"] }`

| You send                          | After                                                    | What happened                           |
| --------------------------------- | -------------------------------------------------------- | --------------------------------------- |
| (no `metadata`)                   | `{ "plan": "free", "region": "south", "tags": ["vip"] }` | nothing changes                         |
| `"metadata": { "plan": "pro" }`   | `{ "plan": "pro", "region": "south", "tags": ["vip"] }`  | `plan` changed; the others stayed       |
| `"metadata": { "region": null }`  | `{ "plan": "free", "tags": ["vip"] }`                    | `region` was removed                    |
| `"metadata": { "tags": ["b2b"] }` | `{ "plan": "free", "region": "south", "tags": ["b2b"] }` | a key you send replaces the whole value |
| `"metadata": {}`                  | `{ "plan": "free", "region": "south", "tags": ["vip"] }` | no key sent, nothing changes            |
| `"metadata": null`                | `{}`                                                     | clears everything                       |

Three points to avoid mistakes (and a warning: **key order is not preserved**, the database may return the keys in another order; do not rely on it):

* **A missing `metadata`** changes nothing. **`metadata: null`** clears everything. They are not the same.
* **The merge is top-level only.** An object you send replaces the previous one whole, with no joining inside. Before `{ "address": { "city": "Porto Alegre", "state": "RS" } }`, you send `{ "address": { "state": "SC" } }`, after `{ "address": { "state": "SC" } }` (`city` is gone).
* **`null` inside an object or array is a normal value.** Only a `null` directly on a top-level key removes it.

```bash
curl -X PATCH "$BACKEND_URL/v1/sellers/$SELLER_ID" \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "metadata": { "plan": "max", "region": null } }'
```

## Filter the list

`GET /v1/sellers` accepts `metadata[key]=value`. In `curl`, the brackets need `-g` (or `--globoff`); without it `curl` tries to interpret them. In code, you don't need it.

One key:

```bash
curl -g "$BACKEND_URL/v1/sellers?metadata[plan]=pro" \
  -H "Authorization: Bearer mk_your_api_key"
```

**OR:** repeating the same key accepts any of the values. Here, plan `pro` or `max`:

```bash
curl -g "$BACKEND_URL/v1/sellers?metadata[plan]=pro&metadata[plan]=max" \
  -H "Authorization: Bearer mk_your_api_key"
```

**AND:** different keys must all match. Here, plan `pro` and region `south`:

```bash
curl -g "$BACKEND_URL/v1/sellers?metadata[plan]=pro&metadata[region]=south" \
  -H "Authorization: Bearer mk_your_api_key"
```

The rules, in a list:

* Stored simple value (text, number, boolean): it must be equal.
* Stored array: it must contain at least one of the requested values.
* A **number** compares by value: `metadata[stores]=3` and `metadata[stores]=3.0` both find the number `3`.
* **Text** compares exactly, case counts: `metadata[stores]=3` finds the text `"3"`, but does **not** find `"3.0"`.
* `true` and `false` find the boolean and the text `"true"`/`"false"`.
* A filter value with the NUL character (`metadata[plan]=%00`) returns `400`. A huge number (`metadata[x]=1e400`) is not an error: it compares only as text.
* An object never matches. A key the seller does not have never matches.
* It combines with `externalId` and `sort`. `pagination.totalItems` counts only the filtered sellers.

### Table of cases

Each row is a seller with the `metadata` of the first column, and the filter of the second.

<SellerMetadataFilterCases />

### A malformed filter returns 400

A filter that does not follow `metadata[key]=value` is refused, not ignored. If we ignored it, you would get every seller without noticing. These return `400`: `metadata=x`, `metadata[]=x`, `metadata[a][b]=x` and `metadata[__proto__]=x`.

Filter limits:

<SellerMetadataLimits scope="filter" />

## Limits

They apply to the `metadata` **after the merge**, that is, to what the seller will have in the end.

<SellerMetadataLimits scope="write" />

The limit of 50 keys and the key-name format apply to the **top-level** keys. Keys inside nested objects are free: they count only for size and depth.

Depth: `{"a":1}` is 1 level, `{"a":{"b":1}}` is 2, and an array counts as a level.

A top-level `null` value does not count as a key: it only removes.

Also `400`: text with the NUL character (`\u0000`) or an unpaired surrogate, in any value or nested key, and a number that does not fit JSON, like `1e400`.

**Big numbers:** every number goes through the API's JSON, so an integer above 2^53 is stored with JavaScript precision: `12345678901234567890` becomes `12345678901234567000`, and the filter compares the same way. If the id does not fit in a number, store it as text.

### When it goes over a limit

The response is `400 VALIDATION_ERROR`, and nothing is stored. `details` lists the problems, with `path` starting at `metadata`. Example: a `PATCH` that would leave the seller with 51 keys.

```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request.",
    "details": [
      {
        "code": "custom",
        "path": ["metadata"],
        "message": "Metadata has 51 keys; the maximum is 50."
      }
    ]
  }
}
```

Other messages: `Metadata text cannot contain the NUL character (\u0000) or an unpaired surrogate.`, `Metadata has a number too large to store as JSON.`, `Metadata keys are 1 to 40 letters, digits, "_" or "-".` (invalid key, with the key name in `path`), `Metadata is nested more than 5 levels deep.` and `Metadata takes 9000 bytes as JSON; the maximum is 8192.`. See the general error format in [Errors](/docs/errors).

---

# Create a seller

URL: https://marketapi.shop/docs/sellers/create
Endpoint: `POST /v1/sellers`

> Registers a merchant in your account.

Creates a seller. Call it when a new customer joins your platform. The response carries the seller's `id`, which you use in the other endpoints.

If you'd rather not store our `id`, send your customer's id in your system as `externalId`. It must be unique within your account.

To store your own data on the seller, use `metadata`. See [Metadata](/docs/sellers/metadata).

Every seller is born with a [default warehouse](/docs/warehouses/overview), created in the same operation. So, right after `seller.created`, a `warehouse.created` arrives too.

## Retry without duplicates

Networks fail. Send an `Idempotency-Key` (a UUID works) and, if the response doesn't arrive, repeat the request **with the same key and the same body**. You get the original response, with the header `Idempotency-Replayed: true`, and no new seller is created. The key is valid for 24 hours. Details in [Idempotency and limits](/docs/idempotency-and-limits).

## Common errors

* **`409 CONFLICT` with `details.field = "externalId"`**: a seller with this `externalId` already exists in your account.
* **`409 CONFLICT` without `details`**: the `Idempotency-Key` was already used with a different body. Generate a new key for a new request.
* **`400 VALIDATION_ERROR`**: invalid body, for example an empty `name`. `details` lists each field with a problem.

---

# List sellers

URL: https://marketapi.shop/docs/sellers/list
Endpoint: `GET /v1/sellers`

> Lists the sellers of your account, one page at a time.

Returns the sellers of your account in pages, from the newest to the oldest by default. Use it to sync your records with ours or to build a list on your screen.

## Sorting

`sort` picks the order. One parameter; a leading minus means descending.

| `sort`       | Order                                 |
| ------------ | ------------------------------------- |
| `-createdAt` | newest first (default)                |
| `createdAt`  | oldest first                          |
| `name`       | by name, A to Z, ignoring letter case |
| `-name`      | by name, Z to A                       |

Ties are broken by `id`, so the order is stable across pages.

```bash
curl "$BACKEND_URL/v1/sellers?sort=name&pageSize=50" \
  -H "Authorization: Bearer mk_your_api_key"
```

## Filtering

`externalId` returns only the seller with that external id, compared exactly. It is unique within your account, so `data` has one item or none, and `pagination.totalItems` is 1 or 0. It is the way to find your customer's record from your own id without storing ours.

```bash
curl "$BACKEND_URL/v1/sellers?externalId=customer-123" \
  -H "Authorization: Bearer mk_your_api_key"
```

`metadata[key]=value` filters by the seller's metadata. See [Metadata](/docs/sellers/metadata).

## Go through all pages

1. Ask for page 1 (`page=1`). Use `pageSize` for the size (1 to 100, default 20).
2. Read the `pagination` of the response: `totalPages` tells how many pages there are, and `hasNext` tells if there is one after this.
3. While `hasNext` comes back `true`, ask for `page + 1`.

```bash
# first page
curl "$BACKEND_URL/v1/sellers?page=1&pageSize=50" \
  -H "Authorization: Bearer mk_your_api_key"

# next page
curl "$BACKEND_URL/v1/sellers?page=2&pageSize=50" \
  -H "Authorization: Bearer mk_your_api_key"
```

With 3 sellers and `pageSize=2`, page 1 brings 2 sellers in `data` and this `pagination`:

```json
{
  "page": 1,
  "pageSize": 2,
  "totalItems": 3,
  "totalPages": 2,
  "hasNext": true,
  "hasPrev": false
}
```

## A page after the last one

Asking for a page past the end (in the example, `page=3`) is not an error. The response is `200` with `data: []` and a `pagination` with the real totals, which show where the list ends.

## Common errors

* **`400 VALIDATION_ERROR`**: `page` below 1, `pageSize` outside 1 to 100, a value that is not an integer, a `sort` outside the table above, or an empty `externalId`. `details` points at the wrong parameter (`path: ["page"]`).

## A seller created while you read

If a seller is created while you go through the pages, the others move down one position. The last seller of a page shows up again at the start of the next one. Use the `id` so you don't process the same seller twice.

---

# Get a seller

URL: https://marketapi.shop/docs/sellers/get
Endpoint: `GET /v1/sellers/{id}`

> Reads a seller by id.

Returns a seller of your account by the `id` the API generated at creation.

The response has the seller's data only. The marketplace accounts they connected come from [List connections](/docs/connections/list), with this `id` in the `Seller-Id` header.

## When the response is `404`

The seller does not exist, or it exists but belongs to **another account**. Both cases get the same answer on purpose, so nobody can discover ids of other accounts. An `id` that is not a UUID returns `400 VALIDATION_ERROR`.

---

# Update a seller

URL: https://marketapi.shop/docs/sellers/update
Endpoint: `PATCH /v1/sellers/{id}`

> Changes the name or the externalId of a seller.

Changes a seller of your account. **Only the fields you send change**. Whatever you leave out stays as it is.

## How to send each field

| You send                | What happens                                               |
| ----------------------- | ---------------------------------------------------------- |
| `"name": "New name"`    | Changes the name.                                          |
| `"name": null`          | `400 VALIDATION_ERROR`: the name can't be empty.           |
| `"externalId": "abc-1"` | Changes the `externalId`. Must be unique in your account.  |
| `"externalId": null`    | Clears the `externalId`.                                   |
| `{}` (empty body)       | Nothing changes. The response returns the seller as it is. |

`metadata` is merged key by key, and `null` removes a key. The rules and examples are in [Metadata](/docs/sellers/metadata).

Repeating the same `PATCH` gives the same result, so there is no `Idempotency-Key` here.

## Common errors

* **`409 CONFLICT`**: another seller in your account already uses this `externalId`.
* **`404 NOT_FOUND`**: the seller does not exist or belongs to another account.

---

# Delete a seller

URL: https://marketapi.shop/docs/sellers/delete
Endpoint: `DELETE /v1/sellers/{id}`

> Removes a seller from your account, for good.

Removes a seller from your account. The response is `204`, with no body.

Everything the seller owns goes with it: link sessions, disconnected connections, products, Variants and kits, warehouses, stock balances and movements, marketplace content (MarketplaceProducts), listings and their bindings, and the buyer data kept for it. The listings **stay published at the marketplace**: deletion only removes the data here.

<Callout type="warn">
  Deletion is **permanent**. There is no trash and no undo.
</Callout>

Repeating the request for a seller already deleted returns `404 NOT_FOUND`. Treat that `404` as success if your code may retry the call.

A seller with an active connection cannot be deleted: the response is `409 CONFLICT` with `details.reason = "active_connections"`. Disconnect first ([Disconnect](/docs/connections/disconnect)).

---

# Overview

URL: https://marketapi.shop/docs/link-sessions/overview

> What a link session is and the rules that apply to all of its endpoints.

A **link session** is the hosted page where the seller connects their marketplace accounts. You create the session through the API, send the `url` to the seller, and they authorize access there, with no marketapi login. Each authorized account becomes a [connection](/docs/connections/overview).

The states of a link session and of the Connection it creates, as diagrams: [States and events](/docs/data-model/states-and-events).

Step by step, from a created seller to a connected account: [Connecting a seller](/docs/connecting-a-seller).

## Endpoints

| Method   | Path                             | What it does                                        |
| -------- | -------------------------------- | --------------------------------------------------- |
| `POST`   | `/v1/sellers/{id}/link-sessions` | [Create a link session](/docs/link-sessions/create) |
| `GET`    | `/v1/link-sessions/{id}`         | [Get a link session](/docs/link-sessions/get)       |
| `DELETE` | `/v1/link-sessions/{id}`         | [Revoke](/docs/link-sessions/revoke)                |

All of them require `Authorization: Bearer mk_your_api_key`. See [Authentication](/docs/authentication).

## The seller goes in the path

A session always belongs to one seller, so their `id` goes in the creation URL (`/v1/sellers/{id}/link-sessions`). After that, the session has its own `id`.

## The `url` shows up once

The page `url` only comes in the creation response (and in the replay of the same `Idempotency-Key`, for 24 h). We keep only a hash of it. Lost it? Create another session.

## The session app

The `app` field (`auto` or `platform`) keeps the choice made at creation. A connection keeps the app it was made with.

## States

| `status`    | Meaning                                                                  |
| ----------- | ------------------------------------------------------------------------ |
| `pending`   | open, within its deadline                                                |
| `completed` | the seller clicked **Finish**; `connectionIds` lists what they connected |
| `expired`   | passed `expiresAt` without finishing                                     |
| `revoked`   | you revoked it                                                           |

---

# Create a link session

URL: https://marketapi.shop/docs/link-sessions/create
Endpoint: `POST /v1/sellers/{id}/link-sessions`

> Generates the links for the seller to connect marketplaces, through our page or a button in your app.

Creates the connect session of a seller and returns two kinds of link. Both share the same deadline and show up **only in this response**.

* **`url`**, the hosted page. Send it to the seller (email, message). They pick the marketplace, authorize on the marketplace screen and come back to the page, which shows the connected account.
* **`connectUrls`**, one link per marketplace to put behind **your own** "Connect" button. Opening it takes the seller straight to the marketplace consent screen; after authorizing, they land on your `redirectUrl` with `?connected=<marketplace>` (or `?error=<code>`). Our page is never in the way. Without a `redirectUrl`, they land on the hosted page instead.

Either way, exchanging the code for a token and renewing it is on us: the seller only sees the marketplace and your app.

* `allowedMarketplaces` limits the marketplaces offered and the keys of `connectUrls`. Without it, every marketplace available in this environment is offered.
* `redirectUrl` is where the seller lands at the end: after the **Finish** button of the page, or right after the marketplace when they came through a `connectUrls` link. It must be `https`.
* `app` picks the marketplace app the seller authorizes: `auto` (default) uses your own app (BYOK) when you registered one for that marketplace, and ours otherwise; `platform` always uses ours. A connection keeps the app it was made with. See [BYOK](/docs/own-apps).
* `expiresInMs` is the deadline of the links: from 1 to 5 min, default 5 min. Create the session when the seller is about to click, not earlier.

Send an `Idempotency-Key`: if the response is lost, repeating the request with the same key returns the same links, without creating another session.

## `?error=` codes

| Code                      | Meaning                                                                                                               |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `authorization_denied`    | The seller did not authorize on the marketplace screen.                                                               |
| `authorization_failed`    | The marketplace refused the code exchange (account without administrator rights, expired code). Create a new session. |
| `session_unavailable`     | The session expired, was revoked or completed before the seller finished.                                             |
| `marketplace_unavailable` | The marketplace is not available in this environment or in this session.                                              |

## Common errors

* **`400 VALIDATION_ERROR` with `details.unavailable`**: a requested marketplace is not available in this environment.
* **`400 VALIDATION_ERROR` with `details.field` set to `redirectUrl`**: the `redirectUrl` is not `https`.
* **`400 VALIDATION_ERROR`** without `details`: `expiresInMs` outside 1 to 5 min.
* **`404 NOT_FOUND`**: the seller in the path does not exist or belongs to another account.

---

# Get a link session

URL: https://marketapi.shop/docs/link-sessions/get
Endpoint: `GET /v1/link-sessions/{id}`

> Reads the state of a link session and the connections made through it.

Returns the session by its `id`, with its `status` and the `connectionIds` of the accounts the seller connected through it. The `url` does not come back here.

`status` is computed on read: a pending session past `expiresAt` comes back `expired`. To know right away when the seller finishes, receive the `link_session.completed` event on your webhook.

---

# Revoke

URL: https://marketapi.shop/docs/link-sessions/revoke
Endpoint: `DELETE /v1/link-sessions/{id}`

> Cancels a pending link session.

Cancels a session that is still pending: the page stops working right away, and the seller sees "This link was canceled". The response is `204`, with no body. Connections already made through the session stay.

Only a `pending` session can be revoked. One that is completed, expired or already revoked returns `409 CONFLICT`, with the current state in `details.status`.

---

# Overview

URL: https://marketapi.shop/docs/connections/overview

> A connected marketplace account and the control point for its operations.

A **Connection** is a marketplace account linked to a Seller: for example, one Mercado Livre account. It is created when the Seller authorizes access through a [link session](/docs/link-sessions/create), the hosted page, or a button embedded in your app. The Connection contains protected credentials, external account identity, and import rules; Products, Variants, and Listings remain separate resources.

How the Connection connects to listings and to the catalog, with diagrams: [Connections and listings](/docs/data-model/connections-and-listings).

## From connected seller to synchronized listing

1. Create a link session and let the Seller authorize the marketplace. Then confirm that the Connection is <code>active</code>.
2. Read [Connection capabilities](/docs/connections/capabilities). They are the actual execution contract of that adapter — do not assume every channel can publish, import, or update inventory.
3. Choose the [import policy](/docs/connections/update-listing-import-policy): observe only, link exact matches, or create a minimum record when no match exists.
4. [Import existing listings](/docs/connections/import-listings). The response is a Job; results become Listings with external state in <code>observed</code>.
5. Review unlinked Listings, associate their Variants, and configure synchronization policy. To recover from a missed notification or check the channel again, [reconcile](/docs/connections/reconcile-listings).
6. Use Listing and Inventory endpoints to edit locally and publish only when capability and policy allow it.

## Current support

The indicators below show static product availability. Read the [complete marketplace matrix](/docs/marketplace-support) for every feature; before a command, the [Connection capabilities](/docs/connections/capabilities) response remains the execution decision.

<MarketplaceFeatureTags feature="connect" />

<MarketplaceFeatureTags feature="byoApp" />

<MarketplaceFeatureTags feature="notifications" />

<MarketplaceCapabilityTags capability="listings.import" />

## Connection endpoints

| Goal                                                                      | Endpoint                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| See a Seller's Connections or one Connection                              | [List](/docs/connections/list) · [get](/docs/connections/get)                                                                                                                                                                                                                                                                                                                                                                      |
| Discover what the adapter actually supports                               | [Connection capabilities](/docs/connections/capabilities)                                                                                                                                                                                                                                                                                                                                                                          |
| Define how listings are imported and linked                               | [Get](/docs/connections/listing-import-policy) · [update](/docs/connections/update-listing-import-policy)                                                                                                                                                                                                                                                                                                                          |
| Read external listings now                                                | [Import](/docs/connections/import-listings)                                                                                                                                                                                                                                                                                                                                                                                        |
| Revalidate external snapshot later                                        | [Reconcile](/docs/connections/reconcile-listings)                                                                                                                                                                                                                                                                                                                                                                                  |
| See how each resource is kept in sync                                     | [Synchronization state](/docs/connections/sync-state)                                                                                                                                                                                                                                                                                                                                                                              |
| Discover categories, requirements, brands, types, size charts and catalog | [Categories](/docs/connections/listing-categories) · [requirements](/docs/connections/listing-requirements) · [brands](/docs/connections/brands) · [product types](/docs/connections/product-types) · [size chart](/docs/connections/size-chart-requirements) · [delivery modes](/docs/connections/fulfillment-options) · [catalog](/docs/connections/catalog-products) · [catalog product](/docs/connections/get-catalog-product) |
| See stock locations and link warehouses to them                           | [Locations](/docs/connections/stock-locations) · [mapping](/docs/connections/fulfillment-mapping) · [update mapping](/docs/connections/update-fulfillment-mapping)                                                                                                                                                                                                                                                                 |
| Bring in orders and set how they move stock                               | [Import](/docs/connections/import-orders) · [reconcile](/docs/connections/reconcile-orders) · [policy](/docs/connections/order-policy) · [update policy](/docs/connections/update-order-policy)                                                                                                                                                                                                                                    |
| See the account's reputation                                              | [Health](/docs/connections/health)                                                                                                                                                                                                                                                                                                                                                                                                 |
| Use what has no common model, with no proxy                               | [List](/docs/connections/list-operations) · [describe](/docs/connections/get-operation) · [run](/docs/connections/run-operation)                                                                                                                                                                                                                                                                                                   |
| Options only one marketplace has                                          | [Get](/docs/connections/marketplace-options) · [update](/docs/connections/update-marketplace-options)                                                                                                                                                                                                                                                                                                                              |
| End account access                                                        | [Disconnect](/docs/connections/disconnect)                                                                                                                                                                                                                                                                                                                                                                                         |

**Every listing is per Seller.** To see what a Seller has connected, send their id in the <code>Seller-Id</code> header:

```bash
curl "$BACKEND_URL/v1/connections?status=active" \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Seller-Id: $SELLER_ID"
```

Every endpoint requires <code>Authorization: Bearer mk_your_api_key</code>. See [Authentication](/docs/authentication). The <code>Seller-Id</code> header exists only for listing; to read, operate, or disconnect a Connection, its id is sufficient.

## States

| status               | Meaning                                                     | What to do                              |
| -------------------- | ----------------------------------------------------------- | --------------------------------------- |
| <code>active</code>  | usable credentials and permitted operations                 | may import/publish if capability exists |
| <code>revoked</code> | client or marketplace ended access                          | create a new link session to reconnect  |
| <code>error</code>   | an operation failed definitively without proving revocation | reconnect and investigate Job/webhook   |

<code>revokedReason: "client"</code> indicates a disconnect you ordered;
<code>"marketplace"</code> indicates the channel removed or refused to renew
access. Reconnecting the same external account restores the same Connection and
its id; status changes arrive by webhook. Temporary failures do not make a
Connection inactive by themselves: Jobs may retry.

## App and isolation

<code>app</code> says whether the Seller authorized the platform app (
<code>platform</code>) or the app credential you registered (<code>own</code>,
BYOK). A Connection retains that choice until its next reconnection. Connection
IDs from another account respond as nonexistent (<code>404</code>) so they never
disclose third-party data.

---

# List connections

URL: https://marketapi.shop/docs/connections/list
Endpoint: `GET /v1/connections`

> Lists the connections of a seller, one page at a time.

Returns the connections of a seller, from the newest to the oldest. The seller goes in the `Seller-Id` header.

```bash
curl "$BACKEND_URL/v1/connections?status=active&page=1&pageSize=20" \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Seller-Id: $SELLER_ID"
```

Use `status` to get only one state (for example, `active`). Pagination works as in [List sellers](/docs/sellers/list): read `pagination.hasNext` and ask for `page + 1`.

## Common errors

* **`400 VALIDATION_ERROR` with `details.header = "Seller-Id"`**: the header is missing or is not a UUID.
* **`404 NOT_FOUND`**: the seller in `Seller-Id` does not exist or belongs to another account.

---

# Get a connection

URL: https://marketapi.shop/docs/connections/get
Endpoint: `GET /v1/connections/{id}`

> Reads a connection by id.

Returns a connection of your account by its `id`. No `Seller-Id` needed: the `id` already identifies the connection.

Use it to check the `status` after a `connection.updated` or `connection.revoked` event. A connection from another account returns `404`, the same as one that does not exist.

---

# Listing import policy

URL: https://marketapi.shop/docs/connections/listing-import-policy
Endpoint: `GET /v1/connections/{id}/listing-import-policy`

> Gets how this Connection links imported listings.

This policy belongs to the Connection, not the entire account. You can therefore import one store conservatively and another with assistance. It decides only what happens when an external offer has no local binding; it does not change listings already imported and does not publish anything.

| Policy                       | Result for each external variation                                                                                   |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| <code>unlinked</code>        | creates/updates the observed MarketplaceProduct and Listing without Product, Variant, variation, or binding creation |
| <code>match_only</code>      | creates the MarketplaceProduct variation and the binding only when SKU or GTIN finds **one unique** local Variant    |
| <code>match_or_create</code> | uses the unique match; if there is none, creates minimum Product/Variant records, the variation, and the binding     |

Matching never uses a title, copy, or similarity. That prevents products with similar names from sharing quantity by accident. If SKU and GTIN find more than one candidate Variant, nothing is bound or created, under any policy, and the Listing becomes <code>linkState: ambiguous</code>; correct identifiers or bind manually. A variation the MarketplaceProduct already knows by its external id is reused, without a new match.

With <code>match_or_create</code>, the created Product is a quarantine base: technical title, <code>metadata.imported: true</code>, <code>disabled</code> inventory, and options that preserve external identity. Complete and review these fields before treating an imported base as canonical catalog data.

---

# Update listing import policy

URL: https://marketapi.shop/docs/connections/update-listing-import-policy
Endpoint: `PATCH /v1/connections/{id}/listing-import-policy`

> Sets the linking policy for future imports of this Connection.

Change this setting **before** requesting import or reconciliation. The body has one decision, <code>policy</code>, but it has important effects on automatic Product, Variant, and binding creation for all future external offers of that Connection.

Choose <code>unlinked</code> for inspection first; <code>match_only</code> when your local SKUs/GTINs are reliable; and <code>match_or_create</code> when you accept minimum quarantine records for every variation that has no unique match. A title never enters the algorithm.

The change is local and returns the updated Connection. It does not start a Job, read the marketplace, change an existing Listing policy, or repair bindings already created. After changing it, deliberately [import listings](/docs/connections/import-listings) or [reconcile](/docs/connections/reconcile-listings).

---

# Import listings

URL: https://marketapi.shop/docs/connections/import-listings
Endpoint: `POST /v1/connections/{id}/listing-imports`

> Queues a complete read of a Connection's external offers.

Use import to bring offers that already exist in the marketplace into MarketAPI. The Connection must be <code>active</code> and announce <code>import: true</code>. The <code>202</code> response creates a Job; it does not mean every listing is already locally available. Track items in [Jobs](/docs/jobs/items) and Listing webhooks.

The worker enumerates external offers, records each normalized snapshot in <code>observed</code>, updates <code>marketplaceStatus</code>, and applies the current [import policy](/docs/connections/listing-import-policy) to decide bindings. External data does not overwrite existing Product or Variant records; marketplace titles are not used for linking.

An imported Listing starts with outbound off and manual inventory so nothing is republished accidentally. Review <code>unlinked</code> Listings or <code>imported</code> bases, associate Variants when needed, and enable each Listing policy only after that review.

## Common errors

* **`409 CONFLICT` with `details.reason = "connection_inactive"`**: the Connection is not active (for example, it was revoked). Ask the seller to connect again.

---

# Reconcile listings

URL: https://marketapi.shop/docs/connections/reconcile-listings
Endpoint: `POST /v1/connections/{id}/listing-reconciliations`

> Queues a new external read to check observed state.

Reconciliation performs the same safe external read as import, but its operational intent is to **compare again** the marketplace with known state. Use it after an outage, suspected missed notification, manual store review, or a window when your application was offline.

The call is external-read-only: it does not publish <code>desired</code>, deliver inventory, or replace Product/Variant with marketplace data. The response is a Job, and each offer's normalized snapshot is updated in <code>observed</code>. The Connection import policy decides only how new bindings are handled; it does not turn Product or Variant into marketplace data.

Do not use reconciliation as high-frequency polling. Prefer inbound notifications for normal changes and use this endpoint as a verifiable repair operation, tracking the Job and then [synchronization state](/docs/connections/sync-state).

## Common errors

* **`409 CONFLICT` with `details.reason = "connection_inactive"`**: the Connection is not active (for example, it was revoked). Ask the seller to connect again.

---

# Import orders

URL: https://marketapi.shop/docs/connections/import-orders
Endpoint: `POST /v1/connections/{id}/order-imports`

> Brings in the marketplace orders of a period.

Creates an `order.import` Job and answers `202`. The job reads the marketplace orders in the background, and each one becomes an [order](/docs/orders/get) with the `order.*` events. The Connection must be `active`, and the marketplace must offer `orders.import` (see the [capabilities](/docs/connections/capabilities)). Otherwise the answer is `422 CAPABILITY_UNSUPPORTED`, with no job created.

**The JSON body is required.** Send `{}` to import the last 30 days:

```bash
curl -X POST "$BACKEND_URL/v1/connections/$CONNECTION_ID/order-imports" \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{}'
```

For another period, send `placedFrom` (inclusive) and `placedTo` (exclusive), on the purchase date:

```json
{
  "placedFrom": "2026-09-01T00:00:00-03:00",
  "placedTo": "2026-10-01T00:00:00-03:00"
}
```

The period already resolved, in absolute instants, comes back in the Job `input`. Importing again is safe: an order already stored is updated, never duplicated, and what did not change sends no event.

The import reads a window of dates, not the whole resource, so it does **not** move the [synchronization state](/docs/connections/sync-state) checkpoint. For that, use the [reconciliation](/docs/connections/reconcile-orders). Follow the Job in [Jobs](/docs/tracking-jobs). Use the `Idempotency-Key` header.

Part of the [order lifecycle](/docs/how-it-works/order-lifecycle).

---

# Reconcile orders

URL: https://marketapi.shop/docs/connections/reconcile-orders
Endpoint: `POST /v1/connections/{id}/order-reconciliations`

> Brings in the orders that changed on the marketplace since the last pass.

Creates an `order.reconcile` Job and answers `202`, with no body. It recovers a missed notification: it reads what the marketplace changed since the last complete pass (the first time, the last 30 days) and applies each order. A new order sends `order.created`; an order that changed sends `order.updated` or `order.cancelled`. The Connection must be `active`, and the marketplace must offer `orders.import` (see the [capabilities](/docs/connections/capabilities)). Otherwise the answer is `422 CAPABILITY_UNSUPPORTED`, with no job created.

It is a repair, not polling. Use it after an outage or when you suspect a lost notification. For normal changes, rely on the marketplace notifications. To bring in a specific period, use the [import](/docs/connections/import-orders).

```bash
curl -X POST "$BACKEND_URL/v1/connections/$CONNECTION_ID/order-reconciliations" \
  -H "Authorization: Bearer mk_your_api_key"
```

When the pass ends, the [synchronization state](/docs/connections/sync-state) moves forward on the `orders` resource (`lastFullSyncAt` and `checkpoint`). If it fails, the reason stays in `lastError`. Follow the Job in [Jobs](/docs/tracking-jobs). Use the `Idempotency-Key` header.

Part of the [order lifecycle](/docs/how-it-works/order-lifecycle).

---

# Get order policy

URL: https://marketapi.shop/docs/connections/order-policy
Endpoint: `GET /v1/connections/{id}/order-policy` (coming soon)

> Shows how the Connection's orders move stock.

The order policy decides whether a sale in the marketplace moves local stock. With it on (`inventory.enabled`), for a line linked to a Variant that marketapi controls:

* paid order: reserves stock;
* shipped order: writes stock off;
* cancelled order: releases the reservation.

`warehouseId` is the warehouse used; `null` means the seller's default warehouse. `enabledSince` is read-only: orders placed before that date never move stock, so the historical import does not reprocess the past. To change it, use [Update order policy](/docs/connections/update-order-policy).

Part of the [order lifecycle](/docs/how-it-works/order-lifecycle).

---

# Update order policy

URL: https://marketapi.shop/docs/connections/update-order-policy
Endpoint: `PATCH /v1/connections/{id}/order-policy` (coming soon)

> Turns the order policy on or off, or changes its warehouse.

Send `inventory.enabled` and/or `inventory.warehouseId`. Turning it off (`enabled: false`) stops moving stock per order. Turning it back on (`false` → `true`) moves `enabledSince` to now: the past is not reprocessed.

Errors: `404` if the warehouse does not exist or belongs to another seller; `409` (`warehouse_archived`) if it is archived. A warehouse used by the policy cannot be archived before you change the policy.

The change fires the `connection.updated` webhook. See the effect of each state in [Orders](/docs/orders/overview).

Part of the [order lifecycle](/docs/how-it-works/order-lifecycle).

---

# Synchronization state

URL: https://marketapi.shop/docs/connections/sync-state
Endpoint: `GET /v1/connections/{id}/sync-state`

> Shows how each Connection resource is kept current.

Returns one entry per synchronized resource. Today there is only `listings`. It is a safe technical view: it never contains tokens, credentials or raw marketplace responses. Use it to tell whether a Connection is up to date and when the next check happens.

A resource the marketplace neither notifies nor lets us enumerate **has no entry**. On Mercado Livre, `listings` has no entry until its listing read exists.

## Mode

* `push`: the marketplace notifies changes.
* `poll`: marketapi enumerates the resource periodically.
* `hybrid`: both. The `mock` adapter is `hybrid`.

## Automatic reconciliation

Every 6 hours, for each active Connection, marketapi enumerates the listings at the marketplace and updates `observed` on the ones that already exist locally, using the same rule as the inbound notification. The `listing.external_changed` event is emitted only when the observed state changed.

* **A listing with no local record is not imported.** It counts in `unknownExternalCount`. To bring it in, use [import listings](/docs/connections/import-listings).
* **If a pass fails**, `lastError` holds a safe message and the next pass follows the normal schedule. If the marketplace says the seller's access has ended, the Connection is deactivated, as with any other call.
* **With `syncPolicy.inbound.mode = disabled`**, the listing does not change, and `listing.external_changed` is sent with `behavior: observed_only` on every pass while the marketplace differs from the stored `observed`. To store what is out there and stop the notice, [import](/docs/connections/import-listings) or [reconcile](/docs/connections/reconcile-listings).
* **An import or reconciliation you requested** that finishes sets `lastFullSyncAt` and pushes `nextReconciliationAt` back 6 hours.
* It is an internal job: it does not appear in [`/v1/jobs`](/docs/tracking-jobs) and emits no `job.*` events.

## Connecting and reconnecting

Connecting or reconnecting sets `nextReconciliationAt` to "now". The first pass recovers what changed while the Connection was revoked.

A notification that arrives while the Connection is inactive is **not applied**: its time is kept in `deferredInboundAt`. Compare it with `lastFullSyncAt` to see whether reconciliation has recovered it.

`lastInboundAt` is the last notification for the resource received while the Connection was active.

## Caveats

* `checkpoint` is opaque and changes per marketplace. Do not parse it.
* To know whether a specific request succeeded, the record is the Job returned by [import](/docs/connections/import-listings) or [reconcile](/docs/connections/reconcile-listings). For an offer's state, read the Listing and its `observed`.

---

# Connection capabilities

URL: https://marketapi.shop/docs/connections/capabilities
Endpoint: `GET /v1/connections/{id}/capabilities`

> Tells, operation by operation, what this Connection can do at its marketplace.

Call it before showing an external action in your app. The answer is for this Connection: it combines what the marketplace adapter offers with the scopes the seller granted.

Each operation in `operations` has a `status`:

| `status`      | What it means                                                | What the route answers                                        |
| ------------- | ------------------------------------------------------------ | ------------------------------------------------------------- |
| `available`   | the Connection can run it                                    | proceeds normally                                             |
| `unsupported` | the marketplace does not offer the operation                 | `422 CAPABILITY_UNSUPPORTED`                                  |
| `not_granted` | the marketplace offers it, but a `requiredScopes` is missing | `403 CAPABILITY_NOT_GRANTED`; the seller must authorize again |

In an inventory batch across several Connections, the error is not in the response. The job is created, and only the item of the Connection without the operation fails, with the `code` in its message.

Even with `available`, the marketplace may refuse the operation when it runs: the app lacks the permission for that resource with this seller (on Mercado Livre, a functional permission turned off in the app). The job item fails with `CAPABILITY_NOT_GRANTED` and the same `details`. To fix it, enable the permission in the marketplace app and have the seller authorize again.

`declarations` holds marketplace facts that change how operations behave. Example: `inventory.scope` says whether stock belongs to each offer (`binding`) or is shared by the marketplace product variant (`marketplace_product_variant`).

<Callout>
  Each endpoint page that reaches a marketplace shows support by marketplace at
  the top. That is the overview. This response is the decision for one specific
  Connection.
</Callout>

---

# List categories

URL: https://marketapi.shop/docs/connections/listing-categories
Endpoint: `GET /v1/connections/{id}/listing-categories`

> Browses the marketplace category tree.

Returns the categories where a listing can be published. With no filter you get the root. Go down with `parentId` or search by text with `query`. Only a category with `leaf` and `listingAllowed` true accepts a listing.

```bash
curl "$BACKEND_URL/v1/connections/$CONNECTION_ID/listing-categories?parentId=mock-electronics" \
  -H "Authorization: Bearer mk_your_api_key"
```

Each item has the `path` (root to category) and the native value in `marketplaceData`. Once you pick a category, see what it requires in [Listing requirements](/docs/connections/listing-requirements).

Part of [Publish listings](/docs/how-it-works/publishing).

## Synchronous read

The Connection discovery and catalog routes answer right away, with no Job: you use the answer to build a form. The time limit is the marketplace adapter's. If it does not answer, you get `503 MARKETPLACE_UNAVAILABLE` and can try again; an inactive Connection gives `409`. See [Errors](/docs/errors).

---

# Listing requirements

URL: https://marketapi.shop/docs/connections/listing-requirements
Endpoint: `GET /v1/connections/{id}/listing-requirements`

> Lists the fields a category or product type requires.

Tells you what to fill in to publish. Send `categoryId`, `productType` or both. With none, the answer is `400`. `catalogProductId` narrows the result with what that catalog product already defines.

```bash
curl "$BACKEND_URL/v1/connections/$CONNECTION_ID/listing-requirements?categoryId=mock-cellphones" \
  -H "Authorization: Bearer mk_your_api_key"
```

Each item of `fields` is a field to fill in:

| Field         | What it says                                                                                                                                                                                      |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `path`        | where the value goes: a canonical field (`fiscal.ncm`), an attribute (`attributes[BRAND]`) or a marketplace option (`marketplaceOptions.mock.listingType`)                                        |
| `scope`       | which record carries the value: `product`, `variant`, `marketplace_product`, `marketplace_product_variant`, `listing` or `binding`                                                                |
| `requirement` | `required` (the marketplace refuses without it), `recommended` (weighs on quality or exposure) or `optional`                                                                                      |
| `conditions`  | empty: the requirement always applies. With items: it only applies when **all** conditions are true. Each has `path`, `operator` (`equals`, `not_equals`, `in`, `present`, `absent`) and `values` |

**Example.** `GTIN` is required, unless the seller states why there is none (`EMPTY_GTIN_REASON`):

```json
{
  "path": "attributes[GTIN]",
  "scope": "marketplace_product_variant",
  "requirement": "required",
  "conditions": [
    {
      "path": "attributes[EMPTY_GTIN_REASON]",
      "operator": "absent",
      "values": []
    }
  ]
}
```

Read it like this: `GTIN` is `required` while `attributes[EMPTY_GTIN_REASON]` is missing (`absent`). Once the reason is filled in, the condition stops holding and `GTIN` is no longer required. `present` and `absent` take no `values`.

Each field also has the allowed values, the units (UCUM) and `variationAxis`, which marks a field that becomes a variation, such as color. Outside the fields, `limits` has the category caps, like `maxPictures`. To check a listing against this list, use [Validate listing](/docs/listings/validate).

Part of [Publish listings](/docs/how-it-works/publishing). Synchronous read, no Job, as in [List categories](/docs/connections/listing-categories).

---

# List brands

URL: https://marketapi.shop/docs/connections/brands
Endpoint: `GET /v1/connections/{id}/brands`

> Lists the brands the marketplace accepts.

Returns the brands the marketplace knows, one page at a time. Filter by `categoryId` or search by name with `query`. Use the `id` or `name` when filling the brand attribute required in [Listing requirements](/docs/connections/listing-requirements).

Creating a new brand has no common model: it is a [native operation](/docs/connections/list-operations).

Part of [Publish listings](/docs/how-it-works/publishing). Synchronous read, no Job, as in [List categories](/docs/connections/listing-categories).

---

# List product types

URL: https://marketapi.shop/docs/connections/product-types
Endpoint: `GET /v1/connections/{id}/product-types`

> Lists the marketplace product types.

Returns the product types, one page at a time. Filter by `categoryId` or search by name with `query`. Each type has the `categoryIds` where it applies.

Use it when the marketplace asks for the type, and not only the category: send the type `id` as `productType` in [Listing requirements](/docs/connections/listing-requirements).

Part of [Publish listings](/docs/how-it-works/publishing). Synchronous read, no Job, as in [List categories](/docs/connections/listing-categories).

---

# Size chart requirements

URL: https://marketapi.shop/docs/connections/size-chart-requirements
Endpoint: `GET /v1/connections/{id}/size-chart-requirements`

> Tells whether a category asks for a size chart.

Send `categoryId` and read the `support` field:

| `support`     | What it means                             |
| ------------- | ----------------------------------------- |
| `unsupported` | the category has no size chart            |
| `optional`    | the category accepts a size chart         |
| `required`    | a listing without a size chart is refused |

```bash
curl "$BACKEND_URL/v1/connections/$CONNECTION_ID/size-chart-requirements?categoryId=mock-tshirts" \
  -H "Authorization: Bearer mk_your_api_key"
```

When there is a chart, `measurements` lists the measures of each row, with `key`, `label`, `unit` and `required` (whether every row needs it). With `unsupported`, the list is empty.

Measures come in the API canonical unit: length in `mm` and weight in `g` (`unit` uses UCUM). The adapter converts from what the marketplace uses, such as cm.

Creating or editing the chart has no common model: use a [native operation](/docs/connections/list-operations).

Part of [Publish listings](/docs/how-it-works/publishing). Synchronous read, no Job, as in [List categories](/docs/connections/listing-categories).

---

# Get fulfillment options

URL: https://marketapi.shop/docs/connections/fulfillment-options
Endpoint: `GET /v1/connections/{id}/fulfillment-options`

> Shows the delivery modes the marketplace account has.

Returns one Connection object (not a list, and not paginated) with `options`. Each option has the `mode` (`seller_ship`, `marketplace_logistics`, `marketplace_fulfillment` or `pickup`), the `externalId`, the `label` and whether it is `available` now.

Use it to know whether the seller can choose, for example, shipping by themselves or using the marketplace logistics before publishing.

Part of [Publish listings](/docs/how-it-works/publishing). Synchronous read, no Job, as in [List categories](/docs/connections/listing-categories).

---

# Search catalog products

URL: https://marketapi.shop/docs/connections/catalog-products
Endpoint: `GET /v1/connections/{id}/catalog-products`

> Searches products in the marketplace catalog.

Search by text (`query`, the name) or by code (`identifier`, such as the GTIN). Send one of them; with neither, the answer is `400`. `categoryId` filters. Each result has name, brand, category, `identifiers` and pictures.

To find a product by its GTIN:

```bash
curl "$BACKEND_URL/v1/connections/$CONNECTION_ID/catalog-products?identifier=7890000000017" \
  -H "Authorization: Bearer mk_your_api_key"
```

If the catalog does not have the code, the answer is `200` with empty `data`, not `404`.

Use it when the marketplace asks the listing to point to a catalog product. One product in detail: [Get catalog product](/docs/connections/get-catalog-product). To link a catalog product to a MarketplaceProduct, see [Catalog reference](/docs/marketplace-products/catalog-reference).

Part of [Publish listings](/docs/how-it-works/publishing).

## Synchronous read

Like the other discovery routes, the search answers right away, with no Job, within the adapter's time limit. If the marketplace does not answer, you get `503 MARKETPLACE_UNAVAILABLE` and can try again. See [List categories](/docs/connections/listing-categories) and [Errors](/docs/errors).

---

# Get catalog product

URL: https://marketapi.shop/docs/connections/get-catalog-product
Endpoint: `GET /v1/connections/{id}/catalog-products/{catalogProductId}`

> Gets one product from the marketplace catalog.

Returns the full product: the `attributes` the catalog fixes and the `variants`, each with its `identifiers`. An id the marketplace does not know gives `404`.

The `id` comes from the search in [Search catalog products](/docs/connections/catalog-products).

Part of [Publish listings](/docs/how-it-works/publishing). Synchronous read, no Job, as in [List categories](/docs/connections/listing-categories).

---

# List stock locations

URL: https://marketapi.shop/docs/connections/stock-locations
Endpoint: `GET /v1/connections/{id}/stock-locations` (coming soon)

> Lists the account's stock locations in the marketplace.

Returns the Connection's external locations, paginated. Each has a `type`:

* `seller_location`: a seller location. marketapi publishes stock to it.
* `marketplace_fulfillment`: stock held by the marketplace (Fulfillment, FBA). Read-only: marketapi observes it and never publishes.

You also get `isPrimary` and the `status` (`active` or `missing`, when the marketplace stopped listing the location). The route reads what the last sync saved and does not call the marketplace. To decide which warehouse feeds which location, see [Fulfillment mapping](/docs/connections/fulfillment-mapping).

Part of [Stay in sync](/docs/how-it-works/staying-in-sync).

---

# Get fulfillment mapping

URL: https://marketapi.shop/docs/connections/fulfillment-mapping
Endpoint: `GET /v1/connections/{id}/fulfillment-mapping` (coming soon)

> Shows which warehouse feeds each external location.

The mapping links each external location to the local warehouses that feed it. There is one entry per active `seller_location`, including empty ones. By default, the seller's default warehouse feeds the primary location.

A warehouse feeds at most one location per Connection: otherwise the same piece would be published twice. To change the mapping, use [Update fulfillment mapping](/docs/connections/update-fulfillment-mapping).

Part of [Stay in sync](/docs/how-it-works/staying-in-sync).

---

# Update fulfillment mapping

URL: https://marketapi.shop/docs/connections/update-fulfillment-mapping
Endpoint: `PATCH /v1/connections/{id}/fulfillment-mapping` (coming soon)

> Replaces the warehouses that feed the external locations.

Send `entries` with `locationId` and `warehouseIds`. The locations you send are replaced, the omitted ones stay as they are, and `warehouseIds: []` clears that location's mapping. The answer is the whole mapping.

Errors:

* `404`: location from another Connection or warehouse from another seller.
* `400`: `marketplace_fulfillment` location (read-only) or a warehouse repeated in two locations.
* `409`: archived warehouse (`warehouse_archived`).

The route does not publish stock. To publish with the new mapping, request an [inventory publication](/docs/inventory/publish). The change fires the `connection.updated` webhook.

Part of [Stay in sync](/docs/how-it-works/staying-in-sync).

---

# Get account health

URL: https://marketapi.shop/docs/connections/health
Endpoint: `GET /v1/connections/{id}/health` (coming soon)

> Shows the account's reputation in the marketplace.

Returns the latest snapshot of the account's health. `level` is normalized: `good`, `warning`, `at_risk`, `restricted` or `unknown`; the native level stays in `externalLevel`. `metrics` has each indicator (such as `claims_rate`) in bps, with its threshold and period. `penalties` has the restrictions, with start and end.

marketapi refreshes the snapshot every 6 h. When `level` changes, the `connection.health_changed` webhook arrives with `previousLevel`. While there is no snapshot, the answer is `200` with `level: unknown` and `observedAt: null`.

Part of [customer service](/docs/how-it-works/customer-service).

---

# List native operations

URL: https://marketapi.shop/docs/connections/list-operations
Endpoint: `GET /v1/connections/{id}/operations` (coming soon)

> Lists the marketplace's own operations for the Connection.

Not every marketplace function has a common model (shipping setup, size charts, boosting a listing). The native operation is the safe path for those. The list shows each `key` with its `status`: `available`, `unsupported` or `not_granted`.

You never send a URL, method, header or token. The adapter picks the call; you send only the `input`. See the format in [Get operation](/docs/connections/get-operation) and run it with [Run operation](/docs/connections/run-operation).

Part of [Capabilities](/docs/how-it-works/capabilities).

---

# Get native operation

URL: https://marketapi.shop/docs/connections/get-operation
Endpoint: `GET /v1/connections/{id}/operations/{operationKey}` (coming soon)

> Describes an operation, effect, target and input and output format.

Returns `description`, `effect` (`read` or `write`), `target` (`connection` or `listing`), whether it is `idempotent`, and the `status`. `inputSchema` and `outputSchema` come as JSON Schema: use them to validate the `input` before running.

A key from another marketplace gives `404`. The key starts with the marketplace name (for example, `mock.listing.boost`). To run it, use [Run operation](/docs/connections/run-operation).

Part of [Capabilities](/docs/how-it-works/capabilities).

---

# Run native operation

URL: https://marketapi.shop/docs/connections/run-operation
Endpoint: `POST /v1/connections/{id}/operations/{operationKey}` (coming soon)

> Runs a marketplace's own operation, as a Job.

Send `input` and, when the target is a listing, `target.listingId` (the Listing must belong to the Connection). The answer is `202` with the `jobId`: a read runs as `native_operation.read` and a write as `native_operation.write`. The result stays in the Job items.

Safety: only registered operations run. An unknown key gives `404`; `input` with an extra field, URL, header or credential gives `400`; an unsupported operation gives `422`; no permission gives `403`. It is never a proxy.

When it ends, `operation.completed` or `operation.failed` arrives. The event does not carry the output: read it in [Job items](/docs/jobs/items). Use the `Idempotency-Key` header.

Part of [Capabilities](/docs/how-it-works/capabilities).

---

# Get marketplace options

URL: https://marketapi.shop/docs/connections/marketplace-options
Endpoint: `GET /v1/connections/{id}/marketplace-options` (coming soon)

> Shows the marketplace's own options on the Connection.

Some settings exist in one marketplace only and live in its state, such as Mercado Livre's exclusion list for automatic campaigns (`automaticCampaigns: allow | exclude`). The answer has `desired` (what you asked for), `observed` (what the marketplace has) and `observedAt`, per marketplace.

The route only reads what marketapi stored. To change it, use [Update marketplace options](/docs/connections/update-marketplace-options).

Part of [Capabilities](/docs/how-it-works/capabilities).

---

# Update marketplace options

URL: https://marketapi.shop/docs/connections/update-marketplace-options
Endpoint: `PATCH /v1/connections/{id}/marketplace-options` (coming soon)

> Changes the marketplace's own options on the Connection.

Send the namespace of the Connection's marketplace, for example `{ "mercadolivre": { "automaticCampaigns": "exclude" } }`. Options are merged per namespace. marketapi saves the `desired` value and creates a `connection.marketplace_options_apply` Job: the answer is `202` with the `jobId`. When the Job ends, `observed` reflects the marketplace.

Errors: `400` for a namespace of another marketplace or a value with a URL or credential; `409` (`connection_not_active`) for a revoked Connection; `422` if the marketplace does not support it. Use the `Idempotency-Key` header. There is no new webhook: follow the Job.

Part of [Capabilities](/docs/how-it-works/capabilities).

---

# Disconnect

URL: https://marketapi.shop/docs/connections/disconnect
Endpoint: `DELETE /v1/connections/{id}`

> Revokes a connection and deletes the credential on our side.

Disconnects the marketplace account: the connection becomes `revoked` with `revokedReason: "client"`, and we delete the credential on our side. The response is `204`, with no body.

We do **not** revoke the authorization the seller gave at the marketplace: it stays in their account there until they remove the access at the marketplace itself. We only stop using the connection and delete the credential.

Repeating the request on a connection already revoked also returns `204`. To come back, the seller connects again through a new link session.

<Callout type="warn">
  A seller with an active connection cannot be deleted. Disconnect their
  connections before calling `DELETE /v1/sellers/{id}`.
</Callout>

---

# Products and variants

URL: https://marketapi.shop/docs/products/overview

> Reusable base records, sellable variants, and kits.

A **Product** is the reusable local base record. A **Variant** is the unit that can actually be sold, reserved, and stocked: it has its own SKU, GTIN, options, and inventory policy. A **Listing** is different: it is one concrete offer in one marketplace Connection. Multiple ads — including A/B experiments in the same channel — can reuse the same Variants.

The full model of Product, Variant and kit, with diagrams: [Catalog and inventory](/docs/data-model/catalog-and-inventory).

In other words: the product describes *what exists*; the variant describes *which combination is sellable*; the listing describes *how and where it is offered*. The marketplace never edits this base. External facts live in the listing's `observed`; a local edit is sent only when you explicitly publish that Listing.

## Recommended end-to-end flow

1. [Create the Product](/docs/products/create) with stable data: brand, model, base description, physical details, and reusable media.
2. Update the automatically created standard Variant or [create Variants](/docs/products/create-variant) for each sellable combination. For an iPhone 16, `black/16 GB`, `white/16 GB`, and `black/32 GB` are three Variants with three independent balances.
3. Set each Variant's `inventoryAuthority`. Use `marketapi` only when this API owns the ledger; use `derived` for a kit Variant; keep `managed_externally` when the ERP is authoritative.
4. For a kit, [replace its complete BOM](/docs/products/replace-components). Kit availability is the smallest whole-number ratio available across its components.
5. Record or inspect balance in [Inventory](/docs/inventory/overview). Then create a [Listing](/docs/listings/create), bind its Variants, and publish when you decide to.

## What belongs at each level

| Question                                                               | Correct place      |
| ---------------------------------------------------------------------- | ------------------ |
| “What is this product: brand, model, reusable description?”            | `Product`          |
| “Which color/storage/SKU/GTIN has its own availability?”               | `Variant`          |
| “Which title, price, media, and commercial rules apply to this offer?” | `Listing.desired`  |
| “What did the marketplace confirm exists remotely?”                    | `Listing.observed` |

<Callout>
  Every Product starts with a standard Variant, with no SKU and `disabled`
  inventory. It ensures that even a simple product has a sellable unit. Update
  it when the product has no options; create additional Variants for
  combinations such as color, size, or storage.
</Callout>

## Inventory and kits in practice

* `marketapi`: immutable movements form the ledger and calculate `onHand`, `reserved`, and `available`.
* `managed_externally`: another system owns the balance; MarketAPI rejects local movements for this Variant.
* `derived`: it receives no movement of its own. Its balance is derived from the Product BOM, such as `min(cables ÷ 2, chargers ÷ 1)` for a kit that consumes two cables and one charger.
* `disabled`: the Variant has no API-managed quantity.

Archiving never deletes history, movements, or audit data. Before archiving a Product or Variant, deliberately remove any Listing bindings that still use it.

Photos and videos are [Assets](/docs/assets/overview): upload the file first and use its `assetId` in `media`. The asset must belong to the same seller.

---

# Create product

URL: https://marketapi.shop/docs/products/create
Endpoint: `POST /v1/sellers/{sellerId}/products`

> Creates the base record and its standard variant.

Use this endpoint to create the **base record**, not a marketplace offer. It does not call a Connection and does not publish anything. The response contains the Product; in the same transaction the API creates a standard Variant with `options: {}`, no SKU, and `inventoryAuthority: "disabled"`.

Put only reusable data here: localized `content`, canonical identity (`brand`, `model`, identifiers), typed attributes, physical data (`physical`: item and package weight in grams and dimensions in millimeters, as integers), fiscal and compliance declarations, media, and your application's `metadata`. Price, commercial terms, and channel-specific copy belong to a Listing; color, storage, SKU, and GTIN specific to a combination belong to a Variant.

Each attribute states its value type, for example `{ "key": "material", "value": { "type": "text", "text": "aluminum" } }`. Use UCUM units for measurable values, such as `g`, `cm`, or `V`. In `physical`, `weightInGrams` and `dimensionsInMm` describe the item; `packageWeightInGrams`, `packageDimensionsInMm`, and `packageQuantity` describe what ships.

Example decision: create `iPhone 16` once as a Product. Then turn its standard Variant into a simple sellable unit or create `black/128 GB` and `white/128 GB` Variants. Only then create an offer on Mercado Livre, Amazon, or another channel.

Use `Idempotency-Key` when retrying a creation after a network failure. Reusing the same key prevents a duplicate Product and standard Variant.

---

# List products

URL: https://marketapi.shop/docs/products/list
Endpoint: `GET /v1/products`

> Lists the account's reusable base catalog.

This collection is the entry point for browsing the account's base catalog. It does not mix external offers or balances: inventory belongs to Variants and offers belong to Listings.

Use the returned `pagination` object to traverse results; do not assume the first result is the only Product with a particular title. For operational queries, filter by `status=active`; use `status=archived` when you need to find retired records without losing history.

After choosing a Product, list its [Variants](/docs/products/list-variants) to see which combinations can be bound to listings or moved in inventory.

---

# Get product

URL: https://marketapi.shop/docs/products/get
Endpoint: `GET /v1/products/{productId}`

> Gets a reusable base record by id.

Use this endpoint when you already know the `productId`, for example after selecting an item from a list or receiving a product webhook. The response is only the base record; it does not embed Variants, balance, listings, or external marketplace state.

Make separate, explicit calls for [Variants](/docs/products/list-variants), [kit components](/docs/products/list-components), and related Listings. That keeps a simple catalog read from loading every commercial and inventory concern.

---

# Update product

URL: https://marketapi.shop/docs/products/update
Endpoint: `PATCH /v1/products/{productId}`

> Updates local reusable base data.

This PATCH changes only the local base record. It does not call the marketplace, change `observed`, or publish any listing as a side effect. After changing an inherited field, inspect affected Listings' [resolved data](/docs/listings/resolved) and publish only the ones you choose.

PATCH is partial **at the top level**: omitting `identity`, `attributes`, `physical`, `fiscal`, `compliance`, `media`, or `metadata` preserves its current value. However, when you send a field such as `content` or `attributes`, send a complete valid representation — `content.title` remains required — because that field replaces the Product object rather than merging its inner keys.

Use `status: "archived"` only as a lifecycle transition. To archive, no Variant of the Product may remain bound to a Listing; the endpoint returns a conflict until those bindings are deliberately removed.

## Common errors

* **`409 CONFLICT` with `details.reason` equal to `variant_archived` or `product_archived`**: when reactivating an archived kit (`status: "active"`), a Variant of its BOM was archived, or is active in an archived product. Remove it from the BOM first (`PUT /v1/products/{id}/components`).
* **`409 CONFLICT` with `details.reason` equal to `product_in_kit` or `product_in_use`**: archiving a product that an active kit or a listing binding still uses.

---

# Archive product

URL: https://marketapi.shop/docs/products/archive
Endpoint: `DELETE /v1/products/{productId}`

> Archives, without deleting, the reusable base record.

This `DELETE` is a **logical archive**, not a physical removal. The Product becomes `archived`, while its history, Variants, inventory movements, and events remain available for audit. There is no endpoint that silently erases that history.

The API rejects the operation while any Product Variant still has a Listing binding. This protection prevents an ad from pointing to a sellable unit that was removed from the catalog. First list the Variants, locate affected listings, and [replace each binding map](/docs/listings/replace-bindings).

Use archiving to retire a record that should no longer be used. Do not use it to unpublish: pausing or removing an offer is an independent [Listing](/docs/listings/remove) operation.

## Common errors

* **`409 CONFLICT` with `details.reason = "product_in_kit"`**: a Variant of this product is a component of an active kit. Remove it from the kit (`PUT /v1/products/{id}/components`) first. Archiving the kit itself is allowed.

---

# List variants

URL: https://marketapi.shop/docs/products/list-variants
Endpoint: `GET /v1/products/{productId}/variants`

> Lists a product's sellable combinations.

Use this list to find the correct local unit before recording inventory or creating a binding. It includes the standard Variant created with the Product, even when the product has no color, size, or other option.

Each item represents a distinct sellable combination. One Product can have `black/16 GB` and `black/32 GB` because their `options` maps differ; each receives its own `variantId`, balance, and movement history.

The response is paginated. Store Variant IDs in your ERP or PIM if it will initiate movements and bindings programmatically.

---

# Get variant

URL: https://marketapi.shop/docs/products/get-variant
Endpoint: `GET /v1/products/{productId}/variants/{variantId}`

> Gets one product Variant by id.

Use this endpoint when you know both the Product and its specific combination. The Variant is returned only when it belongs to the `productId` and the credential account; every other case returns `404`.

The response contains that Variant's own attributes and physical data. Omitted fields still inherit during content resolution without changing the reusable Product base.

---

# Create variant

URL: https://marketapi.shop/docs/products/create-variant
Endpoint: `POST /v1/products/{productId}/variants`

> Creates a sellable combination, such as color and storage.

Create one Variant for each combination that may have its own SKU, GTIN, availability, cost, or content. `options` identifies that combination within the Product, for example `{ "color": "black", "storage": "128GB" }`. The same option combination cannot be created twice in one Product.

SKU and GTIN are particularly important when importing listings: automatic linking uses exact identifiers only and never uses a title. Choose the inventory authority before you start moving quantity. If you provide cost, send `costInCents` (cost in cents) and `costCurrency` together. In `physical`, weight goes in grams (`weightInGrams`) and dimensions in millimeters (`dimensionsInMm`), always as integers; the package has its own (`packageWeightInGrams`, `packageDimensionsInMm`). Variant `attributes` use the same typed format as the Product and win over it on the same `key`.

Creating a Variant remains a local operation. It does not create a marketplace variation yet: use [bindings](/docs/listings/replace-bindings) to associate it with a listing and explicit publication to send it to the channel.

---

# Update variant

URL: https://marketapi.shop/docs/products/update-variant
Endpoint: `PATCH /v1/products/{productId}/variants/{variantId}`

> Updates local data for a sellable unit.

Use this endpoint to change the data that differentiates a combination: SKU, GTIN, options, cost, media, specific attributes, or inventory authority. The change is local; it does not automatically update marketplace title, price, variation, or quantity.

PATCH preserves top-level fields that you omit. When you send `content`, it becomes the Variant's override over the Product. Send `content: {}` when the Variant should stop overriding content and inherit the base again. Cost requires `costInCents` (in cents) and `costCurrency` together when created; keep the pair consistent in updates.

Do not change `inventoryAuthority` as a shortcut to correct quantity. First reconcile the system that will be the source of truth. A Variant also cannot be archived while bound to a Listing; remove or replace those bindings first.

---

# Archive variant

URL: https://marketapi.shop/docs/products/archive-variant
Endpoint: `DELETE /v1/products/{productId}/variants/{variantId}`

> Archives a sellable unit without deleting its history.

This endpoint retires a sellable combination from future use while preserving its SKU, movements, historical balance, and audit trail. It does not delete the Product or change its other Variants.

The Variant cannot be archived while it is bound to any Listing. That includes active, draft, and imported ads. List each affected listing's [bindings](/docs/listings/list-bindings) and send the intended complete set to [replace them](/docs/listings/replace-bindings); only then archive.

If your intent is only to bring quantity to zero, record an adjustment movement. Archiving is a lifecycle change, not an inventory movement.

## Common errors

* **`409 CONFLICT` with `details.reason = "variant_in_kit"`**: the Variant is a component of an active kit. Remove it from the kit (`PUT /v1/products/{id}/components`) first.

---

# List kit components

URL: https://marketapi.shop/docs/products/list-components
Endpoint: `GET /v1/products/{productId}/components`

> Gets the complete product BOM.

The BOM (bill of materials) describes which Variants are consumed to assemble **one unit** of this Product. Each component points to a Variant of the same Seller and records a whole-unit quantity. An empty list means the Product is not treated as a kit.

Use this read to explain derived quantity to a user. Example: if a kit consumes two cables and one charger, and there are 10 cables and 3 chargers available, at most 3 kits can be published — not 5.

The BOM does not move components or create reservations. It only defines the formula; query the [Variant balance](/docs/inventory/balance) whose authority is `derived` to get calculated availability.

---

# Replace kit components

URL: https://marketapi.shop/docs/products/replace-components
Endpoint: `PUT /v1/products/{productId}/components`

> Atomically replaces a product's BOM.

Send the **complete list** that should apply after the call. This is a `PUT`, not a patch: sending only a new component removes previous ones; sending `{ "components": [] }` removes the whole BOM. Replacement is atomic, so invalid input never leaves a partially updated kit.

The response is the complete BOM after the swap, not paginated. A component Variant that does not exist, or belongs to another Seller or account, returns `404 NOT_FOUND` with the message `Variant not found.`, and a missing Product returns `404 NOT_FOUND` with the message `Product not found.`, leaving the BOM unchanged.

The API accepts only Variants of the same Seller, rejects repeated components, and prevents direct or indirect cycles. A kit cannot, for example, depend on itself through another kit.

After defining the BOM, set the kit's sellable Variant to `inventoryAuthority: "derived"`. Do not record movements on it: its balance is calculated from component Variants whenever it is queried or published.

## Common errors

* **`409 CONFLICT` with `details.reason` equal to `variant_archived` or `product_archived`**: a Variant in the body is archived, or active in an archived product. Replace it with another.

---

# Get product media

URL: https://marketapi.shop/docs/products/get-media
Endpoint: `GET /v1/products/{productId}/media` (coming soon)

> Returns the media of the Product and of each Variant.

Returns, in one response, the media of the Product and of all its Variants. Each item points to an [Asset](/docs/assets/overview) (`assetId`) and has `role` and `position`.

Use it before [replacing the media](/docs/products/replace-media) to see what is already there. To get the picture itself, fetch the Asset with [Get asset](/docs/assets/get).

---

# Replace product media

URL: https://marketapi.shop/docs/products/replace-media
Endpoint: `PUT /v1/products/{productId}/media` (coming soon)

> Replaces the media of the Product and its Variants at once.

Replaces the whole set: the Product media (`product`) and the media of each Variant (`variants[].media`). A Variant missing from the list ends up with no media. This is the only route that writes media: create and update of Product and Variant do not accept `media`.

Every `assetId` must be a `product_media` Asset, `ready`, from the same account and the same seller as the Product. An Asset from another seller returns `400`: copy it first with [Copy media](/docs/assets/copy-media). An Asset not yet validated returns `409`. The route only writes to the local base and nothing is published: to take the change to the listings, use [Propagate changes](/docs/products/propagate).

---

# Propagate changes

URL: https://marketapi.shop/docs/products/propagate
Endpoint: `POST /v1/products/{productId}/propagations` (coming soon)

> Takes a change from the Product to the listings that use it.

Changing the Product does not change the listings on its own. Propagation is an explicit command: you say which `fields` to carry (`content`, `media`, `attributes`…) and the `mode`.

* `inheriting_only`: only listings that inherit the field. Those with their own value stay as they are and appear in `skippedFields`.
* `selected`: the same, but only on the listings in `marketplaceProductIds`.
* `force`: removes the own value and goes back to inheriting from the Product.

Each affected listing becomes `dirty` and gets the `marketplace_product.updated` event. With `publish: true`, the route also creates the publish job of each one; if a Connection lacks the capability, only that listing fails. The response lists the result per listing. See [Catalog and stock](/docs/how-it-works/catalog-and-stock).

---

# Overview

URL: https://marketapi.shop/docs/assets/overview

> How the API stores pictures, videos and documents.

An **Asset** is a file stored by the API: product picture and video, invoice, label, attachment. Each Asset belongs to one seller. You send the file straight to storage through a signed URL, without the content going through the API.

Where it fits in the journey: [Catalog and stock](/docs/how-it-works/catalog-and-stock).

## Endpoints

| Method   | Path                                     | What it does                                           |
| -------- | ---------------------------------------- | ------------------------------------------------------ |
| `POST`   | `/v1/sellers/{sellerId}/assets/uploads`  | [Create upload](/docs/assets/create-upload)            |
| `POST`   | `/v1/assets/uploads/{uploadId}/complete` | [Complete upload](/docs/assets/complete-upload)        |
| `POST`   | `/v1/assets/ingestions`                  | [Import from a URL](/docs/assets/ingest) (coming soon) |
| `POST`   | `/v1/sellers/{sellerId}/media-copies`    | [Copy media](/docs/assets/copy-media)                  |
| `GET`    | `/v1/assets/{assetId}`                   | [Get asset](/docs/assets/get)                          |
| `DELETE` | `/v1/assets/{assetId}`                   | [Delete asset](/docs/assets/delete)                    |
| `GET`    | `/v1/assets/{assetId}/content`           | [Read content](/docs/assets/content)                   |

## Two classes

| `class`         | Examples                       | Access                                                                                   |
| --------------- | ------------------------------ | ---------------------------------------------------------------------------------------- |
| `product_media` | listing picture and video      | `GET /v1/assets/{assetId}` returns a signed `url` valid for 15 min                       |
| `confidential`  | invoice XML, label, attachment | never has a `url`; only through [`content`](/docs/assets/content), which logs the access |

Accepted types: `image/jpeg`, `image/png`, `image/webp`, `video/mp4`, `application/pdf`, and `application/xml`, checked against the **content**, not the name. The limit is 10 MiB for images, 100 MiB for MP4, 20 MiB for PDF, and 5 MiB for XML. SVG and HTML are always refused. `application/xml` is only accepted for `confidential`. A `product_media` PDF has a `url`, and it downloads the file as an attachment instead of opening it in the browser.

## How to send

1. [Create the upload](/docs/assets/create-upload) with the class, the type, and the exact size of the file. The response has `url` and `headers`.
2. Send the file with `PUT` to that `url`, repeating the `headers` unchanged. They are signed: another size or type gets `403` from the storage.
3. [Complete the upload](/docs/assets/complete-upload). The API checks size and type against the content and returns the asset `ready` or `failed`, with its `failureReason`.

```bash
# 1. create the upload
curl -X POST "$API/v1/sellers/$SELLER_ID/assets/uploads" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "class": "product_media", "contentType": "image/jpeg", "sizeBytes": 482113, "filename": "front.jpg" }'

# 2. PUT with the url and headers from the response
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: image/jpeg" \
  -H "Content-Length: 482113" \
  --data-binary @front.jpg

# 3. complete
curl -X POST "$API/v1/assets/uploads/$UPLOAD_ID/complete" \
  -H "Authorization: Bearer $API_KEY"
```

The PUT URL is valid for 15 minutes. After that, `complete` answers `409 upload_expired`. If you call `complete` before the file arrives, you get `409 upload_not_received` and can try again. A second `complete` answers `409 upload_already_completed`.

When the asset is ready (or fails), the API sends the `asset.ready` or `asset.failed` webhook. The event carries only the id, class, status, and reason, never the URL.

Bringing a file from a URL ([import from a URL](/docs/assets/ingest)) is not in the API yet.

## Rules

* A seller's listing only accepts that seller's media. To use the same picture in another seller, [copy the asset](/docs/assets/copy-media): the copy is made inside the storage, becomes a new asset (`source: copy`), and the original stays as it is.
* [Delete](/docs/assets/delete) removes the asset and its file. If any product, variant, or listing still points at it, the answer is `409 asset_in_use`: remove the reference first. Deleting the seller deletes its files.
* Products and variants keep media by `assetId`: see [Products](/docs/products/overview).

<Callout type="warn">
  Every asset route answers `503 STORAGE_UNAVAILABLE` when file storage is not
  configured in the environment.
</Callout>

---

# Create upload

URL: https://marketapi.shop/docs/assets/create-upload
Endpoint: `POST /v1/sellers/{sellerId}/assets/uploads`

> Creates the asset and returns the signed URL to upload the file.

First step of an upload. Send the class, the type, and the **exact** size in bytes. The asset starts as `pending_upload`.

`PUT` the file to the `url` (valid for 15 minutes) with the `headers` from the response, unchanged. `Content-Type` and `Content-Length` are signed: any other value gets a `403` from the storage. Then call [complete](/docs/assets/complete-upload).

```bash
curl -X PUT "$UPLOAD_URL" -H "Content-Type: image/jpeg" -H "Content-Length: 482113" --data-binary @front.jpg
```

Without storage configured, the answer is `503 STORAGE_UNAVAILABLE`.

---

# Complete upload

URL: https://marketapi.shop/docs/assets/complete-upload
Endpoint: `POST /v1/assets/uploads/{uploadId}/complete`

> Checks the uploaded file and makes the asset ready.

Call it after the `PUT`. The API checks the size and the type **from the content** (SVG and HTML are refused) and answers the asset as `ready`, or `failed` with a `failureReason`. A `failed` asset does not recover: create another upload.

It works once per upload:

* `409 upload_not_received`: the file has not reached the storage yet. Upload it and call again.
* `409 upload_already_completed`: this upload was already completed.
* `409 upload_expired`: the 15 minutes passed. Create another upload.

The result is also sent in the `asset.ready` and `asset.failed` webhooks.

---

# Ingest from URL

URL: https://marketapi.shop/docs/assets/ingest
Endpoint: `POST /v1/assets/ingestions` (coming soon)

> Downloads a file from a public URL and creates the Asset.

Use it when the picture is already at a URL. Send `sellerId`, `class` and `url`. The route returns `202` with the Asset (`processing`) and the `jobId`: the download runs in a [job](/docs/jobs/overview).

The API refuses internal addresses and limits the size. When it ends, `asset.ready` arrives, or `asset.failed` with the reason. The type is checked by the content, as in the [upload](/docs/assets/complete-upload).

---

# Copy media

URL: https://marketapi.shop/docs/assets/copy-media
Endpoint: `POST /v1/sellers/{sellerId}/media-copies`

> Copies ready assets of the account to another seller.

Use it when the same photo goes to another seller. A listing's media must belong to the listing's seller, so the copy gives the seller in the path an asset of its own.

Send `assetIds`: each one must belong to your account and be `ready`, otherwise the answer is `409 asset_not_ready`. The copy happens inside the storage, becomes a new asset with `source: copy`, and the original does not change.

---

# Get asset

URL: https://marketapi.shop/docs/assets/get
Endpoint: `GET /v1/assets/{assetId}`

> Returns the asset, with a signed URL if it is ready media.

Returns the state of the asset. Only a `ready` `product_media` has a `url` (and `urlExpiresAt`), valid for 15 minutes: ask again when it expires. A `confidential` asset never has a URL; use [the content](/docs/assets/content).

---

# Delete asset

URL: https://marketapi.shop/docs/assets/delete
Endpoint: `DELETE /v1/assets/{assetId}`

> Removes the asset and its file.

Removes the asset and its file in the storage. If a product, a variant, a marketplace product, or a variation still points to it, the answer is `409 asset_in_use`: remove the media from there and try again.

---

# Read content

URL: https://marketapi.shop/docs/assets/content
Endpoint: `GET /v1/assets/{assetId}/content`

> Relays the asset bytes through the API.

Returns the file through the API, with `X-Content-Type-Options: nosniff`. PDF and XML come as `attachment`. It is the only way to read a `confidential` asset: the response has `Cache-Control: private, no-store` and every read is logged.

For photos and videos, prefer the `url` from [Get asset](/docs/assets/get). An asset that is not `ready` answers `409`.

---

# Variant inventory

URL: https://marketapi.shop/docs/inventory/overview

> Auditable ledger, balance per warehouse and selective quantity publication.

Inventory always belongs to a **Variant**, never a Product. Choose authority per Variant: `marketapi` owns the ledger and balance; `managed_externally` leaves the ERP authoritative; `derived` calculates kits from the BOM; `disabled` manages no quantity.

Within the Variant, the balance is **per warehouse**: each Variant has one balance line per [warehouse](/docs/warehouses/overview) of the seller, and the Variant balance is their sum.

How the balance is computed and reaches the marketplace, with diagrams: [Catalog and inventory](/docs/data-model/catalog-and-inventory).

A `marketapi` movement writes the ledger and can create a publication Job for selected targets in the same intent. With automatic inventory opt-in, only eligible bindings receive quantity; title, price, and media are never republished.

## Warehouses and transfer

Every seller starts with a default warehouse. Create others in [Create warehouse](/docs/warehouses/create) if it keeps stock in more than one place.

* **A movement changes one warehouse.** `warehouseId` picks which. Without it, it is the default one.
* **Transfer** (`type: "transfer"`): takes from the source warehouse (`warehouseId`) and puts into the destination (`toWarehouseId`), with a positive quantity. It writes two ledger rows with the same `transferId`. The Variant total does not change, so a transfer **does not publish quantity by itself**.
* **Warehouse of another seller:** `404`. **Archived warehouse:** `409` with `details.reason = "warehouse_archived"`.
* **Not enough stock in the warehouse:** `409` with `details.reason = "inventory_underflow"`, and nothing is written.

## How balances are computed

For a managed Variant, `available = onHand - reserved`, in each warehouse. A reservation lowers `available` without taking the physical unit out of stock; a confirmed sale lowers both. Reservation, release and sale are per warehouse too: take stock out of the same warehouse where you reserved it.

The API does not accept a negative balance: the movement returns `409` (`inventory_underflow`) with the `warehouseId` of the warehouse that went short. Only a Variant that allows backorder may go negative.

A `derived` kit has no movement of its own. The kit is calculated **per warehouse**: in each one, it is the smallest whole quotient of the components that are there, and the warehouses are summed. A kit is not assembled from parts that sit in two places.

If a kit uses two cables and one charger, with 10 cables and 3 chargers in the same warehouse, the kit balance is 3. If the 3 chargers are in another warehouse, with no cables, the kit is worth 0 in both. Check the [BOM](/docs/products/list-components) to explain that number; recording a movement on the components recalculates it on the next read.

## Current support

Use the [complete marketplace matrix](/docs/marketplace-support) to compare product support. The [Connection capability](/docs/connections/capabilities) still decides whether publication can be queued for that account.

<MarketplaceCapabilityTags capability="inventory.publish" />

---

# Get variant balance

URL: https://marketapi.shop/docs/inventory/balance
Endpoint: `GET /v1/variants/{variantId}/inventory`

> Gets on-hand, reserved, and available inventory for a Variant, in total and per warehouse.

`available` is publishable quantity. The response has the **Variant totals** (`onHand`, `reserved`, `available`) and the `warehouses` list, with one line per warehouse. The totals are always the sum of the lines.

```bash
curl "$BACKEND_URL/v1/variants/$VARIANT_ID/inventory" \
  -H "Authorization: Bearer mk_your_api_key"
```

Example: 6 units in the default warehouse and 4 in the branch give `onHand: 10` in total.

## The `warehouses` list

* It has **every active warehouse** of the seller, including the ones where the Variant never had a movement (all zero). An archived warehouse does not show up.
* The default warehouse comes first. The others follow by `name`.
* For the balance of one warehouse, look for its `warehouseId` in the list.

## Derived kits

For derived kits, `available` is calculated at read time, **warehouse by warehouse**: in each one, it is the smallest whole-unit ratio of the active components that are there. The totals sum the warehouses, and the kit's `reserved` is always 0. Parts in different warehouses do not make a kit.

---

# Record inventory movement

URL: https://marketapi.shop/docs/inventory/create-movement
Endpoint: `POST /v1/sellers/{sellerId}/inventory-movements`

> Writes an immutable movement in a warehouse, or a transfer between two, and can publish selected quantity.

Send a whole-unit quantity compatible with the movement type. Only `adjustment` accepts a negative quantity; the other types need a positive one (otherwise `400`). The Variant must have `inventoryAuthority: "marketapi"`.

## In which warehouse

`warehouseId` picks the warehouse. Without it, the movement goes to the seller's **default warehouse**. The warehouse must belong to the same seller as the Variant.

```bash
curl -X POST "$BACKEND_URL/v1/sellers/$SELLER_ID/inventory-movements" \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"type":"adjustment","variantId":"'$VARIANT_ID'","warehouseId":"'$WAREHOUSE_ID'","quantity":10,"reason":"Initial count"}'
```

## Transfer between warehouses

With `type: "transfer"`, `warehouseId` is the source and `toWarehouseId` is the destination. The quantity is positive and the two warehouses must differ.

```bash
curl -X POST "$BACKEND_URL/v1/sellers/$SELLER_ID/inventory-movements" \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"type":"transfer","variantId":"'$VARIANT_ID'","warehouseId":"'$FROM_ID'","toWarehouseId":"'$TO_ID'","quantity":4,"reason":"Branch restock"}'
```

A transfer writes **two rows** in the ledger, with the same `transferId`: an outbound (`-4`) at the source and an inbound (`+4`) at the destination. Everything happens or nothing does. The Variant total does not change, so it **does not publish quantity by itself**, not even with the `automatic` policy. Whoever sends `publishTargets` still gets the job.

## What comes back

* `movements`: the list of movements written. It has 1 item, or 2 for a transfer (the outbound first, then the inbound).
* `balance`: the Variant balance after the command, with the totals and the `warehouses` list. No need to read the [balance](/docs/inventory/balance) again.
* `publicationJobId`: the publication job, when there was an eligible target.

`publishTargets` is optional: select Listings, bindings, or Connections in this same call, without a mandatory second request.

The `inventory.movement_created` webhook is sent **once per ledger row**. Each event carries the `movement` (with `warehouseId` and `transferId`) and the same full `balance`. The two events of a transfer carry the same `balance`.

## Common errors

* **`400`**: a quantity with the wrong sign for the type; `toWarehouseId` missing on a transfer, or sent on another type; `toWarehouseId` equal to `warehouseId`.
* **`404`**: the seller, the Variant or a warehouse does not exist. A warehouse of another seller or another account returns `404`.
* **`409` with `details.reason = "warehouse_archived"`**: the warehouse is archived and takes no movement. `details.warehouseId` says which one. [Reactivate it](/docs/warehouses/update) first.
* **`409` with `details.reason = "inventory_underflow"`**: the movement would leave the warehouse without enough stock. `details.warehouseId` is the warehouse that went short (for a transfer, the source). Nothing is written and no event is sent. A Variant with `allowBackorder` may go negative.
* **`409` with `details.reason = "inventory_authority"`**: the Variant is not managed by MarketAPI.

---

# Republish inventory

URL: https://marketapi.shop/docs/inventory/publish
Endpoint: `POST /v1/sellers/{sellerId}/inventory-publications`

> Queues current quantity publication without a movement.

Use it to resend an already confirmed balance to specific targets. The response is a Job; it changes neither the ledger nor content, price, or media.

---

# List inventory movements

URL: https://marketapi.shop/docs/inventory/list-movements
Endpoint: `GET /v1/inventory-movements`

> Gets a seller's immutable inventory ledger.

Filter by Variant, warehouse (`warehouseId`) or type to audit adjustments, reservations, sales, returns, and transfers. The ledger is not deleted when a Variant or a warehouse is archived.

```bash
curl "$BACKEND_URL/v1/inventory-movements?warehouseId=$WAREHOUSE_ID&type=transfer" \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Seller-Id: $SELLER_ID"
```

Each movement says in which warehouse it happened (`warehouseId`). A transfer is **two movements** with the same `transferId`: the outbound, with a negative `quantity`, at the source, and the inbound, positive, at the destination. When you filter by `warehouseId`, it shows up in the list of both warehouses, one row in each. In any other type, `transferId` is `null`.

---

# List stock discrepancies

URL: https://marketapi.shop/docs/inventory/list-discrepancies
Endpoint: `GET /v1/inventory-discrepancies` (coming soon)

> Lists where the published stock and the marketplace stock do not match.

A discrepancy exists when the quantity you published and the one the marketplace shows do not match, after discounting the external reservation (campaign, promotion). Each item has `publishedQuantity`, `observedAvailable`, `observedReserved` and `difference`.

Filter by seller, Connection, Variant or `status` (`open` or `resolved`). You are told by the `inventory.discrepancy_detected` event. To handle one, use [Resolve discrepancy](/docs/inventory/resolve-discrepancy).

---

# Resolve discrepancy

URL: https://marketapi.shop/docs/inventory/resolve-discrepancy
Endpoint: `POST /v1/inventory-discrepancies/{id}/resolve` (coming soon)

> Closes a stock discrepancy with one of three actions.

Pick the action in `action`:

* `republish`: creates a job that publishes the base quantity again. The response has the `publicationJobId`.
* `accept_observed`: accepts the marketplace number. Writes an adjustment on the warehouse (`warehouseId`) and only works for a Variant with `inventoryAuthority: marketapi`.
* `dismiss`: just closes the discrepancy, without touching stock.

A discrepancy already closed returns `409` (`discrepancy_not_open`). If the difference disappears on its own, the discrepancy closes as `auto`. Every resolution emits the `inventory.discrepancy_resolved` event.

---

# Overview

URL: https://marketapi.shop/docs/warehouses/overview

> What a warehouse is and how it fits into stock.

A **warehouse** is where the seller keeps the goods. The stock balance is per Variant and per warehouse, and the Variant balance is their sum.

Where it fits in the journey: [Catalog and stock](/docs/how-it-works/catalog-and-stock). The full model, with diagrams: [Catalog and inventory](/docs/data-model/catalog-and-inventory).

## Endpoints

| Method   | Path                                | What it does                                  |
| -------- | ----------------------------------- | --------------------------------------------- |
| `POST`   | `/v1/sellers/{sellerId}/warehouses` | [Create warehouse](/docs/warehouses/create)   |
| `GET`    | `/v1/warehouses`                    | [List warehouses](/docs/warehouses/list)      |
| `GET`    | `/v1/warehouses/{warehouseId}`      | [Get warehouse](/docs/warehouses/get)         |
| `PATCH`  | `/v1/warehouses/{warehouseId}`      | [Update warehouse](/docs/warehouses/update)   |
| `DELETE` | `/v1/warehouses/{warehouseId}`      | [Archive warehouse](/docs/warehouses/archive) |

## Rules

* Every seller starts with a **default warehouse** (`name: "Default"`), created together with the seller. A movement without `warehouseId` goes to it.
* Only one warehouse is the default. To change it, mark another one with `isDefault: true`.
* `DELETE` **archives**: the movement history keeps pointing to the warehouse. The API refuses (`409`, `warehouse_in_use`) to archive the default or a warehouse with balance.
* An archived warehouse accepts no movement (`409`, `warehouse_archived`). Reactivate it with `PATCH` and `status: "active"`.
* A movement only accepts a warehouse of the **same seller** as the Variant. From another seller or another account, the response is `404`.
* A transfer between warehouses writes one outbound and one inbound, and the Variant total does not change. See [Record inventory movement](/docs/inventory/create-movement).
* Deleting the seller deletes its warehouses.
* No warehouse route talks to the marketplace: they are local records.

## Webhooks

`warehouse.created`, `warehouse.updated` and `warehouse.archived`. The default warehouse of a new seller arrives as `warehouse.created`, right after `seller.created`. See [Receiving webhooks](/docs/receiving-webhooks).

---

# Create warehouse

URL: https://marketapi.shop/docs/warehouses/create
Endpoint: `POST /v1/sellers/{sellerId}/warehouses`

> Creates a warehouse for a seller.

Use it when the seller keeps stock in more than one place. The default warehouse already exists, because it is born with the seller. Here you create the others.

Send `name` and, to link it to your ERP, `externalId`, which is unique within the seller. With `isDefault: true`, the new warehouse is born as the default and the previous one stops being it. Without it, the default stays the same.

```bash
curl -X POST "$BACKEND_URL/v1/sellers/$SELLER_ID/warehouses" \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"name":"Campinas branch","externalId":"CD-02"}'
```

The response is `201` with the warehouse. The `warehouse.created` event reaches your webhooks. If the new one is the default, the previous default gets `warehouse.updated`.

Repeat with the same `Idempotency-Key` and the same body if the network fails: you get the original response and no new warehouse.

## Common errors

* **`404`**: the seller does not exist or belongs to another account.
* **`409` with `details.field = "externalId"`**: another warehouse of the same seller already uses this `externalId`.

---

# List warehouses

URL: https://marketapi.shop/docs/warehouses/list
Endpoint: `GET /v1/warehouses`

> Lists the account warehouses, paginated.

Returns the warehouses of every seller of the account, newest first, with pagination totals. Without a filter, you get the active and the archived ones.

Filter by `sellerId` and `status` (`active` or `archived`). To find a seller's default warehouse, filter by `sellerId` and look for `isDefault: true`:

```bash
curl "$BACKEND_URL/v1/warehouses?sellerId=$SELLER_ID&status=active" \
  -H "Authorization: Bearer mk_your_api_key"
```

The balance of each warehouse is not here. It is in [Get variant balance](/docs/inventory/balance).

---

# Get warehouse

URL: https://marketapi.shop/docs/warehouses/get
Endpoint: `GET /v1/warehouses/{warehouseId}`

> Returns a warehouse by id.

Use it when you already know the `warehouseId`, for example from a `warehouse.*` event or from the `warehouses` list of the balance. The response is only the warehouse: `name`, `externalId`, `isDefault` and `status`.

A warehouse from another account returns `404`, the same as one that does not exist.

The balance per warehouse is in [Get variant balance](/docs/inventory/balance).

---

# Update warehouse

URL: https://marketapi.shop/docs/warehouses/update
Endpoint: `PATCH /v1/warehouses/{warehouseId}`

> Changes the name, the external id or the default warehouse.

Send only what changes: `name`, `externalId` (accepts `null` to clear it), `isDefault` or `status`.

* **Change the default:** mark the other warehouse with `isDefault: true`. There is no `isDefault: false`, because the seller never ends up without a default. The swap happens in one step: both warehouses get `warehouse.updated`.
* **Reactivate:** `status: "active"` reactivates an archived warehouse. To archive, use [Archive warehouse](/docs/warehouses/archive): `archived` is not accepted here.
* **No change:** if the body changes nothing, the response is `200` with the warehouse, nothing is written and no event is sent.

```bash
curl -X PATCH "$BACKEND_URL/v1/warehouses/$WAREHOUSE_ID" \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"isDefault":true}'
```

## Common errors

* **`400`**: invalid body, for example `isDefault: false` or `status: "archived"`. Neither value exists here.
* **`409` with `details.reason = "warehouse_archived"`**: `isDefault: true` on an archived warehouse. Reactivate it first, or in the same call: `{"status":"active","isDefault":true}`.
* **`409` with `details.field = "externalId"`**: another warehouse of the same seller already uses this `externalId`.
* **`404`**: the warehouse does not exist or belongs to another account.

---

# Archive warehouse

URL: https://marketapi.shop/docs/warehouses/archive
Endpoint: `DELETE /v1/warehouses/{warehouseId}`

> Archives a warehouse without deleting it.

This `DELETE` **archives**, it does not delete: the movement history points to the warehouse. The response is `204`, with no body, and the `warehouse.archived` event reaches your webhooks. Archiving a warehouse that is already archived changes nothing and also returns `204`.

Once archived, the warehouse:

* accepts no new movement (`409`, `warehouse_archived`);
* leaves the `warehouses` list of the balance of the Variants;
* stays in [List warehouses](/docs/warehouses/list) with `status: "archived"`, and in the old movements.

To use it again, [reactivate it](/docs/warehouses/update) with `status: "active"`.

```bash
curl -X DELETE "$BACKEND_URL/v1/warehouses/$WAREHOUSE_ID" \
  -H "Authorization: Bearer mk_your_api_key"
```

## When the API refuses

The response is `409` with `details.reason = "warehouse_in_use"`. The `details.uses` field gives the reason, and may have both:

| `uses`    | Means                                           | What to do                                                                               |
| --------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `default` | it is the seller's default warehouse            | [mark another one](/docs/warehouses/update) with `isDefault: true`                       |
| `stock`   | it has balance: `onHand` or `reserved` is not 0 | move the balance out with a [transfer](/docs/inventory/create-movement) or an adjustment |

Once the reason is solved, repeat the `DELETE`.

---

# Overview

URL: https://marketapi.shop/docs/fulfillment-inbound-shipments/overview

> How to send seller stock to the marketplace warehouse (Full, FBA).

A **fulfillment inbound shipment** moves stock from the seller warehouse to the marketplace warehouse (Full, FBA and similar). It has items with `quantity` and `receivedQuantity`, a destination and a `status`. Without it, the units you sent would still count as available in the local warehouse.

Where it fits in the journey: [Stay in sync](/docs/how-it-works/staying-in-sync).

## Endpoints

| Method   | Path                                     | What it does                                                  |
| -------- | ---------------------------------------- | ------------------------------------------------------------- |
| `GET`    | `/v1/fulfillment-inbound-shipments`      | [List shipments](/docs/fulfillment-inbound-shipments/list)    |
| `POST`   | `/v1/fulfillment-inbound-shipments`      | [Create shipment](/docs/fulfillment-inbound-shipments/create) |
| `GET`    | `/v1/fulfillment-inbound-shipments/{id}` | [Get shipment](/docs/fulfillment-inbound-shipments/get)       |
| `PATCH`  | `/v1/fulfillment-inbound-shipments/{id}` | [Update shipment](/docs/fulfillment-inbound-shipments/update) |
| `DELETE` | `/v1/fulfillment-inbound-shipments/{id}` | [Cancel shipment](/docs/fulfillment-inbound-shipments/cancel) |

All require `Authorization: Bearer mk_your_api_key`. See [Authentication](/docs/authentication).

## States

`draft` → `submitted` → `scheduled` → `in_transit` → `receiving` → `received`. Before `in_transit`, it can become `cancelled`.

## Stock

1. **Create:** reserves the quantity in the source warehouse.
2. **`in_transit`:** the item leaves the warehouse, with the `fulfillment_transfer` movement (lowers `reserved` and `onHand`).
3. **`received`:** shows up as an observation of the fulfillment location, not as your balance.

A failed submit leaves the shipment in `draft`, with `statusReasons`. A `PATCH` resends it.

## Webhooks

`fulfillment_inbound_shipment.created` and `fulfillment_inbound_shipment.updated` (with `previousStatus`). See [Receiving webhooks](/docs/receiving-webhooks).

---

# List shipments

URL: https://marketapi.shop/docs/fulfillment-inbound-shipments/list
Endpoint: `GET /v1/fulfillment-inbound-shipments` (coming soon)

> Lists the account fulfillment inbound shipments, paginated and filtered.

Returns the stock shipments sent to the marketplace warehouses. Filter by `sellerId`, `connectionId` or `status`. It is a local read: it does not call the marketplace.

One shipment: [Get shipment](/docs/fulfillment-inbound-shipments/get).

---

# Create shipment

URL: https://marketapi.shop/docs/fulfillment-inbound-shipments/create
Endpoint: `POST /v1/fulfillment-inbound-shipments` (coming soon)

> Registers a stock shipment to the marketplace warehouse.

Send `connectionId`, `destinationLocationId` (a fulfillment-type location) and the `items` (1 to 100, with `variantId` and `quantity`). Responds `201` with the shipment in `draft` and a `jobId` that sends it to the marketplace.

On creation, the stock in the source warehouse is reserved. A variant not linked to the Connection responds `409` (`inbound_item_unlinked`); insufficient balance responds `409`. A marketplace without the operation responds `422`.

---

# Get shipment

URL: https://marketapi.shop/docs/fulfillment-inbound-shipments/get
Endpoint: `GET /v1/fulfillment-inbound-shipments/{id}` (coming soon)

> Returns one shipment with items and received quantities.

Use it to follow the shipment `status` and, on each item, the `receivedQuantity` the marketplace counted. `statusReasons` has the reasons for a refusal.

It is a local read. A state change arrives in the `fulfillment_inbound_shipment.updated` webhook.

---

# Update shipment

URL: https://marketapi.shop/docs/fulfillment-inbound-shipments/update
Endpoint: `PATCH /v1/fulfillment-inbound-shipments/{id}` (coming soon)

> Changes items, schedule, or marks the shipment as dispatched.

In `draft`, `submitted` or `scheduled`, you can change `items`, `scheduledFor` and `volumeCount`; the reservation follows the difference. With `status: in_transit` (plus `carrier` and `trackingCode`), you say the boxes were dispatched: the stock leaves the warehouse, with the `fulfillment_transfer` movement.

Changing only `reference` creates no job. An invalid transition responds `409` (`inbound_shipment_status`).

---

# Cancel shipment

URL: https://marketapi.shop/docs/fulfillment-inbound-shipments/cancel
Endpoint: `DELETE /v1/fulfillment-inbound-shipments/{id}` (coming soon)

> Cancels the shipment and releases the stock reservation.

The shipment stays listed, with `status: cancelled`. Without `externalId` (it never reached the marketplace), responds `204` and releases the reservation right away. With `externalId`, responds `202` with a [job](/docs/jobs/overview), and the reservation is released when the marketplace confirms.

From `in_transit` on, it can no longer be cancelled: it responds `409` (`inbound_shipment_status`).

---

# Marketplace products

URL: https://marketapi.shop/docs/marketplace-products/overview

> The content a marketplace shares across offers.

A **MarketplaceProduct** is the content of an item inside a Connection: title, description, attributes, media, category, and variations. It is what the marketplace displays and **shares** across offers. Each marketplace has its own name for it:

| Marketplace   | What the MarketplaceProduct is |
| ------------- | ------------------------------ |
| Mercado Livre | User Product (family)          |
| Amazon        | parent ASIN                    |
| Magalu        | product group                  |

Price, inventory, and commercial rules do not live here. They live on the offer, the [Listing](/docs/listings/overview).

## Relation to Listing and binding

```
MarketplaceProduct ── 1:N ── Listing        (the offer)
        │                       │
 MarketplaceProductVariant ── ListingVariantBinding   (the offer line)
        │
     Variant (local catalog)
```

* Every Listing points to one MarketplaceProduct (`marketplaceProductId`).
* Each variation of the MarketplaceProduct (`MarketplaceProductVariant`) links a local [Variant](/docs/products/overview) to what the marketplace shows.
* The Listing [binding](/docs/listings/replace-bindings) points to a MarketplaceProduct variation.
* Two Listings on the same MarketplaceProduct share the content and differ in the offer: that is a **price A/B test**.

You do not create a MarketplaceProduct on its own. It is born [with the Listing](/docs/listings/create), by clone with `marketplaceProduct: "copy"`, or by import.

## Content inheritance

Whatever you leave empty is inherited. From most specific to most general:

* **Content:** MarketplaceProduct variation ← MarketplaceProduct ← Variant ← Product.
* **Offer:** Listing line (binding) ← Listing.

Use [`resolved`](/docs/marketplace-products/resolved) to see the final value and where it came from.

## `publicationState`

Tells whether the local content has reached the marketplace: `never_published`, `dirty` (changed and not published), `pending`, `clean`, or `failed`. A [PATCH](/docs/marketplace-products/update) or a [variations](/docs/marketplace-products/replace-variants) replacement marks it `dirty`.

## No route here calls the marketplace

The routes in this group read and write only the local record, except [content publication](/docs/marketplace-products/publish). Content reaches the marketplace when you call that route (content only, no offer) or [publish the Listing](/docs/listings/publish).

<MarketplaceCapabilityTags capability="listings.read" />

---

# List marketplace products

URL: https://marketapi.shop/docs/marketplace-products/list
Endpoint: `GET /v1/marketplace-products`

> Lists the account's marketplace content, paginated.

Use this collection to see the content each Connection has. Filter by `connectionId` for one channel. Each item shows `publicationState` and `externalId`, which only exists after publication or import. To find which offers use an item, filter [listings](/docs/listings/list) by `marketplaceProductId`.

---

# Get marketplace product

URL: https://marketapi.shop/docs/marketplace-products/get
Endpoint: `GET /v1/marketplace-products/{marketplaceProductId}`

> Gets a marketplace product by id.

Returns the local content (`desired`), the last fact the marketplace confirmed (`observed`, read-only), the `catalogReference`, and `publicationState`. It does not include variations: use [list variations](/docs/marketplace-products/list-variants). On Mercado Livre, the title the channel generated shows in `observed.content`.

---

# Update marketplace product content

URL: https://marketapi.shop/docs/marketplace-products/update
Endpoint: `PATCH /v1/marketplace-products/{marketplaceProductId}`

> Updates only the local content, with merge.

Changes the desired content without calling the marketplace. The merge works like this:

* `content` is merged field by field: sending `content.title` keeps the existing description.
* `content.attributes` is merged by each attribute `key`: a sent key replaces the stored one in place, `{ "key": "origin", "value": null }` removes the key, a new key goes to the end, and keys you do not send stay. `null` only works here: when you create the content or replace the variations, every attribute has a value. Attributes use the same typed format as [Product attributes](/docs/products/create).
* `variationAxes` replaces the whole list.

Every PATCH marks `publicationState: dirty`. The content reaches the marketplace when you [publish the Listing](/docs/listings/publish). Every Listing on the product shares the change.

---

# Get resolved marketplace product content

URL: https://marketapi.shop/docs/marketplace-products/resolved
Endpoint: `GET /v1/marketplace-products/{marketplaceProductId}/resolved`

> Gets content inheritance and the source of each field.

Use before publishing. It does not query the marketplace or change anything. For the product and for each variation, it shows the final value of each field and its source: MarketplaceProduct variation, MarketplaceProduct, Variant, or Product.

The order is: MarketplaceProduct variation → MarketplaceProduct → Variant → Product. The product-level content uses the Product of the first variation, without a Variant. The offer (price) is not here: see the [Listing `resolved`](/docs/listings/resolved).

---

# List marketplace product variations

URL: https://marketapi.shop/docs/marketplace-products/list-variants
Endpoint: `GET /v1/marketplace-products/{marketplaceProductId}/variants`

> Gets the complete variation map.

Returns `{ data }` with every variation, without pagination, like a [kit BOM](/docs/products/list-components): the map is replaced whole by the [PUT](/docs/marketplace-products/replace-variants), with at most 100 items. The order is the one of the last `PUT`; a variation added later (by a [binding](/docs/listings/replace-bindings) or by an import) goes last. Each variation links a local Variant to what the marketplace shows; `externalVariantId` is the id the marketplace assigned, or `null` before publication.

---

# Replace marketplace product variations

URL: https://marketapi.shop/docs/marketplace-products/replace-variants
Endpoint: `PUT /v1/marketplace-products/{marketplaceProductId}/variants`

> Atomically replaces the complete variation map.

Send the **complete list** that must hold after the call. This is a `PUT`: whatever you leave out is removed.

* The body order becomes the map order: the `GET` returns the variations in it.
* A Variant that was already in the map keeps the same variation id.
* One that leaves is removed.
* An omitted `externalVariantId` keeps what the marketplace already assigned.
* The replacement marks `publicationState: dirty` and does not call the marketplace.

## Common errors

* **`409 CONFLICT` with `details.reason` equal to `marketplace_product_variant_in_use`**: an offer line (binding) still sells the variation that would leave. Replace the [binding map](/docs/listings/replace-bindings) first.
* **`409 CONFLICT` with `details.reason` equal to `variant_archived` or `product_archived`**: a Variant in the body is archived, or active in an archived product.

---

# Publish marketplace product content

URL: https://marketapi.shop/docs/marketplace-products/publish
Endpoint: `POST /v1/marketplace-products/{marketplaceProductId}/publish`

> Queues the publication of the content alone, with no offer.

Publishes title, description, attributes, media, and variants, with no price or stock. It is an **asynchronous order**: the `202` response carries a Job; follow it in [Jobs](/docs/jobs/items).

`mode` (default `auto`) chooses between create and update. These errors come before the Job, as `409 CONFLICT`:

* `create` on an already published product: `details.reason` equal to `marketplace_product_published`.
* `update` on a never published product: `marketplace_product_unpublished`.
* Product with no variants: `marketplace_product_without_variants`.

While the Job runs, `publicationState` is `pending`. At the end it becomes `clean` (or `dirty`, if the content changed during the Job) or `failed`, and each step emits `marketplace_product.updated`. If the Connection does not offer `listings.publish`, it responds `422 CAPABILITY_UNSUPPORTED` without creating a Job; check the [capabilities](/docs/connections/capabilities).

---

# Get the catalog link

URL: https://marketapi.shop/docs/marketplace-products/catalog-reference
Endpoint: `GET /v1/marketplace-products/{marketplaceProductId}/catalog-reference` (coming soon)

> The requested link, the confirmed one and the eligibility.

Shows the content link to a marketplace catalog product, in three parts: `desired` (what you asked), `observed` (what the marketplace confirmed) and `eligibility` (the last check). It only reads the database: it does not call the marketplace.

To link, use [Set link](/docs/marketplace-products/replace-catalog-reference). Flow: [Publish listings](/docs/how-it-works/publishing).

---

# Set the catalog link

URL: https://marketapi.shop/docs/marketplace-products/replace-catalog-reference
Endpoint: `PUT /v1/marketplace-products/{marketplaceProductId}/catalog-reference` (coming soon)

> Links the content to a catalog product, locally only.

Sets the link `desired`: `{ catalogProductId, type }`. The change is **local**: the marketplace only learns about it on the next [publication](/docs/marketplace-products/publish). The product goes to `publicationState: dirty` and the previous eligibility is cleared.

A marketplace without a catalog returns `422`. To find the `catalogProductId`, use [Candidates](/docs/marketplace-products/catalog-candidates).

---

# Remove the catalog link

URL: https://marketapi.shop/docs/marketplace-products/delete-catalog-reference
Endpoint: `DELETE /v1/marketplace-products/{marketplaceProductId}/catalog-reference` (coming soon)

> Clears the requested link, locally only.

Clears the link `desired` and returns `204`. As with the [PUT](/docs/marketplace-products/replace-catalog-reference), the change is local and the product becomes `dirty`. The link goes away on the marketplace on the next publication.

---

# Find catalog candidates

URL: https://marketapi.shop/docs/marketplace-products/catalog-candidates
Endpoint: `GET /v1/marketplace-products/{marketplaceProductId}/catalog-candidates` (coming soon)

> Catalog products that match the content.

Searches the marketplace catalog for products that match this content. Each candidate says how it matched in `matchedBy` (`gtin`, `brand_model` or `text`) and which variants matched.

The search uses the Variant GTINs, the brand, the model and the title. It is a live read, with no Job. Pick one and link it with [Set link](/docs/marketplace-products/replace-catalog-reference).

---

# Check catalog eligibility

URL: https://marketapi.shop/docs/marketplace-products/check-catalog-eligibility
Endpoint: `POST /v1/marketplace-products/{marketplaceProductId}/catalog-eligibility-checks` (coming soon)

> Asks the marketplace if the content can use the catalog product.

Asks the marketplace whether the content is eligible for the linked catalog product. It is a Job: it returns `202` with the `jobId`, and the result goes to `eligibility` in [Get the link](/docs/marketplace-products/catalog-reference).

The result is `eligible`, `not_eligible` or `unknown`, with the reasons, the status of each variant and the fields the catalog locks (`blockedFields`). Follow it in [Jobs](/docs/jobs/overview).

---

# Listings

URL: https://marketapi.shop/docs/listings/overview

> The offer in a Connection: price, inventory, and commercial rules.

A **Listing** is the **offer** in a Connection: price, inventory, and commercial rules of an ad on Mercado Livre, Amazon, TikTok Shop, or another channel. The content the marketplace displays (title, description, attributes, media, variations) lives on the [MarketplaceProduct](/docs/marketplace-products/overview), which the Listing points to with `marketplaceProductId`.

The Listing does not replace the local Product and is not the source of truth for your items. One Product and its Variants can supply N Listings.

## Price A/B × content A/B

| I want to test | How                                                                                     | MarketplaceProduct |
| -------------- | --------------------------------------------------------------------------------------- | ------------------ |
| Price          | two Listings on the **same** MarketplaceProduct (`marketplaceProduct: "same"` on clone) | shared             |
| Content        | two Listings on **different** MarketplaceProducts (`marketplaceProduct: "copy"`)        | one for each       |

Changing a MarketplaceProduct's title changes all of its offers. To test a title, create another MarketplaceProduct with [clone](/docs/listings/clone).

Some marketplaces accept only **one offer per MarketplaceProduct**. There, a price A/B test returns `422 CAPABILITY_UNSUPPORTED`; use a content A/B test.

The full model of Listing and binding, and where each published field comes from, with diagrams: [Connections and listings](/docs/data-model/connections-and-listings).

## The three representations

| Representation | Written by                         | Purpose                                                                              |
| -------------- | ---------------------------------- | ------------------------------------------------------------------------------------ |
| `desired`      | your application, through PATCH    | local intent of the offer: `desired.offer` (price, tiers, schedule, terms, warranty) |
| `observed`     | import, reconciliation, or inbound | most recent marketplace-confirmed fact; read-only                                    |
| `resolved`     | MarketAPI, on request              | final value per offer line and inheritance provenance                                |

The Listing's `desired` has only `offer`. Sending `content`, `attributes`, or `fulfillment` returns `400`: those fields belong to the MarketplaceProduct.

Offer precedence is: line override (**binding**, `desired.offer`) → `Listing.desired.offer`. For content, see the [MarketplaceProduct inheritance](/docs/marketplace-products/overview). Absence means “inherit”; it does not mean delete a marketplace value. Read [resolved data](/docs/listings/resolved) before publishing.

## End-to-end flow: from catalog to published offer

1. Check the Connection's [Connection capabilities](/docs/connections/capabilities). The API can perform an external operation only when the connected adapter implements it.
2. Create or import Products and Variants. Define SKUs/GTINs and inventory at the Variant level.
3. [Create the local Listing](/docs/listings/create). It creates the MarketplaceProduct with it, or reuses one you pass. No external call exists at this stage.
4. Fill in the content on the [MarketplaceProduct](/docs/marketplace-products/update) and, if needed, its [variations](/docs/marketplace-products/replace-variants).
5. [Replace the binding map](/docs/listings/replace-bindings), linking each offer line to a MarketplaceProduct variation.
6. Read [`resolved`](/docs/listings/resolved), edit `desired` as many times as needed, and configure the [sync policy](/docs/listings/update-sync-policy).
7. [Publish](/docs/listings/publish) explicitly. The response is a Job; track it until it reaches a final result.
8. For an offer that already exists in a marketplace, use Connection import/reconciliation. External changes arrive in `observed` and in webhooks without changing Product or Variant.

## Mercado Livre: the title comes from the MarketplaceProduct

Mercado Livre generates the ad title from `family_name`. So the MarketplaceProduct's `desired.content.title` is sent as `family_name`, and the title the channel generated shows in `observed`, not in what you sent. The title is shared by every offer on the same MarketplaceProduct.

## States that must not be confused

* `marketplaceStatus`: lifecycle reported by the channel, such as `active`, `paused`, `closed`, `deleted`, `scheduled`, `suppressed`, or `frozen`. Layered next to it: `buyable` and `discoverable` (`true`, `false`, or `null` when unknown), `pendingRevision` (revision under review), and `statusReasons` (`code`, `message`, and `externalCode` from the channel). The Listing and each binding have their own; a line with no layer of its own inherits the offer's.
* `publicationState`: state of local intent, such as `never_published`, `dirty`, `pending`, `clean`, or `failed`. How each one changes: [States and events](/docs/data-model/states-and-events#listing-publicationstate).
* `linkState`: quality of the local link, `linked` with at least one binding, `unlinked` with none, `ambiguous` when an import found more than one Variant for a variation.

## What is stuck

When a listing does not publish, or is not the way you expected, ask in this order:

| Question                                     | Route                                               |
| -------------------------------------------- | --------------------------------------------------- |
| What blocks publishing or selling?           | [issues](/docs/listings/issues)                     |
| What is left to publish? What changed there? | [diff](/docs/listings/diff)                         |
| What happened, and who did it?               | [activity](/docs/listings/activity)                 |
| Why did the last publication fail?           | [publish-attempts](/docs/listings/publish-attempts) |
| Was it relisted or migrated?                 | [relations](/docs/listings/relations)               |

All five only read what MarketAPI already stores. None of them calls the marketplace.

## Conflicts

A conflict is not just `desired` differing from `observed`: the two almost always differ, because the marketplace adds fields you never sent. There is a conflict when you have a **local change not yet published** and the **marketplace changed the same group**. A change on one side only is a pending item of yours or a change of theirs, not a conflict.

Each group (`content`, `catalog`, `offer`, `inventory`, `status`) keeps what the last publication confirmed. The [diff](/docs/listings/diff) compares both sides with that base and gives one `state` per group:

| `state`               | You changed | The marketplace changed |
| --------------------- | ----------- | ----------------------- |
| `in_sync`             | no          | no                      |
| `pending_publication` | yes         | no                      |
| `external_change`     | no          | yes                     |
| `conflict`            | yes         | yes                     |

A group that was never published is `never_published`. Only `content` and `offer` become `conflict` and open a [conflict](/docs/listings/conflicts) record. In `catalog` and `inventory`, with both sides changed, the state is `pending_publication`: the next publication sends your value. `status` has no local value.

The record is born when the two sides meet: a notification or reconciliation arrives with `inbound` active and your edit already pending, or you edit after the outside change had already arrived. MarketAPI keeps your `desired`, publishes nothing and sends `listing.conflict_created`. You [resolve](/docs/listings/resolve-conflict) it with one of four resolutions:

* `keep_local`: your value stays.
* `accept_external`: the marketplace value becomes yours.
* `inherit`: your override goes away, and the field inherits again.
* `manual`: you send the value in `value`.

**Resolving never publishes.** It only changes the local `desired`; for the marketplace to follow, [publish](/docs/listings/publish) afterwards.

## Current support

The indicators are a static product overview. Read the [complete marketplace matrix](/docs/marketplace-support) and, before showing an action in your app, read the [Connection capabilities](/docs/connections/capabilities) for the Connection that will receive the command.

<MarketplaceCapabilityTags capability="listings.read" />

<MarketplaceCapabilityTags capability="listings.publish" />

<MarketplaceCapabilityTags capability="listings.remove" />

<Callout type="warn">
  Do not assume every marketplace has the same operation. Before showing import,
  publish, remove, inventory, or catalog controls, read the Connection
  capabilities. A Mercado Livre Connection can currently authenticate; the
  availability of listing operations is always the actual response of this
  endpoint, never a generic API promise.
</Callout>

## External catalog

The `catalogReference` lives on the **MarketplaceProduct**, not on the Listing: it records that the content is associated with an external catalog product and the fields that catalog blocks. It **does not** become the local Product: your base is still required for SKU, inventory, kits, audit history, and multiple offers. Use marketplace-specific options only in that marketplace's namespace, in [`marketplaceOptions`](/docs/listings/update-marketplace-options), or in `oneTimeMarketplaceOptions` for a single publication.

---

# Create local listing

URL: https://marketapi.shop/docs/listings/create
Endpoint: `POST /v1/sellers/{sellerId}/listings`

> Creates a listing draft in an active Connection.

This endpoint creates only the local offer record. The Connection must belong to the same Seller and be `active`, but the API **does not** call the marketplace, create an external item, or reserve inventory. The Listing starts without an external identity and without bindings until you define them.

The content lives on the [MarketplaceProduct](/docs/marketplace-products/overview). Pick one path:

* **`marketplaceProductId`**: reuses a MarketplaceProduct from the same Connection. This is the price A/B test: two offers, same content.
* **`marketplaceProduct: { desired }`**: creates a new MarketplaceProduct with that content.
* **Neither**: creates an empty MarketplaceProduct that you fill in later with the [PATCH](/docs/marketplace-products/update).

Sending both returns `400`. A `marketplaceProductId` from another Connection also returns `400`.

`desired` carries only the offer, in `desired.offer` (price, reference price, purchase limit). `content`, `attributes`, and `fulfillment` return `400`: they belong to the MarketplaceProduct. `marketplaceOptions` is persistent and must be namespaced by marketplace, such as `mercadolivre`; do not use it for common data that the API already normalizes.

The default policy accepts inbound, permits explicit outbound commands, and keeps automatic inventory off. Create the Listing, [add bindings](/docs/listings/replace-bindings), review [`resolved`](/docs/listings/resolved), and only then [publish](/docs/listings/publish).

## Common errors

* **`422 CAPABILITY_UNSUPPORTED`**: the marketplace accepts only one offer per MarketplaceProduct (`listings.offersPerMarketplaceProduct: one`) and the `marketplaceProductId` already has a Listing.
* **`404 NOT_FOUND`**: the `marketplaceProductId` does not exist or belongs to another account.

---

# List listings

URL: https://marketapi.shop/docs/listings/list
Endpoint: `GET /v1/listings`

> Lists local and imported listings.

Use this collection as the operational view of every account offer, whether it is a local draft or an imported marketplace ad. Each result belongs to exactly one Connection and retains its external identity when it already exists.

Filter by `connectionId` to show one channel, by `marketplaceStatus` for work such as “all paused listings,” by `marketplaceProductId` to see the offers that share one content, and by `publicationState` to find pending work, local changes not yet published (`dirty`), publications in progress (`pending`), or failed ones (`failed`). These filters represent different axes; a Listing may be `active` in the marketplace and still be locally `dirty`.

An `unlinked` Listing stays visible on purpose. It means the offer was created or imported but the API does not yet know which local Variant should receive inventory or participate in publication. Correct it with [bindings](/docs/listings/replace-bindings), rather than creating a duplicate offer.

---

# Get listing

URL: https://marketapi.shop/docs/listings/get
Endpoint: `GET /v1/listings/{listingId}`

> Gets a local listing by id.

This is the main read of an offer record: it shows the Connection, `marketplaceProductId`, the local offer (`desired.offer`), stored external state, policy, and the three lifecycle states. Use it to load the screen or cache for one specific listing.

The content (title, attributes, `catalogReference`) is not here: read the [MarketplaceProduct](/docs/marketplace-products/get). The response also does not calculate inheritance or return bindings. To prepare publication, read [`resolved`](/docs/listings/resolved); to edit inventory/variation association, read [bindings](/docs/listings/list-bindings); to show only the most recent external fact, read [`observed`](/docs/listings/observed).

`externalMarketplaceId` exists only after confirmed import or publication. Do not use its absence to conclude that the base record or Variant does not exist — it only indicates that this offer is still a local draft.

---

# Update listing intent

URL: https://marketapi.shop/docs/listings/update
Endpoint: `PATCH /v1/listings/{listingId}`

> Updates only the listing's desired local data.

Use this PATCH to adjust the offer deliberately, and publish only when you are satisfied. It never calls the marketplace; every change marks local intent as `dirty`.

Only the **offer** (`desired.offer`) changes here. Title, description, media, and attributes belong to the MarketplaceProduct: use [its PATCH](/docs/marketplace-products/update). Sending `content`, `attributes`, or `fulfillment` returns `400`.

`desired` uses deep merge, key by key. For `offer`, provide the required `priceInCents` (price in cents) and `currency` pair. Besides price, the offer accepts quantity tiers, scheduled prices, audience, minimum purchase, handling time, warranty, and fulfillment channels.

**`marketplaceOptions` no longer goes through here:** sending the field returns `400`. Use [Get](/docs/listings/get-marketplace-options) and [Update marketplace options](/docs/listings/update-marketplace-options). For an exception in a single publication, use `oneTimeMarketplaceOptions` in [publish](/docs/listings/publish).

---

# Remove listing

URL: https://marketapi.shop/docs/listings/remove
Endpoint: `DELETE /v1/listings/{listingId}`

> Removes local data or explicitly requests remote removal.

This `DELETE` has two independent decisions: what to do with the local record and whether to make a remote order. Choose `publication=keep` to remove **only** the local Listing; it never touches the marketplace. It is the right choice when you want to stop managing an external offer without unpublishing it.

With `publication=auto` (the default), an offer that already has `externalMarketplaceId` receives a remote removal only when outbound is enabled. With `publication=delete`, you explicitly request remote removal whenever an external identity exists. In both remote cases, the response is `202` with a Job; the local record is removed only after the worker confirms the operation.

A deletion or closure made in the marketplace never triggers a local `DELETE` by itself. The Listing remains for audit with the received external status, and your application is notified by webhook. Product and Variant are not removed by any of these choices.

---

# Clone listing for A/B

URL: https://marketapi.shop/docs/listings/clone
Endpoint: `POST /v1/listings/{listingId}/clone`

> Creates a local draft with no external identity.

Cloning is the safe way to start an A/B experiment. The copy uses the source Listing's Connection, policy, and local Variants, but receives a new `listingId` and does not copy `observed`, `externalMarketplaceId`, or the external identities of the lines. It therefore never accidentally points at the published ad.

The `marketplaceProduct` field picks the kind of test:

| Value            | Test        | What happens                                                                                                                                                 |
| ---------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `same` (default) | **price**   | the clone uses the same MarketplaceProduct; bindings point at the same variations                                                                            |
| `copy`           | **content** | creates another MarketplaceProduct with the same local content (`externalId` and `catalogReference` empty) and equivalent variations; bindings point at them |

The clone already has bindings, but every binding has no `externalId` and no `externalSku`: that identity will be created or discovered during the new publication. It is not published automatically.

Body overrides are applied by **top-level field**. If you send `desired.offer` or `marketplaceOptions.mercadolivre`, send the complete object you intend to retain at that level; it replaces the corresponding source Listing object. With `copy`, change the copy's content on the [MarketplaceProduct](/docs/marketplace-products/update). Then review [`resolved`](/docs/listings/resolved) and publish the clone as a new offer.

## Common errors

* **`422 CAPABILITY_UNSUPPORTED`**: with `same`, the marketplace accepts only one offer per MarketplaceProduct. Use `copy`.
* **`409 CONFLICT` with `details.reason` equal to `variant_archived` or `product_archived`**: a binding of the source listing points at an archived Variant (or one active in an archived product). Nothing is cloned.

---

# Publish listing

URL: https://marketapi.shop/docs/listings/publish
Endpoint: `POST /v1/listings/{listingId}/publish`

> Queues an explicit marketplace publication.

Publishing is an **asynchronous order**, not immediate confirmation that the marketplace accepted the offer. The `202` response contains a Job; follow its items in [Jobs](/docs/jobs/items) and treat the final result as the source of truth. While the order is running, the Listing is `pending`.

Before calling it, check [capabilities](/docs/connections/capabilities), keep `syncPolicy.outbound.enabled` on, and ensure at least one binding exists. If you provide `bindingIds`, every one must belong to the Listing; omitting the field orders publication of every binding. Use `mode: "auto"` to let the adapter choose create or update, or choose `create`, `update`, and `recreate` only when the channel capability/documentation justifies it.

`oneTimeMarketplaceOptions` is the bypass for one order: it stays in the marketplace namespace, is adapter-validated, and travels only in this Job. It does not alter saved `desired` or `marketplaceOptions`. Use it, for example, for an operational exception that must not be repeated in the next publication.

---

# Validate the offer

URL: https://marketapi.shop/docs/listings/validate
Endpoint: `POST /v1/listings/{listingId}/validate` (coming soon)

> Checks the offer before publishing, without publishing.

Runs a full check of the offer: the local rules and the marketplace validation. It does not publish and does not change `desired`. Use it before [publishing](/docs/listings/publish).

Returns `202` with the `jobId`. The result (`valid` and the list of problems) is in the [Job](/docs/jobs/overview) item and in the offer last state. The problems also show up in [issues](/docs/listings/issues).

---

# Run a marketplace action

URL: https://marketapi.shop/docs/listings/marketplace-actions
Endpoint: `POST /v1/listings/{listingId}/marketplace-actions` (coming soon)

> Pause, activate, close or relist the listing.

Requests a native marketplace action on the listing. The body is `{ action, params }`, with `action` from a closed list: `pause`, `activate`, `close` and `relist`. Each action has its own capability; without it, the route returns `422` before creating the Job. An offer never published returns `409`.

Returns `202` with the `jobId`. The new status arrives in `listing.status_changed`. `relist` creates a **new Listing**, with a new marketplace ID, linked to the old one by `relisted_from` (see [relations](/docs/listings/relations)). Flow: [Publish listings](/docs/how-it-works/publishing).

---

# Refresh the offer from the marketplace

URL: https://marketapi.shop/docs/listings/refresh
Endpoint: `POST /v1/listings/{listingId}/marketplace-refreshes` (coming soon)

> Reads the listing again from the marketplace.

Requests a fresh read of the listing on the marketplace and stores the result in `observed`. Use it when you suspect the local state is stale. It follows the offer inbound policy: if it is off, it only emits `listing.external_changed` with `observed_only`.

Returns `202` with the `jobId`. If the read finds a change that clashes with a pending local edit, a [conflict](/docs/listings/conflicts) is born. See [Stay in sync](/docs/how-it-works/staying-in-sync).

---

# Publish listings in batch

URL: https://marketapi.shop/docs/listings/publish-batch
Endpoint: `POST /v1/sellers/{sellerId}/listing-publications` (coming soon)

> Publishes 1 to 100 seller listings in one Job.

Publishes several listings of the seller at once. The body has `items`, from 1 to 100, one per listing (`listingId`, and optionally `mode` and `bindingIds`). A listing that does not exist or belongs to another seller returns `400` and no Job is created.

Returns `202` with the `jobId`, with **one item per listing**. Missing capability, outbound off or a listing without binding fail only that item, and the Job ends `partial`. Publishing has steps (`steps`): if an item fails midway, the retry resumes from the step that stopped, without duplicating the listing. See [Publish listings](/docs/how-it-works/publishing) and [Jobs](/docs/jobs/overview).

---

# Get resolved data

URL: https://marketapi.shop/docs/listings/resolved
Endpoint: `GET /v1/listings/{listingId}/resolved`

> Gets publishable inheritance and field provenance.

Use this read immediately before publication or to explain a preview to the user. It neither calls the marketplace nor changes anything; for every line (binding), it calculates the representation that the API would attempt to send and identifies the source of each field. The response carries the Listing, the MarketplaceProduct, and the list of lines with the resolved value.

The **offer** follows: `binding.desired.offer` → `listing.desired.offer`. The **content** follows: MarketplaceProduct variation → MarketplaceProduct → Variant → Product. A title can therefore come from the MarketplaceProduct, color from the Variant, and brand from the Product in one final representation, and the price can come from the line. If a field is absent from the most specific level, it inherits from the next one.

This response prevents two common mistakes: duplicating on the ad information that already lives on the Product and publishing without noticing a local override replaced it. It is not `observed`: `resolved` is calculated intent; [`observed`](/docs/listings/observed) is what the channel confirmed. For content only, see the [MarketplaceProduct `resolved`](/docs/marketplace-products/resolved).

---

# Get observed state

URL: https://marketapi.shop/docs/listings/observed
Endpoint: `GET /v1/listings/{listingId}/observed`

> Gets the last marketplace-confirmed representation.

`observed` is read-only and is the state of the **offer** the marketplace confirmed (status, price, lines), with hashes by group. It is updated by import, reconciliation, or active inbound, never by local PATCH. The confirmed content, such as the title Mercado Livre generated, lives in the [MarketplaceProduct](/docs/marketplace-products/get) `observed`.

---

# Compare desired and observed

URL: https://marketapi.shop/docs/listings/diff
Endpoint: `GET /v1/listings/{listingId}/diff`

> Shows, per group, what changed here and what changed on the marketplace.

Shows what is left to publish and what changed on the marketplace, one group at a time. Use it before [publishing](/docs/listings/publish) or after a `listing.external_changed` notice. The route only reads what MarketAPI already stores: it does not call the marketplace.

```bash
curl "$BACKEND_URL/v1/listings/<listingId>/diff" \
  -H "Authorization: Bearer mk_your_api_key"
```

The response always has the five groups, in this order: `content` and `catalog` (from the [MarketplaceProduct](/docs/marketplace-products/overview)), `offer`, `inventory` and `status` (from the offer). One group, shortened:

```json
{
  "group": "offer",
  "state": "pending_publication",
  "changedPaths": ["priceInCents"],
  "desired": { "priceInCents": 489900, "currency": "BRL" },
  "observed": { "priceInCents": 499900, "currency": "BRL" }
}
```

## How to read `state`

`desired` and `observed` almost always differ: the marketplace adds fields you never sent. So each side is compared with what the **last publication** confirmed. Only what changed since then counts.

| `state`               | Means                                                                                      | What it asks of you                                                           |
| --------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
| `in_sync`             | Neither side changed since the last publication.                                           | Nothing.                                                                      |
| `pending_publication` | You changed it here and have not published yet.                                            | [Publish](/docs/listings/publish).                                            |
| `external_change`     | It changed on the marketplace and you did not touch this group.                            | Decide: copy the `observed` value into `desired`, or publish to impose yours. |
| `conflict`            | Both sides changed, or there is an open [conflict](/docs/listings/conflicts) on the group. | [Resolve the conflict](/docs/listings/resolve-conflict).                      |
| `never_published`     | The group was never published: there is no base to compare with.                           | [Publish](/docs/listings/publish).                                            |

## Watch out

* **Only `content` and `offer` become `conflict`.** In `catalog` and `inventory`, with both sides changed, the state is `pending_publication`: the next publication sends your value. Inventory comes from your balance; to undo a change made there, [resend the stock](/docs/inventory/publish).
* **`status` has no desired value** (`desired` is `null`). It only goes to `external_change` when the marketplace changes the status. The reason is in [issues](/docs/listings/issues).
* **`changedPaths` lists only what you set** and that differs from `observed`: the keys of the value and, inside it, `attributes.<key>` (in `content`) and `lines.<identity>` (in `inventory`).
* **`conflict` in `content` or `offer` always has a record in [conflicts](/docs/listings/conflicts)** to resolve, whoever moved first: the outside change arriving with your edit pending, or your edit arriving after the outside change.

How it fits: [Stay in sync](/docs/how-it-works/staying-in-sync).

---

# List offer issues

URL: https://marketapi.shop/docs/listings/issues
Endpoint: `GET /v1/listings/{listingId}/issues`

> Lists what blocks the offer from being published or sold.

Lists what blocks the offer from being published or sold, and what the marketplace says about it. Use it before [publishing](/docs/listings/publish) and when a listing stalls. The route only reads: it does not call the marketplace and has no pagination, because the set is small.

```bash
curl "$BACKEND_URL/v1/listings/<listingId>/issues" \
  -H "Authorization: Bearer mk_your_api_key"
```

`error` items come first. With no problem at all, `issues` is empty. Decide by `code`: the `message` is free text and can change.

## Local issues

With `source: local`. They come from MarketAPI's own data.

| `code`                    | `severity` | What to do                                                                                                                            |
| ------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `listing_unlinked`        | `error`    | No Variant linked (or an import matched more than one). [Replace the bindings](/docs/listings/replace-bindings).                      |
| `binding_invalid`         | `error`    | The binding's Variant, or its Product, is archived, or belongs to another seller. Change the link.                                    |
| `price_missing`           | `error`    | No price. Set `desired.offer.priceInCents` on the [listing](/docs/listings/update) or on the line.                                    |
| `outbound_disabled`       | `error`    | Outbound is off. [Turn it on in the sync policy](/docs/listings/update-sync-policy).                                                  |
| `connection_inactive`     | `error`    | The Connection is not active. The seller must reconnect.                                                                              |
| `capability_unsupported`  | `error`    | The marketplace does not publish listings, or the Connection lacks the scope. See the [capabilities](/docs/connections/capabilities). |
| `last_publication_failed` | `error`    | The last publication failed. See the [attempts](/docs/listings/publish-attempts) and publish again.                                   |
| `open_conflict`           | `warning`  | A conflict is waiting. [Resolve it](/docs/listings/resolve-conflict).                                                                 |

## Marketplace issues

With `source: marketplace`. There is one item for each reason in `statusReasons` of the offer and of each line. The `code` is the normalized reason (`out_of_stock`, `policy_violation`…) and `externalCode` is the channel's own. `severity` is `error` when the marketplace says it cannot be bought (`buyable: false`) and `warning` otherwise.

`source: validation` is part of the format, but nothing emits it yet: it arrives with [validate](/docs/listings/validate).

---

# View offer activity

URL: https://marketapi.shop/docs/listings/activity
Endpoint: `GET /v1/listings/{listingId}/activity`

> Timeline of everything that happened to the offer.

Returns the offer timeline, paginated, newest first. Use it to explain to a user why the listing is the way it is.

```bash
curl "$BACKEND_URL/v1/listings/<listingId>/activity?page=1&pageSize=20" \
  -H "Authorization: Bearer mk_your_api_key"
```

Each entry says what happened (`type`), who caused it (`actor`), the Job behind it (`jobId`, or `null`) and where it touched (`changes`: the groups and the paths). **Values are not kept here**: to see them, read the [diff](/docs/listings/diff) or the listing itself.

| `actor`       | Who                                                            |
| ------------- | -------------------------------------------------------------- |
| `client`      | an API call of yours                                           |
| `system`      | our own work, such as a Job or an import                       |
| `marketplace` | a change that came from there (notification or reconciliation) |

The `type` values group like this:

* **Editing:** `created`, `updated`, `sync_policy_changed`, `bindings_changed`.
* **Publishing:** `publication_requested`, `published`, `inventory_published`, `publication_failed`, `removal_requested`.
* **Outside change:** `external_changed`, `status_changed`.
* **Conflict:** `conflict_opened`, `conflict_resolved`.

The types follow the `listing.*` events you get by [webhook](/docs/receiving-webhooks). Changing the content of the [MarketplaceProduct](/docs/marketplace-products/update) shows up here as `updated`, with `groups: ["content"]`, on each of its offers.

---

# List offer relations

URL: https://marketapi.shop/docs/listings/relations
Endpoint: `GET /v1/listings/{listingId}/relations`

> Catalog twin, relisting and migration.

Lists this offer's links to other offers, in both directions, paginated, newest first. Use it to follow a listing that changed ID on the marketplace.

```bash
curl "$BACKEND_URL/v1/listings/<listingId>/relations" \
  -H "Authorization: Bearer mk_your_api_key"
```

| `type`          | Means                                                            |
| --------------- | ---------------------------------------------------------------- |
| `catalog_twin`  | the offer's copy in the marketplace catalog                      |
| `relisted_from` | a [relist](/docs/listings/marketplace-actions) that got a new ID |
| `migrated_from` | the same offer, moved to a new marketplace model                 |

`direction` tells which side you are on. With `outgoing`, the relation starts at this offer (for `relisted_from` and `migrated_from`, this offer came out of the other one). With `incoming`, it ends here (the other one came out of this one). The relation shows up on both offers, each with its own `direction`.

`relatedListingId` is the other offer: read it with [Get listing](/docs/listings/get). The old marketplace ID stays in `externalIds`.

Today the list comes back empty: relations are born with relisting and the catalog twin, which do not exist in the API yet.

---

# List publish attempts

URL: https://marketapi.shop/docs/listings/publish-attempts
Endpoint: `GET /v1/listings/{listingId}/publish-attempts`

> Every attempt to publish or remove the offer.

Lists the attempts to publish or remove the offer, newest first. Each entry is a Job item that touched the offer (`listing.publish` or `listing.remove`). Use it when the offer is `failed` or stuck: the `error` of the failed item says why.

```bash
curl "$BACKEND_URL/v1/listings/<listingId>/publish-attempts" \
  -H "Authorization: Bearer mk_your_api_key"
```

One attempt that failed, shortened:

```json
{
  "jobId": "7c6b5a49-3827-4615-a0b9-c8d7e6f5a4b3",
  "jobItemId": "b4a39281-7065-4f43-9e21-0d1c2b3a4f5e",
  "type": "listing.publish",
  "status": "failed",
  "mode": "auto",
  "error": "The marketplace refused the price.",
  "result": null
}
```

* `status` is `pending`, `succeeded` or `failed`. `finishedAt` is `null` while pending.
* `mode` is the mode that was asked for (`auto`, `create`, `update` or `recreate`). On a removal it is `null`.
* `result` is the item's summary. It is `null` while pending and after a failure.
* `jobId` leads to the [Job](/docs/jobs/overview), and `jobItemId` to the [item](/docs/jobs/items).

Fix what the `error` points at and [publish](/docs/listings/publish) again. Full flow: [Publish listings](/docs/how-it-works/publishing).

---

# List conflicts

URL: https://marketapi.shop/docs/listings/conflicts
Endpoint: `GET /v1/listings/{listingId}/conflicts`

> Outside changes that clash with a pending local change.

Lists the offer conflicts, paginated, newest first. Filter with `status=open` to see what is waiting for you, or `status=resolved` for what was already decided.

```bash
curl "$BACKEND_URL/v1/listings/<listingId>/conflicts?status=open" \
  -H "Authorization: Bearer mk_your_api_key"
```

A conflict exists when the marketplace changed a group **and** that group has a local change not yet published. A change on one side only is not a conflict: see the `state` in the [diff](/docs/listings/diff).

* **Only `content` and `offer` open conflicts.** Each conflict has the `group`, the `paths` that differ and two snapshots: `desired` (your value when it was born, which does not follow new edits) and `observed` (the marketplace value, which follows new outside changes while it is open).
* **It is born when the two sides meet.** Two orders open the same conflict: your edit was already pending and a notification or reconciliation brought the outside change (`source: inbound` or `reconciliation`, with `inbound` active; with `inbound` disabled, no conflict is born), or the outside change arrived first and you edited afterwards (`source: local_edit`). You get `listing.conflict_created` in both cases.
* **Content belongs to the [MarketplaceProduct](/docs/marketplace-products/overview)**, shared by offers. A `content` conflict shows up on the first offer that read the change.
* **MarketAPI does not choose or publish.** Your `desired` stays as it is until you resolve.

Resolve it with [Resolve conflict](/docs/listings/resolve-conflict).

---

# Resolve conflict

URL: https://marketapi.shop/docs/listings/resolve-conflict
Endpoint: `POST /v1/listings/{listingId}/conflicts/{conflictId}/resolve`

> Pick the side that wins, local or marketplace.

Picks the side that wins in an open conflict. It only changes the local `desired`: **resolving never publishes**. No Job is created and nothing goes to the marketplace. To send it, [publish](/docs/listings/publish) afterwards.

```bash
curl -X POST "$BACKEND_URL/v1/listings/<listingId>/conflicts/<conflictId>/resolve" \
  -H "Authorization: Bearer mk_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"resolution":"manual","value":{"priceInCents":479900}}'
```

The response is the conflict, with `status: "resolved"`.

## The four resolutions

| `resolution`      | What it does to `desired`                                                                            | Afterwards                                                 |
| ----------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `keep_local`      | Nothing: your value stays.                                                                           | The group stays `pending_publication`.                     |
| `accept_external` | Each conflicting path takes the value the marketplace has.                                           | The group goes back to `in_sync`. Nothing to publish.      |
| `inherit`         | Removes your override of each path. The field inherits again (in content, from Variant and Product). | It becomes `dirty`: publish for the marketplace to follow. |
| `manual`          | Each path takes what you send in `value`.                                                            | It becomes `dirty`: publish.                               |

`dirty` applies to the Listing (group `offer`) or to the MarketplaceProduct (group `content`).

`value` is required with `manual` and not accepted with the others. Its keys are exactly the `paths` of the conflict (`priceInCents`, or `attributes.<key>` in content).

In all of them, the outside change now counts as seen: the [diff](/docs/listings/diff) stops treating it as new.

## Watch out

* **`inherit` on the price gives `400`:** the price has nothing to inherit from. The result must stay valid for the group.
* **Resolving `content` changes the MarketplaceProduct**, and applies to all of its offers.
* **Events:** `listing.conflict_resolved`. When `desired` changes, `listing.updated` (offer) or `marketplace_product.updated` (content) also arrives.
* **Resolving again** gives `409 CONFLICT` with `details.reason: "conflict_not_open"`. A conflict of another offer or another account gives `404`.

---

# List variant bindings

URL: https://marketapi.shop/docs/listings/list-bindings
Endpoint: `GET /v1/listings/{listingId}/variant-bindings`

> Gets the mapping from external variations to local Variants.

The map explains which local SKU receives each listing inventory or publication. Each line points to a [MarketplaceProduct](/docs/marketplace-products/list-variants) variation (`marketplaceProductVariantId`); `variantId` comes along, read-only. `externalId` is the external id of the offer line, and `desired.offer` is the offer override for that line only. A Listing may remain unlinked after import.

---

# Replace variant bindings

URL: https://marketapi.shop/docs/listings/replace-bindings
Endpoint: `PUT /v1/listings/{listingId}/variant-bindings`

> Atomically replaces the complete binding map.

Send the complete set, including `[]` to unlink. Each line points to a variation of the Listing's MarketplaceProduct. Provide one of these:

* **`marketplaceProductVariantId`**: uses the variation that already exists.
* **`variantId`**: uses the local Variant; if the MarketplaceProduct does not have a variation for it yet, the API **creates** the variation in the same call.

If you send both, they must match. Every Variant must belong to the same Seller, and an external identity (`externalId`) cannot appear twice. On each line, `desired.offer` is the offer override for that line only.

## Common errors

* **`400 VALIDATION_ERROR`** with `details.field` equal to `bindings`: the variation belongs to another MarketplaceProduct, the `variantId` does not match the variation, or the Variant belongs to another Seller.
* **`409 CONFLICT` with `details.reason` equal to `variant_archived` or `product_archived`**: a Variant in the body is archived, or active in an archived product. Replace it with another.

---

# Update variant binding

URL: https://marketapi.shop/docs/listings/update-binding
Endpoint: `PATCH /v1/listings/{listingId}/variant-bindings/{bindingId}`

> Adjusts one offer line without replacing the whole map.

Use it to change **one** offer line without resending the map via [Replace bindings](/docs/listings/replace-bindings). The binding `id` stays the same. Send at least one field.

* `desired.offer` is merged key by key over the line's current override.
* Changed `externalId`, `externalSku`, or the variant? The link becomes `linkSource: manual` and `matchedBy: null`.
* It never calls the marketplace. The Listing becomes `publicationState: dirty` and emits `listing.updated`.

## Common errors

* **`404 NOT_FOUND`**: the binding does not exist or belongs to another Listing.
* **`400 VALIDATION_ERROR`**: `details.field` is `marketplaceProductVariantId` (variant from another MarketplaceProduct, or already linked to another line) or `externalId`/`externalSku` (identity already used by another line of the Listing).
* **`409 CONFLICT`** with `details.reason` equal to `variant_archived`: the Variant is archived.

---

# Remove variant binding

URL: https://marketapi.shop/docs/listings/remove-binding
Endpoint: `DELETE /v1/listings/{listingId}/variant-bindings/{bindingId}`

> Removes one offer line, local record only.

Removes only the line. The MarketplaceProduct variant and the Variant stay as they are, and the marketplace is not called.

* The id leaves `syncPolicy.inventory.eligibleBindingIds`.
* `linkState` is recalculated: with no lines left, it becomes `unlinked`.
* The Listing becomes `publicationState: dirty` and emits `listing.updated`.

Responds `204` with no body. A missing binding, or one from another Listing, returns `404 NOT_FOUND`.

---

# Get synchronization policy

URL: https://marketapi.shop/docs/listings/sync-policy
Endpoint: `GET /v1/listings/{listingId}/sync-policy`

> Gets listing inbound, outbound, and inventory synchronization.

The three directions are independent. Disabled inbound still informs the client by webhook; outbound permits only explicit commands; automatic inventory is opt-in per binding.

---

# Update synchronization policy

URL: https://marketapi.shop/docs/listings/update-sync-policy
Endpoint: `PATCH /v1/listings/{listingId}/sync-policy`

> Replaces a listing's effective policy.

Use it to accept or only observe external changes, authorize publication, and select automatic-inventory bindings. Updating the policy does not publish.

---

# Get marketplace options

URL: https://marketapi.shop/docs/listings/get-marketplace-options
Endpoint: `GET /v1/listings/{listingId}/marketplace-options`

> Returns the marketplace-specific options saved on the listing.

Returns the saved map, by marketplace namespace (for example `{ "mock": { ... } }`). These are the persistent options of the local intent; the ones for a single order (`oneTimeMarketplaceOptions`) do not appear here.

To change them, use [Update marketplace options](/docs/listings/update-marketplace-options).

---

# Update marketplace options

URL: https://marketapi.shop/docs/listings/update-marketplace-options
Endpoint: `PATCH /v1/listings/{listingId}/marketplace-options`

> Merges the listing's marketplace-specific options.

This is the door for `marketplaceOptions` after creation: the [Listing PATCH](/docs/listings/update) returns `400` if it receives that field. Send a map by namespace, with at least one namespace.

The merge is key by key inside the namespace: updating one key preserves the others in that namespace. The response is the whole map. It never calls the marketplace; the Listing becomes `dirty` and emits `listing.updated`.

Only the namespace of the Connection's marketplace is valid. A namespace from another marketplace, a URL, or a credential key returns `400 VALIDATION_ERROR` with `details.field` equal to `marketplaceOptions`.

For an exception in a single publication, use `oneTimeMarketplaceOptions` in [publish](/docs/listings/publish).

---

# Overview

URL: https://marketapi.shop/docs/orders/overview

> Orders imported from marketplaces: status, totals and buyer data.

An order is a sale made on a marketplace. marketapi imports the orders of each Connection, keeps the exact amounts the marketplace reported, and tells you by webhook.

The whole journey, from sale to shipping: [The order lifecycle](/docs/how-it-works/order-lifecycle).

## Endpoints

| Method | Path                                         | What it does                                           |
| ------ | -------------------------------------------- | ------------------------------------------------------ |
| `GET`  | `/v1/orders`                                 | [List orders](/docs/orders/list)                       |
| `GET`  | `/v1/orders/{orderId}`                       | [Get order](/docs/orders/get)                          |
| `GET`  | `/v1/orders/{orderId}/buyer`                 | [Get buyer](/docs/orders/buyer)                        |
| `POST` | `/v1/connections/{id}/order-imports`         | [Import orders](/docs/connections/import-orders)       |
| `POST` | `/v1/connections/{id}/order-reconciliations` | [Reconcile orders](/docs/connections/reconcile-orders) |
| `POST` | `/v1/orders/{orderId}/actions`               | [Run order action](/docs/orders/actions)               |

All of them need `Authorization: Bearer mk_your_api_key`. See [Authentication](/docs/authentication).

## The order is born on the marketplace

The API only reads. There is no `POST`, `PATCH` or `DELETE` for an order. An order arrives in three ways, and all three end up in the same place:

* the marketplace **notification**, the normal path;
* the [import](/docs/connections/import-orders), to bring in a period;
* the [reconciliation](/docs/connections/reconcile-orders), to recover what a lost notification left behind.

Receiving the same order again does not duplicate it or send an event. A notice older than what is already stored is ignored.

## Status on axes

An order does not have a single status. Each axis moves on its own:

| Field               | What it says                                                                |
| ------------------- | --------------------------------------------------------------------------- |
| `status`            | `open`, `completed` or `cancelled`                                          |
| `paymentStatus`     | from `pending` to `paid`, or `partially_refunded`, `refunded`               |
| `fulfillmentStatus` | from `pending` to `shipped` and `delivered`, or `not_delivered`             |
| `invoiceStatus`     | the invoice: `not_required`, `pending`, `submitted`, `accepted`, `rejected` |

`invoiceStatus` only leaves `not_required` or `pending` when the [invoice](/docs/invoices/overview) routes move it.

## Totals

Totals come in components, in cents, and **nothing is recomputed**. `totalInCents` is the total the marketplace reported. The gap to the sum of the components stays in `roundingAdjustmentInCents`:

```text
roundingAdjustmentInCents = totalInCents
  − (subtotalInCents + shippingPaidByBuyerInCents + taxesInCents − order discounts)
```

It is `0` when the components add up, and negative when they exceed the total. Fees (`feesInCents`) stay out of the sum: they are what the marketplace withholds from the seller. `netInCents` is what the seller receives and is `null` when the marketplace does not say. marketapi does not compute it.

## Unlinked line

Each line has `linkState`:

* **`linked`**: marketapi found the binding of the listing sold. `listingId`, `variantBindingId` and `variantId` are set.
* **`unlinked`**: it did not (a listing created outside marketapi, for example). Those three fields are `null`, and the line keeps the external identity (`externalListingId`, `externalVariantId`) and the `externalSku`.

The order is imported either way: an unlinked line breaks nothing. When the binding comes to exist, the next read of the order links the line and an `order.updated` goes out.

## Stock

The Connection [order policy](/docs/connections/order-policy) (coming soon) sets the effect on stock: paid reserves, shipped writes off, cancelled releases.

## Buyer data

The order and the webhook carry no personal data: only `hasBuyerContact`. Name, CPF or CNPJ, e-mail, phone and addresses come out only through [Get buyer](/docs/orders/buyer). Every access is logged, and the data goes away at the end of the retention period. Each field comes with a state:

| State       | What it says                                           |
| ----------- | ------------------------------------------------------ |
| `available` | the value is in `value`                                |
| `masked`    | the marketplace only shows the masked text, in `value` |
| `withheld`  | the marketplace does not show this field               |
| `purged`    | erased at the end of retention; the order stays        |

## Webhooks

* **`order.created`**: the order appeared in marketapi, even if it arrives already cancelled.
* **`order.updated`**: a status, total, line, payment or the link of a line changed.
* **`order.cancelled`**: the order was cancelled. It goes out **alone**, without `order.updated`.

The `data` is `{ order }`, the same order as [Get order](/docs/orders/get), with only IDs and fields with no personal data. The reasons in `statusReasons` come without `message` (free text from the marketplace): use the `code`. Invoices and shipments have their own events: [Invoices](/docs/invoices/overview) and [Shipments](/docs/shipments/overview).

---

# List orders

URL: https://marketapi.shop/docs/orders/list
Endpoint: `GET /v1/orders`

> Lists the account orders, newest first.

Returns the orders of every seller of the account, paginated, newest first (in the order marketapi stored them). Use it as the work queue of your system.

* **Status:** filter by `status`, `paymentStatus` and `fulfillmentStatus`. Each axis moves on its own, so combine them: `paymentStatus=paid&fulfillmentStatus=ready_to_ship` returns what is ready to ship.
* **Purchase period:** `placedFrom` is inclusive and `placedTo` is exclusive.
* **What changed:** `updatedSince` filters the marketapi `updatedAt`, not the marketplace's own time. Keep the instant of your last read and ask again from it. To react right away, use the `order.*` webhooks.

Each item is the whole order, as in [Get order](/docs/orders/get), with no personal data. The buyer data comes out only through [Get buyer](/docs/orders/buyer).

---

# Get order

URL: https://marketapi.shop/docs/orders/get
Endpoint: `GET /v1/orders/{orderId}`

> Returns an order with its lines, payments and totals.

Returns an order by `id`, with its lines, payments, status on separate axes and totals in components. Use it after `order.created` or `order.updated`, or to open an order from the list.

* **Totals:** nothing is recomputed. `totalInCents` is the marketplace total and `roundingAdjustmentInCents` is the gap to the sum of the components (see [Totals](/docs/orders/overview#totals)). `netInCents` is `null` when the marketplace does not say.
* **Unlinked line:** `linkState: "unlinked"` means marketapi did not find the binding. `listingId`, `variantBindingId` and `variantId` are `null`, and `externalListingId`, `externalVariantId` and `externalSku` keep the identity of the item on the marketplace.
* **Reasons:** `statusReasons` explains the state, for example who cancelled. The `message` is free text from the marketplace and only comes here: the webhook carries the `code`.
* **No personal data:** `hasBuyerContact` says whether a buyer is stored. The data comes out through [Get buyer](/docs/orders/buyer).

## Common errors

* **`404 NOT_FOUND`**: the order does not exist or belongs to another account.

---

# Get buyer

URL: https://marketapi.shop/docs/orders/buyer
Endpoint: `GET /v1/orders/{orderId}/buyer`

> Returns the personal data of an order's buyer.

It is the only door to the buyer's personal data: name, `taxId` (CPF or CNPJ), state registration, e-mail, phone and the billing and shipping addresses. Neither the order nor the webhook carries these data.

Each field comes with a `state`:

| State       | What it says                                                    |
| ----------- | --------------------------------------------------------------- |
| `available` | the value is in `value`                                         |
| `masked`    | the marketplace only shows the masked text, which is in `value` |
| `withheld`  | the marketplace does not show this field; there is no `value`   |
| `purged`    | erased at the end of retention; there is no `value`             |

Every call is logged: who read, when and for what. marketapi erases the fields at the end of retention and they become `purged`; the order stays. The deadline is in `retainUntil` (default: 90 days after the order closes, or less if the marketplace requires it). While the order is open, `retainUntil` is `null`. Keep only what you need.

## Common errors

* **`404 NOT_FOUND`** with `Buyer contact not found.`: the order exists but has no buyer stored. Check `hasBuyerContact` in [Get order](/docs/orders/get).
* **`404 NOT_FOUND`** with `Order not found.`: the order does not exist or belongs to another account.

---

# Run order action

URL: https://marketapi.shop/docs/orders/actions
Endpoint: `POST /v1/orders/{orderId}/actions` (coming soon)

> Cancels the order or blocks the buyer, in a job.

Runs an action on the marketplace. The body has `type`: `cancel` (with `reason`, `restock` and an optional `note`) or `block_buyer`. It answers `202` with an `order.action` [job](/docs/jobs/overview); the order changes when the marketplace confirms and `order.updated` (or `order.cancelled`) arrives.

`cancel` moves the order to `cancelled` and releases the stock reservation under the [order policy](/docs/connections/order-policy). If the marketplace does not support the action, the answer is `422 CAPABILITY_UNSUPPORTED`, and no job is created.

---

# Overview

URL: https://marketapi.shop/docs/invoices/overview

> 'Invoices (NF-e): you send the XML, marketapi validates it and sends it to the marketplace.'

marketapi **does not issue** invoices (NF-e). You send the already authorized XML, marketapi validates it and sends it to the marketplace in a job. On many marketplaces the invoice blocks shipping, so it comes before the label.

The whole journey: [The order lifecycle](/docs/how-it-works/order-lifecycle).

## Endpoints

| Method | Path                            | What it does                               |
| ------ | ------------------------------- | ------------------------------------------ |
| `GET`  | `/v1/orders/{orderId}/invoices` | [List order invoices](/docs/invoices/list) |
| `POST` | `/v1/orders/{orderId}/invoices` | [Send invoice](/docs/invoices/create)      |
| `GET`  | `/v1/invoices/{invoiceId}`      | [Get invoice](/docs/invoices/get)          |
| `GET`  | `/v1/invoices/{invoiceId}/xml`  | [Download invoice XML](/docs/invoices/xml) |

## Flow

1. Upload the XML as a `confidential` [asset](/docs/assets/overview).
2. Call [Send invoice](/docs/invoices/create) with the `xmlAssetId`. The invoice is born `pending`, and so is the order `invoiceStatus`.
3. The `invoice.submit` job sends it to the marketplace. The invoice moves to `submitted`.
4. The marketplace answers: `accepted` or `rejected`. The webhook arrives, and the order changes with it.

## Validation

Before creating the job, marketapi checks the XML. If it fails, the answer is `422` with `details.reason`, and nothing is saved: `invoice_xml_invalid`, `invoice_not_authorized`, `invoice_access_key_invalid`, `invoice_issuer_mismatch` or `invoice_total_mismatch`.

Other refusals are `409`: repeated key, cancelled order, or an invoice that was not rejected already exists. To replace a rejected one, send `replacesInvoiceId`.

## States

| `status`    | Meaning                           |
| ----------- | --------------------------------- |
| `pending`   | validated, not sent yet           |
| `submitted` | sent, waiting for the marketplace |
| `accepted`  | accepted                          |
| `rejected`  | rejected; see `statusReasons`     |

## Webhooks

`invoice.accepted` and `invoice.rejected`. They do not carry the issuer tax ID.

Legal custody of the XML is yours, as issuer. marketapi keeps a confidential copy for the order retention period.

---

# List order invoices

URL: https://marketapi.shop/docs/invoices/list
Endpoint: `GET /v1/orders/{orderId}/invoices` (coming soon)

> Lists the invoices sent for an order.

Returns the invoices of an order, paginated. Use it to see the `status` of each one and the reason when it was rejected (`statusReasons`). An invoice that replaces another points to it in `replacesInvoiceId`.

---

# Send invoice

URL: https://marketapi.shop/docs/invoices/create
Endpoint: `POST /v1/orders/{orderId}/invoices` (coming soon)

> Sends the XML of an already authorized invoice for the order.

marketapi does not issue invoices. Upload the authorized XML as a `confidential` [asset](/docs/assets/overview) and send its `xmlAssetId` here (never the XML in the body). To replace a rejected invoice, also send `replacesInvoiceId`.

marketapi validates the XML before accepting it: key, total amount and issuer tax ID. It answers `202` with the invoice as `pending` and an `invoice.submit` [job](/docs/jobs/overview), which sends it to the marketplace. The result arrives as `invoice.accepted` or `invoice.rejected`.

Refusals that create no job: `422` if the XML is invalid, not authorized, or the key, issuer or total do not match; `409` if the key already exists, the order is `cancelled`, or an invoice that was not rejected already exists.

---

# Get invoice

URL: https://marketapi.shop/docs/invoices/get
Endpoint: `GET /v1/invoices/{invoiceId}` (coming soon)

> Returns an invoice by id.

Returns the invoice with access key, number, series, amount, `status` and `statusReasons`. Use it after `invoice.accepted` or `invoice.rejected` to read the reason. The file is in [Download XML](/docs/invoices/xml).

---

# Download invoice XML

URL: https://marketapi.shop/docs/invoices/xml
Endpoint: `GET /v1/invoices/{invoiceId}/xml` (coming soon)

> Returns the invoice XML file.

Returns the XML bytes, as `application/xml` and an attachment (`<key>.xml`). The file is confidential: the response has no public URL, is not cached, and every read is logged.

---

# Overview

URL: https://marketapi.shop/docs/shipments/overview

> Shipments: N:N items with the order, tracking, label and dispatch.

A shipment is the package that takes one or more order items to the buyer. marketapi syncs the shipment, tracking and dispatch deadline, and you request labels and dispatch through jobs.

The whole journey: [The order lifecycle](/docs/how-it-works/order-lifecycle).

## Endpoints

| Method | Path                                          | What it does                                             |
| ------ | --------------------------------------------- | -------------------------------------------------------- |
| `GET`  | `/v1/shipments`                               | [List shipments](/docs/shipments/list)                   |
| `GET`  | `/v1/shipments/{shipmentId}`                  | [Get shipment](/docs/shipments/get)                      |
| `GET`  | `/v1/shipments/{shipmentId}/tracking`         | [Get tracking](/docs/shipments/tracking)                 |
| `GET`  | `/v1/shipments/{shipmentId}/dispatch-options` | [Get dispatch options](/docs/shipments/dispatch-options) |
| `POST` | `/v1/shipments/{shipmentId}/label-requests`   | [Request label](/docs/shipments/request-label)           |
| `POST` | `/v1/shipments/{shipmentId}/dispatches`       | [Dispatch shipment](/docs/shipments/dispatch)            |
| `POST` | `/v1/shipments/{shipmentId}/actions`          | [Run shipment action](/docs/shipments/actions)           |

## Rules

* **N:N with lines.** Each shipment item links an order line to a quantity. One shipment carries lines from several orders, and one line can ship in several shipments.
* **The label is a confidential file** (`pdf` or `zpl`). It never has a public URL. You download it through [asset](/docs/assets/content), and every read is logged.
* **The invoice blocks shipping.** With `invoice_required` in `statusReasons`, label and dispatch answer `409` until the [invoice](/docs/invoices/overview) is accepted.
* **Commands are jobs.** Label, dispatch and actions answer `202` with a [job](/docs/jobs/overview). Without marketplace support: `422 CAPABILITY_UNSUPPORTED`, and no job is created.

## States

`status`: `pending`, `ready_to_ship`, `shipped`, `delivered`, `not_delivered`, `cancelled`. The order `fulfillmentStatus` follows. When the shipment moves to `shipped`, the reserved stock is written off ([order policy](/docs/connections/order-policy)).

## Webhooks

`shipment.updated` (any change) and `shipment.label_ready` (carries `assetId` and `format`).

---

# List shipments

URL: https://marketapi.shop/docs/shipments/list
Endpoint: `GET /v1/shipments` (coming soon)

> Lists the account shipments, paginated and filtered.

Returns the shipments of every seller of the account. Filter by `sellerId`, `connectionId`, `orderId`, `status` or `dispatchDueBefore` to find what must leave today.

---

# Get shipment

URL: https://marketapi.shop/docs/shipments/get
Endpoint: `GET /v1/shipments/{shipmentId}` (coming soon)

> Returns a shipment with items, costs and labels.

Returns the shipment with `mode`, `status`, carrier, dispatch deadline (`dispatchDueAt`), costs, items and labels. Each item links an order line to a quantity: one shipment can carry lines from several orders, and one line can ship in several shipments.

`statusReasons` explains the status, for example `invoice_required`. The label is a confidential file, downloaded through [asset](/docs/assets/content).

---

# Get tracking

URL: https://marketapi.shop/docs/shipments/tracking
Endpoint: `GET /v1/shipments/{shipmentId}/tracking` (coming soon)

> Returns the tracking events of a shipment.

Returns the list of `events` (`status`, `description`, `occurredAt`, `location`) that marketapi has already synced. It does not call the marketplace live. To be told about changes, listen to `shipment.updated`.

---

# Get dispatch options

URL: https://marketapi.shop/docs/shipments/dispatch-options
Endpoint: `GET /v1/shipments/{shipmentId}/dispatch-options` (coming soon)

> Lists how the shipment can be dispatched.

Returns the options the last sync observed (`drop_off`, `pickup` or `self_delivery`), each with `id`, label and deadline. `observedAt` says when they were read: there is no live call to the marketplace. Use the `id` in [Dispatch shipment](/docs/shipments/dispatch).

---

# Request label

URL: https://marketapi.shop/docs/shipments/request-label
Endpoint: `POST /v1/shipments/{shipmentId}/label-requests` (coming soon)

> Requests the shipment label, as pdf or zpl, in a job.

Send the `format` (`pdf` or `zpl`). It answers `202` with a `shipment.label` [job](/docs/jobs/overview). When the label is ready, `shipment.label_ready` arrives with the `assetId`, and you download the file in [Download asset content](/docs/assets/content). It is confidential: it never has a public URL.

Refusals, all before the job: `422` if the format is not supported; `409` if the shipment is not `ready_to_ship` (`shipment_not_ready`) or the invoice blocks the shipment (`invoice_required`). See [Send invoice](/docs/invoices/create).

---

# Dispatch shipment

URL: https://marketapi.shop/docs/shipments/dispatch
Endpoint: `POST /v1/shipments/{shipmentId}/dispatches` (coming soon)

> Records the shipment dispatch, in a job.

Tells the marketplace the package has left. The body accepts `optionId` (from [Get dispatch options](/docs/shipments/dispatch-options)), `carrier`, `trackingNumber` and `dispatchedAt`. It answers `202` with a `shipment.dispatch` [job](/docs/jobs/overview); the shipment moves to `shipped` and `shipment.updated` arrives.

The label rule applies here too: with the invoice pending, the answer is `409 invoice_required`. Once shipped, the order moves to `shipped` and the reserved stock is written off.

---

# Run shipment action

URL: https://marketapi.shop/docs/shipments/actions
Endpoint: `POST /v1/shipments/{shipmentId}/actions` (coming soon)

> Marks ready, updates tracking or records delivery, in a job.

The body has `type`: `mark_ready_to_ship`, `update_tracking` (`carrier`, `trackingNumber`), `mark_delivered` (`deliveredAt`) or `mark_not_delivered` (`reason`). It answers `202` with a `shipment.action` [job](/docs/jobs/overview); the result arrives as `shipment.updated`.

Each action needs marketplace support. Without it, the answer is `422 CAPABILITY_UNSUPPORTED`, and no job is created.

---

# Overview

URL: https://marketapi.shop/docs/returns/overview

> What a return is and the rules that apply to all its endpoints.

A **return** is the product coming back from an order. It has lines (`items`, with `orderLineId` and `quantity`), a reason (`reason`), a `status`, the tracking of the way back (`tracking`) and the amount given back to the buyer (`refundInCents`). On some marketplaces it starts from a [dispute](/docs/disputes/overview); then `disputeId` links the two.

Where it fits in the journey: [The order lifecycle](/docs/how-it-works/order-lifecycle).

## Endpoints

| Method | Path                             | What it does                             |
| ------ | -------------------------------- | ---------------------------------------- |
| `GET`  | `/v1/returns`                    | [List returns](/docs/returns/list)       |
| `GET`  | `/v1/returns/{returnId}`         | [Get return](/docs/returns/get)          |
| `POST` | `/v1/returns/{returnId}/actions` | [Act on a return](/docs/returns/actions) |

All require `Authorization: Bearer mk_your_api_key`. See [Authentication](/docs/authentication).

## States

| `status`       | Meaning                    |
| -------------- | -------------------------- |
| `requested`    | the buyer asked for it     |
| `approved`     | approved                   |
| `in_transit`   | the product is coming back |
| `received`     | the product arrived        |
| `under_review` | under review               |
| `refunded`     | refunded                   |
| `closed`       | closed                     |
| `cancelled`    | cancelled                  |

`refundStatus` is `pending`, `refunded` or `retained`.

## Stock

A `received` return with an `ok` review (or no review, if the marketplace has none) puts the item back in stock, once only (`restockedAt`). A `failed` review, a damaged product, does not go back. It only applies with the order stock policy on and a SKU with `marketapi` authority. A kit goes back by component.

## Actions and webhooks

`allowedActions` lists `action`, `mandatory` and `dueAt`. Every action is a [job](/docs/jobs/overview) (`return.action`). Events: `return.created` and `return.updated`, with no personal data. See [Receiving webhooks](/docs/receiving-webhooks).

---

# List returns

URL: https://marketapi.shop/docs/returns/list
Endpoint: `GET /v1/returns` (coming soon)

> Lists the account returns, paginated and filtered.

Returns the returns of every seller of the account. Filter by `orderId`, `status` or `connectionId` to build your after-sales queue.

Each return has the reason, the returned lines, the tracking of the way back and `refundInCents`. One return: [Get return](/docs/returns/get).

---

# Get return

URL: https://marketapi.shop/docs/returns/get
Endpoint: `GET /v1/returns/{returnId}` (coming soon)

> Returns one return with lines, tracking and allowed actions.

Use it to see the current state of a return: `status`, `refundStatus`, `tracking` and `review`.

`allowedActions` tells you what you can do now and until when (`dueAt`). To act, use [Act on a return](/docs/returns/actions). If a dispute is linked, `disputeId` points to it: [Get dispute](/docs/disputes/get).

---

# Act on a return

URL: https://marketapi.shop/docs/returns/actions
Endpoint: `POST /v1/returns/{returnId}/actions` (coming soon)

> Approves, refunds or records the review of a return.

Sends an action to the marketplace: `approve`, `refund` or `review` (with `outcome` `ok` or `failed`, a reason and evidence photos). Responds `202` with a [job](/docs/jobs/overview); the result arrives in the `return.updated` webhook.

An action outside `allowedActions` responds `409` (`action_not_allowed`), before any job is created. A marketplace without the operation responds `422`. Evidence is a confidential [asset](/docs/assets/overview) of the account.

---

# Overview

URL: https://marketapi.shop/docs/disputes/overview

> What a dispute is and the rules that apply to all its endpoints.

A **dispute** is an open conflict about an order: a claim, a mediation, a cancellation request, a return request or a chargeback. The critical data is the deadline: on the marketplace, a missed action can lose the claim and hurt the seller reputation.

Where it fits in the journey: [The order lifecycle](/docs/how-it-works/order-lifecycle).

## Endpoints

| Method | Path                                | What it does                                  |
| ------ | ----------------------------------- | --------------------------------------------- |
| `GET`  | `/v1/disputes`                      | [List disputes](/docs/disputes/list)          |
| `GET`  | `/v1/disputes/{disputeId}`          | [Get dispute](/docs/disputes/get)             |
| `GET`  | `/v1/disputes/{disputeId}/messages` | [List messages](/docs/disputes/list-messages) |
| `POST` | `/v1/disputes/{disputeId}/messages` | [Send message](/docs/disputes/send-message)   |
| `POST` | `/v1/disputes/{disputeId}/actions`  | [Act on a dispute](/docs/disputes/actions)    |

All require `Authorization: Bearer mk_your_api_key`. See [Authentication](/docs/authentication).

## Type and status

`type`: `claim`, `mediation`, `cancellation_request`, `return_request` or `chargeback`.

`status`: `open`, `waiting_seller`, `waiting_buyer`, `under_review`, `resolved` or `closed`.

## Deadline and allowed actions

* `deadlineAt` is the earliest deadline among the mandatory seller actions.
* `allowedActions` lists `action`, `mandatory` and `dueAt`. Only what is listed can be sent; anything else responds `409`.
* `affectsReputation` says whether the dispute weighs on reputation (`null` when the marketplace does not say).
* `reason` has the normalized code and the marketplace native one.
* Once resolved, `resolution` has `outcome`, `refundInCents` and `decidedBy`.

## Messages and attachments

The message text **never** goes in the webhook: the event only carries IDs. Read the text with [List messages](/docs/disputes/list-messages). Attachments and evidence are confidential [assets](/docs/assets/overview), only for your account.

## Webhooks

`dispute.created`, `dispute.updated` and `dispute.message_received`. See [Receiving webhooks](/docs/receiving-webhooks).

---

# List disputes

URL: https://marketapi.shop/docs/disputes/list
Endpoint: `GET /v1/disputes` (coming soon)

> Lists the account disputes, paginated and filtered.

Returns the disputes of every seller of the account. Sort your queue by `deadlineAt`: a missed action can cost the claim and the seller reputation.

Each dispute has `type`, `status`, `deadlineAt` and `allowedActions`. One dispute: [Get dispute](/docs/disputes/get).

---

# Get dispute

URL: https://marketapi.shop/docs/disputes/get
Endpoint: `GET /v1/disputes/{disputeId}` (coming soon)

> Returns one dispute with deadline, allowed actions and resolution.

Use it to see the state of a dispute: `type`, `status`, `reason`, `deadlineAt` and `allowedActions`. Once resolved, `resolution` has the outcome and `refundInCents`.

If the dispute created a return, `returnId` points to it: [Get return](/docs/returns/get). The conversation is in [List messages](/docs/disputes/list-messages).

---

# List dispute messages

URL: https://marketapi.shop/docs/disputes/list-messages
Endpoint: `GET /v1/disputes/{disputeId}/messages` (coming soon)

> Lists the messages exchanged in the dispute.

Returns the dispute conversation. Each message has `authorRole` (`buyer`, `seller` or `marketplace`), `text` and `attachments`. On your own messages, `deliveryStatus` shows whether it was `pending`, `sent` or `failed`.

The text never goes in the webhook: when `dispute.message_received` arrives, read the message from this route. To reply: [Send message](/docs/disputes/send-message).

---

# Send dispute message

URL: https://marketapi.shop/docs/disputes/send-message
Endpoint: `POST /v1/disputes/{disputeId}/messages` (coming soon)

> Sends a seller message to the buyer or the marketplace.

Sends `text` and, optionally, `attachmentAssetIds`. The message is saved as `pending` and the response is `202` with a [job](/docs/jobs/overview) (`dispute.message`). Then it becomes `sent`, or `failed` if the marketplace refuses it.

An attachment is a confidential [asset](/docs/assets/overview) of the account. Without storage, a message with an attachment responds `503`; text only works as usual.

---

# Act on a dispute

URL: https://marketapi.shop/docs/disputes/actions
Endpoint: `POST /v1/disputes/{disputeId}/actions` (coming soon)

> Refunds, allows a return, escalates or submits evidence on a dispute.

Actions: `refund`, `partial_refund` (with `amountInCents`), `allow_return`, `escalate` and `submit_evidence`. Responds `202` with a [job](/docs/jobs/overview) (`dispute.action`); the result arrives in the `dispute.updated` webhook.

An action outside `allowedActions` responds `409` (`action_not_allowed`), before the job. A marketplace without the operation responds `422`. Evidence is a confidential [asset](/docs/assets/overview) of the account.

---

# Overview

URL: https://marketapi.shop/docs/questions/overview

> What a question is and the rules that apply to all its endpoints.

A question is the **public** doubt a buyer asks on a listing, **before the purchase**. The seller answers, and the answer shows on the listing. It belongs to a Connection and, when the listing was imported, to a Listing (`listingId`).

How customer service works end to end: [Serve buyers](/docs/how-it-works/customer-service).

## Endpoints

| Method | Path                                 | What it does                              |
| ------ | ------------------------------------ | ----------------------------------------- |
| `GET`  | `/v1/questions`                      | [List questions](/docs/questions/list)    |
| `GET`  | `/v1/questions/{questionId}`         | [Get question](/docs/questions/get)       |
| `POST` | `/v1/questions/{questionId}/answers` | [Answer question](/docs/questions/answer) |
| `POST` | `/v1/questions/{questionId}/actions` | [Run an action](/docs/questions/actions)  |

All require `Authorization: Bearer mk_your_api_key`. See [Authentication](/docs/authentication).

## States

| `status`       | Meaning                          |
| -------------- | -------------------------------- |
| `unanswered`   | waiting for an answer            |
| `answered`     | the answer was published         |
| `under_review` | the marketplace is moderating it |
| `deleted`      | the question was deleted         |

The answer has its own state: `sending`, `published`, `under_review`, `rejected` or `failed`.

## Rules

* Only an `unanswered` question can be answered. Before that, the response is `409` `question_not_answerable`.
* Answering and acting are [jobs](/docs/jobs/overview): the API returns `202` and you follow the `id`.
* The author appears only as an external id, with no name.
* Limits (such as the answer length) vary by marketplace. See [Capabilities](/docs/how-it-works/capabilities).

## Webhook

`question.received` arrives when a new question comes in. It carries ids only (`questionId`, `listingId`, `status`…), never the text. Read the text with [Get question](/docs/questions/get). See [Receiving webhooks](/docs/receiving-webhooks).

---

# List questions

URL: https://marketapi.shop/docs/questions/list
Endpoint: `GET /v1/questions` (coming soon)

> Lists the buyer questions of the account, paginated and filtered.

Returns the questions asked on the listings of the account sellers, newest first. Filter by `connectionId`, `listingId` or `status` to build the queue of unanswered questions.

The question text comes only from this route and from [Get question](/docs/questions/get). The `question.received` webhook carries ids only.

---

# Get question

URL: https://marketapi.shop/docs/questions/get
Endpoint: `GET /v1/questions/{questionId}` (coming soon)

> Returns one question with its text and answer.

Returns the question with its text, its `status` (`unanswered`, `answered`, `under_review` or `deleted`) and the `answer`, when there is one. The answer has its own status (`sending`, `published`, `under_review`, `rejected` or `failed`) and, if it failed, the reason in `statusReason`.

The author appears only as an external id (`author.externalId`). A question from another account returns `404`.

---

# Answer question

URL: https://marketapi.shop/docs/questions/answer
Endpoint: `POST /v1/questions/{questionId}/answers` (coming soon)

> Publishes the seller answer to a question.

Send the answer `text`. The API returns `202` with a [job](/docs/jobs/overview). When it finishes, the question is `answered` and the answer is `published`. If the marketplace refuses it or the job fails, the answer becomes `rejected` or `failed`, with the reason.

Only an `unanswered` question can be answered: in any other state the response is `409` (`question_not_answerable`). Text above the marketplace limit is `400`, and no job is created. The limit is in [Capabilities](/docs/how-it-works/capabilities).

---

# Run a question action

URL: https://marketapi.shop/docs/questions/actions
Endpoint: `POST /v1/questions/{questionId}/actions` (coming soon)

> Deletes the question or blocks its author.

Takes `action`: `delete` deletes the question on the marketplace and `block_author` blocks the person who asked. The API returns `202` with a [job](/docs/jobs/overview). After `delete`, the question is `deleted`.

Each action depends on a Connection capability. Without it, the error is `422` `CAPABILITY_UNSUPPORTED`, with the capability in `details.capability`.

---

# Overview

URL: https://marketapi.shop/docs/conversations/overview

> What a conversation is, how to send messages and which limits apply.

A conversation is the **private** channel between buyer and seller, **after the sale**. It is born with the order: there is no `POST /v1/conversations`. To start contact, send a message in the order's conversation.

How customer service works end to end: [Serve buyers](/docs/how-it-works/customer-service). Open claims (disputes) are a separate resource.

## Endpoints

| Method | Path                                          | What it does                                       |
| ------ | --------------------------------------------- | -------------------------------------------------- |
| `GET`  | `/v1/conversations`                           | [List conversations](/docs/conversations/list)     |
| `GET`  | `/v1/conversations/{conversationId}`          | [Get conversation](/docs/conversations/get)        |
| `GET`  | `/v1/conversations/{conversationId}/messages` | [List messages](/docs/conversations/list-messages) |
| `POST` | `/v1/conversations/{conversationId}/messages` | [Send message](/docs/conversations/send-message)   |

All require `Authorization: Bearer mk_your_api_key`. See [Authentication](/docs/authentication).

## Conversation

* `channel`: `post_sale` or `support_ticket`.
* `status`: `open`, `blocked` or `closed`. When it is `blocked`, `blockedReason` says why.
* `orderId`: the order of the conversation.

## When you can send: `sendability`

Each conversation has `sendability`, computed before you send:

* `canSend`: `true` or `false`.
* `issues`: what blocks it, each with a `code` (`conversation_blocked`, `text_too_long`, `attachments_unsupported`, `too_many_attachments`, `contact_reason_required`, `contact_reason_exhausted`).
* `maxTextLength` and `maxAttachments`: the limits.
* `contactReasons`: the remaining quota (`remaining`) of each contact reason.

On `POST …/messages`, the same check runs again. If something does not fit, the response is `409` `message_not_sendable` with the `issues`, and no job is created. Without errors, it is `202` with a [job](/docs/jobs/overview), and the message goes from `sending` to `sent` or `failed`.

## Messages

`authorRole` is `buyer`, `seller` or `marketplace`. Moderation (`approved`, `pending`, `rejected`, `not_moderated`) comes in `moderation`. Attachments are private files: read them with [Get content](/docs/assets/content).

## Webhook

`message.received` arrives when the buyer or the marketplace writes. It carries ids only (`conversationId`, `messageId`, `orderId`…), never the text or the attachment. Read the message with [List messages](/docs/conversations/list-messages). See [Receiving webhooks](/docs/receiving-webhooks).

---

# List conversations

URL: https://marketapi.shop/docs/conversations/list
Endpoint: `GET /v1/conversations` (coming soon)

> Lists the account post-sale conversations, paginated and filtered.

Returns the conversations between buyers and sellers, filtered by `connectionId`, `orderId`, `channel` or `status`. Use it to find the order that has a new message.

Each conversation has a `sendability` field that says whether you can send now. See the [overview](/docs/conversations/overview).

---

# Get conversation

URL: https://marketapi.shop/docs/conversations/get
Endpoint: `GET /v1/conversations/{conversationId}` (coming soon)

> Returns one conversation with its state and what you can send.

Returns the conversation with `channel`, `status` (`open`, `blocked` or `closed`), the order (`orderId`) and `sendability`. Read `sendability` before you send: `canSend`, `issues`, the text and attachment limits and the remaining quota per contact reason.

A conversation from another account returns `404`.

---

# List messages

URL: https://marketapi.shop/docs/conversations/list-messages
Endpoint: `GET /v1/conversations/{conversationId}/messages` (coming soon)

> Lists the messages of a conversation, oldest first.

Returns the messages in conversation order (oldest first), paginated. Each message has `authorRole` (`buyer`, `seller` or `marketplace`), `text`, `status` and moderation.

An attachment comes as an `assetId`. The file is private: read it with [Get content](/docs/assets/content).

---

# Send message

URL: https://marketapi.shop/docs/conversations/send-message
Endpoint: `POST /v1/conversations/{conversationId}/messages` (coming soon)

> Sends a seller message to the buyer.

Send `text` and, optionally, `attachmentAssetIds` and `contactReason`. If there is no error, the API stores the message as `sending` and returns `202` with a [job](/docs/jobs/overview). When it finishes, the message is `sent` or `failed`.

Before it creates the job, the API checks the limits. If something does not fit (blocked conversation, long text, exhausted quota, missing contact reason), it returns `409` `message_not_sendable` with the `issues` list. An attachment needs storage on; without it, the error is `503`.

---

# Overview

URL: https://marketapi.shop/docs/reviews/overview

> What a review is, how to read the rating and when a reply is refused.

A review is the rating a buyer gives after the purchase. The seller can reply once, within a deadline.

How customer service works end to end: [Serve buyers](/docs/how-it-works/customer-service).

## Endpoints

| Method | Path                             | What it does                             |
| ------ | -------------------------------- | ---------------------------------------- |
| `GET`  | `/v1/reviews`                    | [List reviews](/docs/reviews/list)       |
| `GET`  | `/v1/reviews/{reviewId}`         | [Get review](/docs/reviews/get)          |
| `POST` | `/v1/reviews/{reviewId}/replies` | [Reply to a review](/docs/reviews/reply) |

All require `Authorization: Bearer mk_your_api_key`. See [Authentication](/docs/authentication).

## What is rated: `subject`

| `subject` | Meaning                              |
| --------- | ------------------------------------ |
| `product` | the product (links to a `listingId`) |
| `sale`    | the sale (links to an `orderId`)     |
| `seller`  | the seller as a whole                |

## The rating

Marketplaces use different scales. The API gives you two things:

* `ratingBps`: the rating in basis points, the same for everyone. 4 of 5 stars is `8000`; positive is `10000`, neutral `5000` and negative `0`.
* `originalRating`: the rating as the marketplace gave it, stars (`value` and `max`) or sentiment.

Filter by rating with `maxRatingBps` in [List reviews](/docs/reviews/list).

## Replying

[Reply to a review](/docs/reviews/reply) returns `202` with a [job](/docs/jobs/overview). The API refuses before the job when:

* the `subject` does not accept replies: `422` `CAPABILITY_UNSUPPORTED`;
* a reply already exists: `409` `review_already_replied`;
* the deadline (`replyDeadlineAt`) has passed: `409` `review_reply_window_closed`.

## Webhook

`review.received` arrives when a new review comes in. It carries ids and the rating only (`reviewId`, `subject`, `ratingBps`…), never the text. Read the text with [Get review](/docs/reviews/get). See [Receiving webhooks](/docs/receiving-webhooks).

---

# List reviews

URL: https://marketapi.shop/docs/reviews/list
Endpoint: `GET /v1/reviews` (coming soon)

> Lists the received reviews, paginated and filtered.

Returns the reviews of the account sellers. Filter by `connectionId`, `subject`, `orderId`, `listingId` or `maxRatingBps`. For example, `maxRatingBps=6000` returns only ratings up to 60%, the ones that call for a reply.

---

# Get review

URL: https://marketapi.shop/docs/reviews/get
Endpoint: `GET /v1/reviews/{reviewId}` (coming soon)

> Returns one review with its rating, text and reply.

Returns the review with `subject`, `ratingBps`, the original rating (`originalRating`), the text and the `reply`, when there is one. `replyDeadlineAt` shows until when you can reply.

A review from another account returns `404`.

---

# Reply to a review

URL: https://marketapi.shop/docs/reviews/reply
Endpoint: `POST /v1/reviews/{reviewId}/replies` (coming soon)

> Publishes the seller reply to a review.

Send the reply `text`. The API returns `202` with a [job](/docs/jobs/overview). When it finishes, the reply is `published`, `rejected` or `failed`, with the reason in `statusReason`.

The reply is refused before the job when the `subject` does not accept replies (`422` `CAPABILITY_UNSUPPORTED`), when the review already has a reply (`409` `review_already_replied`) or when the deadline has passed (`409` `review_reply_window_closed`).

---

# Overview

URL: https://marketapi.shop/docs/promotions/overview

> What a promotion is, where it comes from and how to join one.

A promotion is a discount with a deadline on a Connection. You create your own and accept the campaign invitations the marketplace sends. Each listing takes part through an **entry**, with its own promotional price.

Where this fits in the journey: [Grow and track](/docs/how-it-works/commerce).

## Endpoints

| Method   | Path                                               | What it does                                                          |
| -------- | -------------------------------------------------- | --------------------------------------------------------------------- |
| `GET`    | `/v1/promotions`                                   | [List promotions](/docs/promotions/list)                              |
| `POST`   | `/v1/promotions`                                   | [Create promotion](/docs/promotions/create)                           |
| `GET`    | `/v1/promotions/{promotionId}`                     | [Get promotion](/docs/promotions/get)                                 |
| `PATCH`  | `/v1/promotions/{promotionId}`                     | [Update promotion](/docs/promotions/update)                           |
| `DELETE` | `/v1/promotions/{promotionId}`                     | [Cancel promotion](/docs/promotions/delete)                           |
| `GET`    | `/v1/promotions/{promotionId}/listing-entries`     | [List entries](/docs/promotions/list-entries)                         |
| `PUT`    | `/v1/promotions/{promotionId}/listing-entries`     | [Replace entries](/docs/promotions/replace-entries)                   |
| `GET`    | `/v1/listings/{listingId}/promotions`              | [Listing promotions](/docs/promotions/list-for-listing)               |
| `GET`    | `/v1/listings/{listingId}/promotion-eligibilities` | [Listing eligibilities](/docs/promotions/list-eligibilities)          |
| `POST`   | `/v1/listings/{listingId}/promotions`              | [Create promotion for a listing](/docs/promotions/create-for-listing) |

All of them need `Authorization: Bearer mk_your_api_key`. See [Authentication](/docs/authentication).

## Origin and type

* `origin: seller`: created by you through the API.
* `origin: marketplace`: a campaign invitation. You only join; you cannot edit or cancel it.

`type` is `discount`, `campaign`, `coupon`, `bundle`, `flash_sale` or `volume`. A campaign starts only at the marketplace. What each Connection can create comes from its capabilities.

## Period and subsidy

A promotion has `startsAt`, `endsAt` and, on an invitation, `joinDeadlineAt`: the deadline to join. `sellerShareBps` is the share of the discount the seller pays, in bps (`5000` is 50%). `null` means the marketplace does not say.

## States

| `status`              | Meaning                                             |
| --------------------- | --------------------------------------------------- |
| `pending`             | created here, the marketplace has not confirmed yet |
| `scheduled`           | confirmed, starts in the future                     |
| `active` / `paused`   | running / paused                                    |
| `ended` / `cancelled` | finished / cancelled                                |
| `rejected`            | the marketplace refused it; see `statusReasons`     |
| `unknown`             | a state we do not know                              |

## Entries

Each entry links a listing to the promotion, with `promotionalPriceInCents` or `discountBps`, `stockLimit` and `purchaseLimit`. Its status is `candidate` (invited), `pending`, `active`, `paused`, `rejected`, `removed`, `ended` or `unknown`.

Before joining, check [eligibility](/docs/promotions/list-eligibilities): it shows the accepted price range (`priceRange`).

## Effects

Writes return `202` with a [job](/docs/jobs/overview). Reads come from our database, kept fresh by sync. You receive the `promotion.created`, `promotion.updated` and `promotion.entry_updated` webhooks. To renew your own promotion before it ends, see [Promotion renewals](/docs/promotion-renewals/overview).

---

# List promotions

URL: https://marketapi.shop/docs/promotions/list
Endpoint: `GET /v1/promotions` (coming soon)

> Lists the account's promotions, paginated and filtered.

Returns the promotions of every seller in the account: your own and the campaign invitations from marketplaces. Filter by `connectionId`, `status`, `type` or `origin`.

The route reads only our database. It does not call the marketplace. A single promotion is in [Get promotion](/docs/promotions/get).

---

# Create promotion

URL: https://marketapi.shop/docs/promotions/create
Endpoint: `POST /v1/promotions` (coming soon)

> Creates a promotion on a Connection.

Creates a `discount`, `coupon`, `flash_sale`, `bundle` or `volume` promotion on a Connection. You cannot create a `campaign`: it starts at the marketplace and arrives as an invitation. Send `startsAt`, `endsAt`, `terms` and, optionally, `entries` with the price of each listing.

Returns `202` with a `promotion.create` [job](/docs/jobs/overview). The promotion starts as `pending` and changes state when the marketplace answers; the `promotion.created` webhook tells you. A type the marketplace cannot create returns `422`. An inactive Connection returns `409`.

---

# Get promotion

URL: https://marketapi.shop/docs/promotions/get
Endpoint: `GET /v1/promotions/{promotionId}` (coming soon)

> Gets a promotion by id.

Returns the promotion with `type`, period (`startsAt`, `endsAt`, `joinDeadlineAt`), `status`, `statusReasons` and `sellerShareBps`, the share of the discount the seller pays.

Reads only our database. For the price of each listing, use [List entries](/docs/promotions/list-entries).

---

# Update promotion

URL: https://marketapi.shop/docs/promotions/update
Endpoint: `PATCH /v1/promotions/{promotionId}` (coming soon)

> Changes the name, period or rules of your own promotion.

Changes `name`, `startsAt`, `endsAt` or `terms`. Send only what changes.

Returns `202` with a `promotion.update` job. The promotion changes only when the marketplace accepts. A campaign invitation (`origin: marketplace`) returns `409`, because the marketplace controls it. An ended, cancelled or rejected promotion also returns `409`.

---

# Cancel promotion

URL: https://marketapi.shop/docs/promotions/delete
Endpoint: `DELETE /v1/promotions/{promotionId}` (coming soon)

> Ends one of your promotions.

Ends the promotion. If it is still `pending` and never reached the marketplace, it is deleted and the answer is `204`. Otherwise it returns `202` with a `promotion.end` job, and the final status is `cancelled`. The row stays as history.

The same `409` errors as [Update promotion](/docs/promotions/update) apply.

---

# List promotion entries

URL: https://marketapi.shop/docs/promotions/list-entries
Endpoint: `GET /v1/promotions/{promotionId}/listing-entries` (coming soon)

> Lists the listings that take part in the promotion.

Each entry is a listing in the promotion, with `promotionalPriceInCents`, `discountBps`, `stockLimit`, `purchaseLimit` and `status`. In an invitation, the `candidate` entry carries `priceRange`: the minimum, maximum and suggested price the marketplace accepts.

Filter by `status` or `listingId`. To join or leave, use [Replace entries](/docs/promotions/replace-entries).

---

# Replace promotion entries

URL: https://marketapi.shop/docs/promotions/replace-entries
Endpoint: `PUT /v1/promotions/{promotionId}/listing-entries` (coming soon)

> Replaces the set of listings in the promotion.

Sends the whole set of entries, up to 100. Each one carries `bindingId` and `promotionalPriceInCents` or `discountBps`, plus optional limits. Entries that join, change or leave are found by comparing with the current set.

Returns `202` with a `promotion.entries_update` job, with one item per entry. To join an invitation, put the `candidate` entry in the set. A `pending` promotion returns `409`. So does a closed one. A binding from another Connection returns `400`.

---

# List listing promotions

URL: https://marketapi.shop/docs/promotions/list-for-listing
Endpoint: `GET /v1/listings/{listingId}/promotions` (coming soon)

> Lists the promotions a listing takes part in.

Returns the listing entries in promotions, with the status of each one. Filter by entry `status`.

Use it on a listing screen to show which discounts are running. Reads only our database.

---

# List listing eligibilities

URL: https://marketapi.shop/docs/promotions/list-eligibilities
Endpoint: `GET /v1/listings/{listingId}/promotion-eligibilities` (coming soon)

> Lists the promotions a listing can still join.

Returns the open invitations for the listing: `scheduled` or `active` promotions whose join deadline (`joinDeadlineAt`) has not passed. Each item has the promotion and the `candidate` entry, with the accepted price range.

Check here before joining. To join, use [Replace entries](/docs/promotions/replace-entries).

---

# Create promotion for a listing

URL: https://marketapi.shop/docs/promotions/create-for-listing
Endpoint: `POST /v1/listings/{listingId}/promotions` (coming soon)

> Creates a direct discount on a listing.

A shortcut to create a `discount` promotion from a listing. Send `startsAt`, `endsAt` and `discountBps` (or `entries`), with `stockLimit` and `purchaseLimit` if needed. Without `entries`, one entry per binding of the listing is added.

Returns `202` with a `promotion.create` job, like [Create promotion](/docs/promotions/create).

---

# Overview

URL: https://marketapi.shop/docs/promotion-renewals/overview

> How renewal recreates a promotion before it ends.

A promotion has a deadline, and forgetting to redo it drops the promotional price. **Renewal** is our own rule: before the end, it creates a copy of the promotion. No marketplace renews on its own.

Where this fits in the journey: [Grow and track](/docs/how-it-works/commerce).

## Endpoints

| Method   | Path                                 | What it does                                      |
| -------- | ------------------------------------ | ------------------------------------------------- |
| `GET`    | `/v1/promotion-renewals`             | [List renewals](/docs/promotion-renewals/list)    |
| `POST`   | `/v1/promotion-renewals`             | [Create renewal](/docs/promotion-renewals/create) |
| `PATCH`  | `/v1/promotion-renewals/{renewalId}` | [Update renewal](/docs/promotion-renewals/update) |
| `DELETE` | `/v1/promotion-renewals/{renewalId}` | [Delete renewal](/docs/promotion-renewals/delete) |

All of them need `Authorization: Bearer mk_your_api_key`. See [Authentication](/docs/authentication).

## How it works

The rule has `duration` and `renewBefore`, both in days (`duration` 1 to 180, `renewBefore` 1 to 30). Renewal runs at `endsAt - renewBefore`, the `nextRunAt`.

When it runs, a `promotion.renew` [job](/docs/jobs/overview) creates the next promotion with the same `terms` and the active entries. It starts when the previous one ends and lasts `duration`. The rule then follows the new promotion (`currentPromotionId`).

## Rules

* Only your own promotion (`origin: seller`), with `endsAt`, of type `discount`, `coupon`, `bundle` or `volume`. Otherwise: `409` (`promotion_not_renewable`).
* One rule per promotion chain. Otherwise: `409` (`renewal_exists`).
* If the marketplace refuses, the error goes to `lastError`, the rule stays on and tries again at the next sweep.
* Inactive Connection: the rule stays paused.

## Webhooks

`promotion.renewed` arrives on every renewal. The new promotion also fires `promotion.created`. See [Promotions](/docs/promotions/overview).

---

# List renewals

URL: https://marketapi.shop/docs/promotion-renewals/list
Endpoint: `GET /v1/promotion-renewals` (coming soon)

> Lists the promotion renewal rules.

Returns the account renewal rules. Filter by `connectionId`, `promotionId` or `enabled`. Each rule shows `nextRunAt`, `lastRenewedAt` and `lastError`.

---

# Create renewal

URL: https://marketapi.shop/docs/promotion-renewals/create
Endpoint: `POST /v1/promotion-renewals` (coming soon)

> Turns on renewal for a promotion.

Send `promotionId`, `duration` and `renewBefore`, both in days. The rule recreates the promotion before it ends, with the requested duration.

Only your own promotions (`origin: seller`) with `endsAt`, of type `discount`, `coupon`, `bundle` or `volume`. An invitation, a promotion with no end or of another type returns `409` (`promotion_not_renewable`). A rule already exists on the same chain: `409` (`renewal_exists`). Returns `201`.

---

# Update renewal

URL: https://marketapi.shop/docs/promotion-renewals/update
Endpoint: `PATCH /v1/promotion-renewals/{renewalId}` (coming soon)

> Turns a renewal rule on or off, or changes it.

Changes `enabled`, `duration` or `renewBefore`. Send only what changes. `nextRunAt` is recalculated. If the date has already passed, the renewal runs at the next sweep.

---

# Delete renewal

URL: https://marketapi.shop/docs/promotion-renewals/delete
Endpoint: `DELETE /v1/promotion-renewals/{renewalId}` (coming soon)

> Deletes a renewal rule.

Deletes the rule. Promotions it already created stay. Returns `204`.

---

# Overview

URL: https://marketapi.shop/docs/pricing/overview

> Fee estimate and competitive price of a listing.

Before changing a price, the seller wants to know what the marketplace charges and which price wins the dispute. Two routes answer this per listing.

Where this fits in the journey: [Grow and track](/docs/how-it-works/commerce).

## Endpoints

| Method | Path                                          | What it does                                               |
| ------ | --------------------------------------------- | ---------------------------------------------------------- |
| `POST` | `/v1/listings/{listingId}/fee-estimates`      | [Estimate fees](/docs/pricing/fee-estimates)               |
| `GET`  | `/v1/listings/{listingId}/competitive-prices` | [Get competitive prices](/docs/pricing/competitive-prices) |

All of them need `Authorization: Bearer mk_your_api_key`. See [Authentication](/docs/authentication).

## Fee estimate

You send a price (or use the current one) and get the fees in cents: `commission`, `fixed_fee`, `shipping` and others. You also get the net amount: `netAmountInCents = priceInCents × quantity - totalFeesInCents`. It is a [job](/docs/jobs/overview), and the result is in the item. It works before publishing.

## Competitive price

Shows, per binding, the `status` in the dispute (`winning`, `competing`, `not_competing`…), the `priceToWinInCents` and the `suggestedPriceInCents`. Reads our database, kept fresh by sync. A marketplace that does not report a field returns `null`.

A marketplace without these operations returns `422`. See the capabilities of the [Connection](/docs/connections/overview).

---

# Estimate fees

URL: https://marketapi.shop/docs/pricing/fee-estimates
Endpoint: `POST /v1/listings/{listingId}/fee-estimates` (coming soon)

> Estimates the marketplace fees for a listing.

Calculates what the marketplace charges on a sale, before you set the price. With no body, one scenario per binding is used, with the current listing price. Or send up to 10 `scenarios`, each with `priceInCents`, `quantity` and, optionally, `bindingId`.

Returns `202` with a `listing.fee_estimate` [job](/docs/jobs/overview), with one item per scenario. The item `result` has `fees`, `totalFeesInCents` and `netAmountInCents`. A listing with no binding and no scenarios returns `409`.

---

# Get competitive prices

URL: https://marketapi.shop/docs/pricing/competitive-prices
Endpoint: `GET /v1/listings/{listingId}/competitive-prices` (coming soon)

> Shows the listing price position against competitors.

Returns, per binding, the position of the listing: `status`, `currentPriceInCents`, `priceToWinInCents`, `suggestedPriceInCents`, `lowestPriceInCents` and `competitorCount`. A field the marketplace does not report is `null`.

Reads only our database, kept fresh by sync. If it was never synced, you get `observedAt: null` and an empty `entries`.

---

# Overview

URL: https://marketapi.shop/docs/metrics/overview

> Visits, units, revenue and conversion for products and listings, as a series over a period.

Metrics show how each product and each Listing perform: visits, units sold, revenue and conversion. You read a series over a period, without calling the marketplace. The API only reads data that is already synced.

Where metrics fit in the journey: [Grow and track](/docs/how-it-works/commerce).

## Endpoints

| Method | Path                               | What it does                                     |
| ------ | ---------------------------------- | ------------------------------------------------ |
| `GET`  | `/v1/products/{productId}/metrics` | [Product metrics](/docs/metrics/product-metrics) |
| `GET`  | `/v1/listings/{listingId}/metrics` | [Listing metrics](/docs/metrics/listing-metrics) |
| `GET`  | `/v1/listings/{listingId}/visits`  | [Listing visits](/docs/metrics/listing-visits)   |

All require `Authorization: Bearer mk_your_api_key`. See [Authentication](/docs/authentication).

## The series

Every response has `period` (`from` and `to`, inclusive), `granularity` (`day`, `week` or `month`) and `timezone`. The week is ISO and starts on Monday. The period is at most 366 days.

Every bucket in the period appears, even with no data. `date` is the start of the bucket in `timezone`.

## Values

| Field            | What it is                        |
| ---------------- | --------------------------------- |
| `visits`         | visits to the listing             |
| `units`          | units sold                        |
| `orders`         | orders                            |
| `revenueInCents` | product revenue, in cents         |
| `conversionBps`  | `orders × 10000 ÷ visits`, in bps |

Revenue is the value of the product sold: no shipping, before fees and taxes. The net amount is in [Finance](/docs/finance/overview).

`conversionBps` is recalculated in each bucket from the sums. It is never an average of ratios.

## `null` is not zero

`null` means the marketplace does not report the value. `0` means it reported and it was zero. `conversionBps` is `null` when `visits` is `0` or `null`.

## Where sales come from

When the marketplace has no sales series, `units`, `orders` and `revenueInCents` come from the imported orders. The `salesSource` field tells which source was used:

| `salesSource` | Means                                           |
| ------------- | ----------------------------------------------- |
| `marketplace` | the marketplace's own sales series              |
| `orders`      | calculated from [orders](/docs/orders/overview) |
| `mixed`       | product with listings from different sources    |
| `null`        | no sales source available                       |

Pass `salesSource=auto` (default), `marketplace` or `orders` in the query. One source per Connection: the same sale never counts twice. Canceled or refunded orders do not count.

## Errors

* `400`: `from` after `to`, or a period over 366 days.
* `404`: resource from another account.
* `409`: the product series mixes listings with different time zones (`mixed_timezones`) or currencies (`mixed_currencies`).
* `422`: the Connection does not offer the required capability.

---

# Product metrics

URL: https://marketapi.shop/docs/metrics/product-metrics
Endpoint: `GET /v1/products/{productId}/metrics` (coming soon)

> Visits, sales and conversion series for a product.

Sums the metrics of the Listings linked to the product, over the period and granularity you ask for. Use it to see how the Acme Store iPhone 16 performs across all marketplaces at once.

The response has `listingIds` (the Listings summed) and `excludedListingIds`. A Listing is left out when its data comes only for the whole ad and the ad has variants of other products. Listings with different time zones or currencies return `409`. Series rules: [Overview](/docs/metrics/overview). One Listing only: [Listing metrics](/docs/metrics/listing-metrics).

---

# Listing metrics

URL: https://marketapi.shop/docs/metrics/listing-metrics
Endpoint: `GET /v1/listings/{listingId}/metrics` (coming soon)

> Visits, sales and conversion series for a Listing.

Returns the series of one Listing: `visits`, `units`, `orders`, `revenueInCents` and `conversionBps`, with `totals` and one point per bucket. The route only reads the database, never the marketplace.

What the marketplace does not report comes as `null`. `salesSource` tells where sales came from. It returns `422` only when the Connection has neither a sales series nor visits. Series rules: [Overview](/docs/metrics/overview). Visits only: [Listing visits](/docs/metrics/listing-visits).

---

# Listing visits

URL: https://marketapi.shop/docs/metrics/listing-visits
Endpoint: `GET /v1/listings/{listingId}/visits` (coming soon)

> Visits series for a Listing.

Returns only the visits of a Listing, with `totals.visits` and one point per bucket. Use it when the marketplace gives visits but no sales series.

It needs the `visits.read` capability: without it, the response is `422`. Period and granularity rules are in the [Overview](/docs/metrics/overview). Visits with sales and conversion: [Listing metrics](/docs/metrics/listing-metrics).

---

# Overview

URL: https://marketapi.shop/docs/ad-campaigns/overview

> Paid ad campaigns, metrics and actions.

Paid ads are where the seller spends to sell. A campaign (`AdCampaign`) comes from the marketplace and is synced to the API. You list campaigns, read metrics over a period, and can pause, activate or change the budget.

Where Ads fit in the journey: [Grow and track](/docs/how-it-works/commerce).

## Endpoints

| Method | Path                                    | What it does                                    |
| ------ | --------------------------------------- | ----------------------------------------------- |
| `GET`  | `/v1/ad-campaigns`                      | [List campaigns](/docs/ad-campaigns/list)       |
| `GET`  | `/v1/ad-campaigns/{campaignId}`         | [Get campaign](/docs/ad-campaigns/get)          |
| `GET`  | `/v1/ad-campaigns/{campaignId}/metrics` | [Campaign metrics](/docs/ad-campaigns/metrics)  |
| `POST` | `/v1/ad-campaigns/{campaignId}/actions` | [Act on a campaign](/docs/ad-campaigns/actions) |
| `POST` | `/v1/connections/{id}/ad-metric-syncs`  | [Sync metrics](/docs/ad-campaigns/metric-syncs) |

All require `Authorization: Bearer mk_your_api_key`. See [Authentication](/docs/authentication).

## Campaign

| Field    | Values                                                                   |
| -------- | ------------------------------------------------------------------------ |
| `status` | `active`, `paused`, `ended`, `draft`, `unknown`                          |
| `adType` | `product`, `brand`, `display`, `other`                                   |
| `budget` | `{ amountInCents, period }`, with `period` `daily` or `total`, or `null` |

`nativeType` and `externalStatus` keep the marketplace's original value.

## Metrics

The series has `period`, `granularity` (`day`, `week` or `month`) and `timezone`, as in [Metrics](/docs/metrics/overview). The values:

* `impressions`, `clicks`, `orders`, `units`
* `spendInCents` and `revenueInCents`
* `ctrBps = clicks × 10000 ÷ impressions`
* `roasBps = revenueInCents × 10000 ÷ spendInCents`: ROAS 5.0 is `50000`

The API stores only the counts. CTR and ROAS are calculated on read, in each bucket, because ratios do not add up. A zero or null denominator gives `null`.

## Revised data

The marketplace revises numbers for up to 3 days. The latest days can change. The daily sync rewrites that window. Each marketplace keeps history for a limited time: asking for days beyond it returns `400`.

## Actions

Pause, activate and change budget run as a [job](/docs/jobs/overview) (`ad_campaign.action`), with `202`. An `ended` campaign returns `409`. Not every marketplace allows actions: when it is read-only, the response is `422`.

## Errors

* `404`: campaign from another account.
* `422`: the Connection does not offer the capability (`ads.read` or `ads.manage`).

---

# List campaigns

URL: https://marketapi.shop/docs/ad-campaigns/list
Endpoint: `GET /v1/ad-campaigns` (coming soon)

> Lists the account ad campaigns.

Returns the campaigns of every Connection of the account, paginated. Filter by `connectionId`, `status` or `adType`. With `connectionId`, the API checks that the Connection offers Ads (`422` if not).

One campaign: [Get campaign](/docs/ad-campaigns/get). Results: [Campaign metrics](/docs/ad-campaigns/metrics).

---

# Get campaign

URL: https://marketapi.shop/docs/ad-campaigns/get
Endpoint: `GET /v1/ad-campaigns/{campaignId}` (coming soon)

> Returns one ad campaign.

Returns the campaign with `status`, `budget`, targets (`targetRoasBps`, `targetAcosBps`, null when the marketplace does not report them), dates and `lastSyncedAt`. A campaign from another account returns `404`.

To change the campaign: [Act on a campaign](/docs/ad-campaigns/actions).

---

# Campaign metrics

URL: https://marketapi.shop/docs/ad-campaigns/metrics
Endpoint: `GET /v1/ad-campaigns/{campaignId}/metrics` (coming soon)

> Performance series for a campaign.

Returns the campaign series: impressions, clicks, `spendInCents`, `revenueInCents`, orders, units, `ctrBps` and `roasBps`. It accepts `from`, `to` and `granularity`, like [product metrics](/docs/metrics/overview).

It reads the database only. The latest days can change for up to 3 days. To force a refresh: [Sync metrics](/docs/ad-campaigns/metric-syncs).

---

# Act on a campaign

URL: https://marketapi.shop/docs/ad-campaigns/actions
Endpoint: `POST /v1/ad-campaigns/{campaignId}/actions` (coming soon)

> Pauses, activates or changes the budget of a campaign.

Runs an action on the marketplace: `pause`, `activate` or `update_budget` (with `budget.amountInCents` and `period`). The response is `202` with an `ad_campaign.action` [job](/docs/jobs/overview); follow the result through it.

An `ended` campaign returns `409`. A budget below the marketplace minimum makes the job item fail. Send `Idempotency-Key` to retry without duplicating.

---

# Sync metrics

URL: https://marketapi.shop/docs/ad-campaigns/metric-syncs
Endpoint: `POST /v1/connections/{id}/ad-metric-syncs` (coming soon)

> Requests a refresh of a Connection's campaigns and metrics.

Fetches the campaigns and metrics of the period from the marketplace. Without `from` and `to`, it uses the last 7 closed days. The window is up to 90 days and cannot go past the history the marketplace keeps (`400`).

The response is `202` with an `ad_campaign.sync` [job](/docs/jobs/overview). An inactive Connection returns `409`. The API also syncs on its own every day.

---

# Overview

URL: https://marketapi.shop/docs/finance/overview

> Transactions, settlements, fee statements and balances of the sellers, for reconciliation.

Finance shows the seller's money in each marketplace: what came in, the fees charged, what was paid out and the balance. Use it for **reconciliation**. Do not use it as an operational source: an order's price and fees come from the [order](/docs/orders/overview).

Where finance fits in the journey: [Grow and track](/docs/how-it-works/commerce).

## Endpoints

| Method | Path                         | What it does                                        |
| ------ | ---------------------------- | --------------------------------------------------- |
| `GET`  | `/v1/financial-transactions` | [List transactions](/docs/finance/transactions)     |
| `GET`  | `/v1/settlements`            | [List settlements](/docs/finance/settlements)       |
| `GET`  | `/v1/fee-statements`         | [List fee statements](/docs/finance/fee-statements) |
| `GET`  | `/v1/financial-balances`     | [List balances](/docs/finance/balances)             |

All require `Authorization: Bearer mk_your_api_key`. See [Authentication](/docs/authentication).

## Transaction

Each transaction has a normalized `type`: `sale`, `refund`, `fee`, `shipping`, `adjustment`, `chargeback`, `payout`, `advertising`, `tax`, `promotion` or `other`. The marketplace's original type is always kept in `externalType`. On `fee` transactions, `feeType` tells which fee.

`status` is `pending`, `available`, `settled` or `cancelled`. When possible, the transaction points to the order (`orderId`) and to the settlement (`settlementId`).

## The sign of the amount

`amountInCents` is signed: credit is positive, debit is negative. A sale of R$ 100.00 is `10000`. Its commission is, for example, `-1200`.

## Lag

Data is read from the marketplace once a day. What happened today may show up only tomorrow. A transaction can arrive before its order: the `orderId` link is retried on every round.

## Settlements, statements and balances

* **Settlement** (`Settlement`): status `scheduled`, `in_transit`, `paid`, `failed` or `cancelled`.
* **Fee statement** (`FeeStatement`): status `open`, `closed`, `paid`, `overdue` or `unknown`, with a `breakdown` per fee and `documents` (invoices and credit notes).
* **Balance** (`FinancialBalance`): `availableInCents`, `pendingInCents` and `blockedInCents`, per currency.

## Rules

* Transactions come newest first, by `occurredAt`.
* Filter by `connectionId`. Without it, the list covers every Connection of the account.
* An inverted period (`from` after `to`) returns `400`. A resource from another account returns `404`.
* Not every marketplace offers all four reads. When the Connection does not, the response is `422`.
* The transaction description carries no personal data.

---

# List transactions

URL: https://marketapi.shop/docs/finance/transactions
Endpoint: `GET /v1/financial-transactions` (coming soon)

> Lists the sellers' financial transactions.

Returns sales, refunds, fees, shipping and other transactions, paginated and newest first. Filter by `connectionId`, `type`, `status`, `orderId`, `settlementId`, `occurredFrom` and `occurredTo`.

`amountInCents` is signed: credit positive, debit negative. The marketplace original type is in `externalType`. To reconcile an order, filter by `orderId`. Rules: [Overview](/docs/finance/overview).

---

# List settlements

URL: https://marketapi.shop/docs/finance/settlements
Endpoint: `GET /v1/settlements` (coming soon)

> Lists the payouts made to sellers.

Returns payouts (`scheduled`, `in_transit`, `paid`, `failed`, `cancelled`) with amount, period, expected date (`expectedAt`), payment date (`paidAt`) and `transactionCount`. Filter by `connectionId`, `status`, `from` and `to`.

The transactions of a payout: [List transactions](/docs/finance/transactions) with `settlementId`.

---

# List fee statements

URL: https://marketapi.shop/docs/finance/fee-statements
Endpoint: `GET /v1/fee-statements` (coming soon)

> Lists the fee statements charged to sellers.

Returns fee statements by period, with `totalInCents`, `unpaidInCents`, due date (`dueAt`), a `breakdown` by fee type and `documents` (invoices and credit notes). Filter by `connectionId`, `status`, `from` and `to`.

Use it to check what the marketplace charged against the `fee` transactions.

---

# List balances

URL: https://marketapi.shop/docs/finance/balances
Endpoint: `GET /v1/financial-balances` (coming soon)

> Lists the sellers' balance in each marketplace.

Returns the balance per Connection and currency: `availableInCents`, `pendingInCents` and `blockedInCents`, with `observedAt`. Filter by `connectionId`.

The balance is the last read from the marketplace, up to one day old. Do not use it for real-time payment decisions.

---

# Overview

URL: https://marketapi.shop/docs/webhooks/overview

> Endpoints that receive the account events, and the rules that apply to all of them.

An **endpoint** is a URL of your system that receives our events: an account connected, disconnected, a job finished. Each event arrives as a signed `POST`. You can register as many endpoints as you want and pick the event types of each one.

Which event each change sends and how a delivery changes state, as diagrams: [States and events](/docs/data-model/states-and-events).

How to verify the signature and handle retries: [Receiving webhooks](/docs/receiving-webhooks).

## Endpoints

| Method   | Path                                        | What it does                                           |
| -------- | ------------------------------------------- | ------------------------------------------------------ |
| `GET`    | `/v1/webhooks/event-types`                  | [List event types](/docs/webhooks/list-event-types)    |
| `POST`   | `/v1/webhooks/endpoints`                    | [Register an endpoint](/docs/webhooks/create-endpoint) |
| `GET`    | `/v1/webhooks/endpoints`                    | [List endpoints](/docs/webhooks/list-endpoints)        |
| `GET`    | `/v1/webhooks/endpoints/{id}`               | [Get an endpoint](/docs/webhooks/get-endpoint)         |
| `PATCH`  | `/v1/webhooks/endpoints/{id}`               | [Update an endpoint](/docs/webhooks/update-endpoint)   |
| `DELETE` | `/v1/webhooks/endpoints/{id}`               | [Delete an endpoint](/docs/webhooks/delete-endpoint)   |
| `POST`   | `/v1/webhooks/endpoints/{id}/rotate-secret` | [Rotate secret](/docs/webhooks/rotate-secret)          |
| `GET`    | `/v1/webhooks/deliveries`                   | [List deliveries](/docs/webhooks/list-deliveries)      |
| `GET`    | `/v1/webhooks/deliveries/{id}`              | [Get a delivery](/docs/webhooks/get-delivery)          |
| `POST`   | `/v1/webhooks/deliveries/{id}/redeliver`    | [Redeliver](/docs/webhooks/redeliver)                  |

All of them require `Authorization: Bearer mk_your_api_key`. See [Authentication](/docs/authentication).

## The `secret`

Creation returns the `secret` (`whsec_...`) **only once**. You use it to verify the signature of every delivery, in the `X-Marketapi-Signature` header. It never comes back in another response: if you lose it or it leaks, [rotate it](/docs/webhooks/rotate-secret): the new one shows up once, and the old one stops working at once.

## Events

<WebhookEventsTable />

`events: ["*"]` receives all of them, including future ones. The `*` goes alone: mixing it with specific types gives `400`.

## Deliveries

Each event becomes one **delivery** per subscribed endpoint. If your endpoint does not answer `2xx` within 10 seconds, we try again after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h; after the 8th attempt the delivery is `failed`. The log keeps the outcome of the last attempt of each delivery.

The same event can arrive more than once (a late answer, a redelivery). Deduplicate on the `id` of the body; `X-Marketapi-Delivery-Id` changes on every delivery.

## A paused endpoint

`enabled: false` pauses the endpoint: new events skip it, and its pending deliveries fail without a call. `enabled: true` turns it back on; events from the paused time are not resent.

---

# List event types

URL: https://marketapi.shop/docs/webhooks/list-event-types
Endpoint: `GET /v1/webhooks/event-types`

> Every event an endpoint can subscribe to.

Returns every event type, sorted by `type`, with its domain and a short English description. Use it to build your own event picker or to check what exists.

The list is static: it changes only when we add an event, so it has no pagination. The `type` follows `domain.action` and is the value that goes in `events` when you [register](/docs/webhooks/create-endpoint) or [update](/docs/webhooks/update-endpoint) an endpoint.

---

# Register an endpoint

URL: https://marketapi.shop/docs/webhooks/create-endpoint
Endpoint: `POST /v1/webhooks/endpoints`

> Registers a URL of your system to receive events.

Registers a URL that receives the account events. The response carries the `secret` **only once**: keep it, you use it to verify the signature.

* `events`: event types, with no repeats, or `["*"]` for all of them.
* In production the URL must be `https` and public. An internal address (`localhost`, private IP, `*.internal`) gives `400` with `details.field = "url"`. Outside production, `http` works too.

The endpoint receives only the events that happen after it is created.

---

# List endpoints

URL: https://marketapi.shop/docs/webhooks/list-endpoints
Endpoint: `GET /v1/webhooks/endpoints`

> Lists the webhook endpoints of the account.

Returns the endpoints of your account, from the newest to the oldest, without the `secret`. Pagination works as in [List sellers](/docs/sellers/list).

---

# Get an endpoint

URL: https://marketapi.shop/docs/webhooks/get-endpoint
Endpoint: `GET /v1/webhooks/endpoints/{id}`

> Reads a webhook endpoint by id.

Returns an endpoint of your account by its `id`. The `secret` does not come back: it only shows up at creation.

---

# Update an endpoint

URL: https://marketapi.shop/docs/webhooks/update-endpoint
Endpoint: `PATCH /v1/webhooks/endpoints/{id}`

> Changes the URL, the events or the description, or pauses the endpoint.

Changes only the fields you send, and at least one must be there: an empty body `{}` gives `400`. Use `enabled: false` to pause the endpoint without losing it, and `enabled: true` to turn it back on. The `secret` stays the same.

A new URL follows the same rule as creation: in production, only public `https`.

---

# Delete an endpoint

URL: https://marketapi.shop/docs/webhooks/delete-endpoint
Endpoint: `DELETE /v1/webhooks/endpoints/{id}`

> Deletes an endpoint and its delivery log.

Deletes the endpoint. The response is `204`, with no body.

<Callout type="warn">
  The endpoint's delivery log goes with it, and it cannot be undone. To stop
  receiving without losing the log, pause it with `enabled: false`.
</Callout>

---

# Rotate secret

URL: https://marketapi.shop/docs/webhooks/rotate-secret
Endpoint: `POST /v1/webhooks/endpoints/{id}/rotate-secret`

> Replaces the endpoint's signing secret.

Generates a new `secret` for the endpoint and returns it in the response, **only once**. The old secret stops working at once: the next delivery, including pending retries, is signed with the new one.

Use it when the secret leaks or gets lost. Update your system right after: until then, signatures will not match. A delivery already on its way at the moment of the swap may arrive with the old signature.

The dashboard has the same button, **Rotate secret**, on the endpoint page.

---

# List deliveries

URL: https://marketapi.shop/docs/webhooks/list-deliveries
Endpoint: `GET /v1/webhooks/deliveries`

> The webhook delivery log, newest first.

Returns the delivery log of your account. Filter by endpoint, by event or by `status` — for example, the ones that failed:

```bash
curl "$BACKEND_URL/v1/webhooks/deliveries?endpointId=<id>&status=failed&page=1&pageSize=20" \
  -H "Authorization: Bearer mk_your_api_key"
```

The list is newest first, so a new delivery pushes older ones to the next page. When walking pages, the same delivery can show up twice: deduplicate on `id`.

---

# Get a delivery

URL: https://marketapi.shop/docs/webhooks/get-delivery
Endpoint: `GET /v1/webhooks/deliveries/{id}`

> Reads a delivery by id.

Returns a delivery by its `id`, which is the `X-Marketapi-Delivery-Id` header your endpoint received. It shows how many attempts there were (`attempt`), what your endpoint answered on the last one (`responseCode`, `responseMs`, `error`) and when the next one is (`nextRetryAt`).

---

# Redeliver

URL: https://marketapi.shop/docs/webhooks/redeliver
Endpoint: `POST /v1/webhooks/deliveries/{id}/redeliver`

> Sends the event of a delivery again.

Creates a new delivery of the same event to the same endpoint, which goes out within a second. The response is `202`, with the new delivery. The original delivery does not change.

Use it after fixing your endpoint, to recover events that failed. The event `id` repeats, so your code deduplicates with no extra work. The dashboard has the same **Redeliver** button in the delivery log.

---

# Overview

URL: https://marketapi.shop/docs/jobs/overview

> What a job is and the rules that apply to all of its endpoints.

Everything that depends on the marketplace runs in the background, as a **job**. The API answers right away and the work goes on afterwards. You track the job by its `id`.

The states of a Job and who creates each type, as diagrams: [States and events](/docs/data-model/states-and-events).

There are six types; the table of who creates each one is in [States and events](/docs/data-model/states-and-events#job). One of them is `connection.sync`, started for each connected account. Its `id` arrives in the `connection.created` event (`data.syncJobId`), and in the `connection.updated` of a reconnection.

How to follow a job to the end: [Tracking jobs](/docs/tracking-jobs).

## Endpoints

| Method | Path                  | What it does                   |
| ------ | --------------------- | ------------------------------ |
| `GET`  | `/v1/jobs`            | [List jobs](/docs/jobs/list)   |
| `GET`  | `/v1/jobs/{id}`       | [Get a job](/docs/jobs/get)    |
| `GET`  | `/v1/jobs/{id}/items` | [List items](/docs/jobs/items) |

All of them require `Authorization: Bearer mk_your_api_key`. See [Authentication](/docs/authentication).

## States

| `status`    | Meaning                                     |
| ----------- | ------------------------------------------- |
| `queued`    | waiting in the queue                        |
| `running`   | in progress; `processed` moves item by item |
| `completed` | every item succeeded                        |
| `partial`   | some succeeded; see the items with `failed` |
| `failed`    | no item succeeded; `error` sums it up       |

## Items

Each job has items, one per resource, each with its own `status`, `result` and `error`. Items keep the job order (`index`), not newest first like the other lists.

## Internal jobs

Our own maintenance (for example, token refresh) also runs as jobs, but they are not in the API: an internal job returns `404`, the same as a job from another account.

---

# List jobs

URL: https://marketapi.shop/docs/jobs/list
Endpoint: `GET /v1/jobs`

> Lists the account jobs, one page at a time.

Returns the jobs of your account, from the newest to the oldest. Filter by `status` and by `type`.

```bash
curl "$BACKEND_URL/v1/jobs?status=running&type=connection.sync&page=1&pageSize=20" \
  -H "Authorization: Bearer mk_your_api_key"
```

A new job pushes the older ones to the next page. When walking pages, deduplicate on `id`. To wait for one job, prefer [Get a job](/docs/jobs/get) or the job events (`job.completed`, `job.partially_completed`, `job.failed`).

---

# Get a job

URL: https://marketapi.shop/docs/jobs/get
Endpoint: `GET /v1/jobs/{id}`

> Reads a job by id.

Returns a job of your account by its `id`, with its counters (`total`, `processed`, `succeeded`, `failed`).

To know when it finishes, receive the `job.completed`, `job.partially_completed` and `job.failed` events on your webhook instead of polling in a loop. If you poll, wait a few seconds between calls.

---

# List items

URL: https://marketapi.shop/docs/jobs/items
Endpoint: `GET /v1/jobs/{id}/items`

> Lists the items of a job, in the job order.

Returns the items of a job in its order (`index` ascending), with the outcome and the error of each one.

```bash
curl "$BACKEND_URL/v1/jobs/<id>/items?status=failed&page=1&pageSize=100" \
  -H "Authorization: Bearer mk_your_api_key"
```

Items are fixed when the job is created, so unfiltered pages stay stable while the job runs. With `status`, an item moves to another page when it finishes (`pending` → `succeeded`): filter after the job is done.