MarketplaceHub Docs Docs

Inventory

Push price and stock by SKU with per-SKU results, read your catalogue, and create products: search categories, read attribute schemas, publish with validation.

Reading the catalogue and searching a marketplace's taxonomy needs InventoryRead; pushing price and stock needs PricingWrite, and creating products needs CatalogWrite. The older InventoryWrite permission does both, so a credential that has it keeps working.

The price and quantity endpoint applies changes before it answers, so the response reports the outcome for every SKU you sent — updated, unchanged, not found, ambiguous or failed — and hands back the job ids for the marketplace pushes it triggered Your request Up to 500 items, keyed by SKU WIDGET-1 19.99 qty 42 WIDGET-2 qty 0 NOPE 12.00 qty 5 applied One result per SKU Nothing is ever silently dropped Updated — a value differed and was written Unchanged — already equal, not an error NotFound · Ambiguous · Failed JobIds The marketplace pushes this triggered. Your values are already saved; the jobs are outbound. Omitting a field means “not provided” It never means “set to zero”. Send Price or Quantity only when you are actually changing it. A repeated SKU collapses The last occurrence in the request wins, and it is resolved before anything is written. A nightly full-catalogue push that reports mostly Unchanged is working correctly — it means nothing moved, not that nothing happened.
Every SKU you send comes back with its own outcome.

POST /api/inventory/price-quantity

Applies price and stock from your system, keyed by SKU. This is the endpoint for an ERP or warehouse system acting as the source of truth.

{
  "Items": [
    { "Sku": "WIDGET-1", "Price": 19.99, "Quantity": 42 },
    { "Sku": "WIDGET-2", "Quantity": 0 }
  ],
  "PropagateToListings": true
}
  • Up to 500 items per request.
  • Omitting Price or Quantity means "not provided", never "set to zero". Send the field only when you are changing it.
  • A SKU repeated in one request collapses to its last occurrence before anything is written.

The changes are applied before the response returns, so you are told what happened to each SKU:

{
  "Results": [
    { "Sku": "WIDGET-1", "Result": "Updated" },
    { "Sku": "WIDGET-2", "Result": "Unchanged" },
    { "Sku": "NOPE",     "Result": "NotFound" }
  ],
  "Updated": 1, "Unchanged": 1, "NotFound": 1, "Ambiguous": 0, "Failed": 0,
  "JobIds": ["6a73..."]
}
ResultMeans
UpdatedMatched, and at least one value differed and was written.
UnchangedMatched, but everything you sent already equalled what we hold. Not an error — this is what a nightly full-catalogue push looks like.
NotFoundNo product carries that SKU. Reported, never silently dropped.
AmbiguousMore than one product carries that SKU and none matched its exact casing.
FailedMatched, but the write failed. The rest of the batch still applied.

JobIds are the marketplace pushes this triggered. The values are already saved by the time you get the response — the jobs are the outbound half. Follow them with the jobs endpoint.

Set PropagateToListings false to change the stored values without scheduling any outbound write — useful for back-filling data the marketplaces already hold.

Send Currency (for example "EUR") when your prices are in one currency and your marketplaces sell in others. Each listing then gets the price in its own currency, at the day's exchange rate and rounded to that currency's decimals; the response's Listings show each converted PriceAfter with its PriceCurrency. A listing whose currency has no rate that day keeps its price (its stock still changes), and a warning says how many did. Leave Currency out to send the same number to every listing.

POST /api/products/List

Your catalogue, with the marketplace listings each SKU has.

{ "Sku": "WIDGET-1", "Limit": 50, "Offset": 0 }

All optional. Limit defaults to 50, maximum 200. Page with Offset against the returned TotalCount.

GET /api/products/{sku}

One product by SKU — the identifier your own system already holds, and the same key the price and quantity sync matches on. Matching is case-insensitive.

{
  "Product": {
    "Sku": "WIDGET-1",
    "Quantity": 42,
    "Listings": [
      {
        "Platform": "Amazon",
        "Country": "UnitedKingdom",
        "ListingId": "B0TEST12345",
        "ExternalSku": "AMZ-001",
        "ListingUrl": "https://www.amazon.co.uk/dp/B0TEST12345"
      }
    ]
  }
}

Listings is the durable map of where a SKU already exists. It is what makes an update revise the listing that is there rather than create a second one, and it is what to reconcile your own records against. A SKU listed nowhere yet returns an empty array — that is normal, not an error.

If several products differ only in the case of their SKU and none matches yours exactly, you get 409 conflict rather than a guess.

BusinessOffers lists the Amazon Business offers of the SKU and of each variation, one per Amazon store (MarketplaceId): the business price, the quantity discounts, what Amazon said when it checked the offer (AmazonCheck; AmazonCheckIsCurrent is false once the offer changed), what Amazon last accepted (LiveBusinessPrice, LastAnswer), and whether a send or a removal is waiting. It is read-only: a business price is set on the product or in an import file. POST /api/products/List leaves it null.

Creating a product and listing it

A marketplace does not accept "a product" — it accepts a product in a category, with the attributes that category demands, and both differ per marketplace and country. So creating goes through three endpoints:

  1. Find the category — POST /api/product-types/Search searches a connected marketplace's own taxonomy.
  2. Read its schema — GET /api/product-types/Schema returns the attribute contract for that category and the ProductTypeViewId the create needs.
  3. Create — POST /api/products/Create validates your payload against that same schema before anything is written, then queues the marketplace publish.

The schema endpoint and the create validation run through the same code, so what you read in step 2 is exactly what step 3 enforces — create can only demand what the schema advertised.

Searches a connected marketplace's categories for one to list a product under. Needs InventoryRead.

{
  "MarketplaceId": "6a73...",
  "SearchText": "running shoes",
  "Limit": 50,
  "Offset": 0
}
  • MarketplaceId is a value from GET /api/marketplaces. The taxonomy differs per marketplace and country, so there is no marketplace-independent answer to "the category for running shoes".
  • SearchText matches category names case-insensitively; a number is also matched against the marketplace's own category id. Omit it to walk the whole tree page by page.
  • Limit defaults to 50, maximum 200. Page with Offset against the returned TotalCount.
{
  "ProductTypes": [
    {
      "CategoryId": "112529",
      "Title": "Running Shoes",
      "Path": "Clothing, Shoes & Accessories > Shoes > Running Shoes"
    }
  ],
  "TotalCount": 3
}

Only leaf categories come back — the ones a listing can actually be placed in. CategoryId is the marketplace's own id (an eBay category id, an Amazon browse node), returned as a string because several marketplaces use ids past what a JSON number can carry exactly.

GET /api/product-types/Schema

The attributes a category's product type accepts, required ones flagged. Needs InventoryRead.

GET /api/product-types/Schema?marketplaceId=6a73...&categoryId=112529
{
  "ProductTypeViewId": "6a73...",
  "CategoryId": "112529",
  "CategoryPath": "Clothing, Shoes & Accessories > Shoes > Running Shoes",
  "ProductTypeCode": "SHOES",
  "SchemaVersion": "2026.07",
  "Attributes": [
    {
      "Code": "fabric_type",
      "Title": "Fabric Type",
      "Type": "Text",
      "IsRequired": true,
      "AllowedValues": [ { "Value": "polyester", "Title": "Polyester" } ],
      "AllowsCustomValues": false
    }
  ]
}
  • ProductTypeViewId is the value the create endpoint takes. It identifies the product-type schema at the currently active taxonomy version. Taxonomies are re-imported periodically and this id moves when they do — fetch it near the create, don't store it for months.
  • ProductTypeCode is the marketplace's own code behind this category — Amazon's product type (e.g. SHOES), or common on the marketplaces whose attributes hang off the category rather than a product type.
  • Per attribute: Code is the key to send in the create's Attributes; Type is the value shape (Text, Integer, Decimal, Date, Boolean) — descriptive, closed sets are expressed by AllowedValues, not here; for a closed-set attribute send an AllowedValues entry's Value and display its Title. When AllowsCustomValues is true the list is suggestions, not a hard enum.
  • AllowedValues is already narrowed to this category — a value the marketplace accepts elsewhere in the tree can still be rejected here.

The schema lists only what the API can carry. Required things it cannot carry — images, eBay business policies — are excluded here and come back as warnings on create instead.

GET /api/products/{sku}/facts

Reads what a product is, so an outside system can judge it. Needs InventoryRead. The SKU is matched without regard to case, as for GET /api/products/{sku}.

The top level carries the hub's own Price (null when the hub holds none, which is not the same as free) and Quantity. Then Listings has one entry per marketplace listing, and each reports its own content: Title, Description, Brand, Barcode and BarcodeType, the Category (the marketplace's own id and a path), Images (a link and alt text each), Attributes (code, title and value), the listing's Status and a Readiness score from 0 to 100 with the version of the rules that made it. A product has one set of values per marketplace and the hub does not pick one of them as the truth, so there is no merged title.

A product with variants also has Variants at the top level: each variant's Sku, Quantity and hub Price (null when the hub holds none). Each listing has its own Variants too: the variant's Sku, its Status on that marketplace, its Barcode, and Options, the name and value that tell it apart (for example Size: A4), in the marketplace's words or the source's when the marketplace has none. Both lists are ordered by SKU and empty for a product without variants. At most 250 are listed.

There is no buyer data and no cost price. SchemaVersion is 1: fields are only ever added to a version, and a field that changes meaning gets a new version.

POST /api/products/Facts

The same answer for a page of the catalogue, so a system can read every product's content without a call per SKU. Needs InventoryRead. Body fields, all optional: Limit (10 if left out, 20 at most; more is refused, not reduced), Offset, and ModifiedSince, which pages exactly as POST /api/products/List does: only products changed at or after it, oldest first, so the last product on a page is where the next call starts. A change to a listing's title, photos or attributes counts as a change.

The answer is Products, each the same shape as the single-product answer above, and TotalCount. AI assistants do not have this call: over MCP they page with products_list and read each product with products_facts.

POST /api/products/Readiness

Lists the listings that are least ready to sell, so a system can work down a list instead of reading every product. Needs InventoryRead. Body fields, all optional: MaxScore (0 to 100; only listings at or below it), Platform (a marketplace name such as Etsy), Limit (50 if left out, 200 at most) and Offset.

The answer is Listings, lowest score first, and TotalCount. Each listing has its Sku, Platform, Score, the Version of the rules that made it, when it was scored, and Fixes: what to fix, most valuable first. A fix has the Rule it belongs to, a stable Reason code, one plain sentence in Text, and PointsLost, which is what fixing it earns back. The same Fixes are on the Readiness part of the product facts answer.

Scores are stored and recomputed every night, so this is free to call, and a change you made today shows tomorrow. A listing that has not been scored yet is not in the list. Scores made by older rules are left out, because scores of different versions cannot be compared.

POST /api/products/Create

Adds a SKU to your catalogue and queues it for publishing to a marketplace. Needs CatalogWrite (or the older InventoryWrite).

{
  "Sku": "WIDGET-1",
  "Quantity": 42,
  "MarketplaceId": "6a73...",
  "ProductTypeViewId": "6a73...",
  "CategoryId": "112529",
  "Attributes": {
    "item_name": "Trail runner",
    "fabric_type": "polyester"
  }
}
  • Upserts on SKU. Sending the same SKU again updates the catalogue entry rather than creating a second one, and the response's ListingAction tells you whether the marketplace will get a new listing (Create) or a revision of the one it already has (Revise).
  • Omitting Quantity leaves stock untouched — it never means "set to zero". Send the field only when you are changing it.
  • ProductTypeViewId must belong to the marketplace in MarketplaceId — one from another marketplace's taxonomy is a per-field 400, not an id that fails hours later inside the publish.
  • CategoryId is required for eBay, Etsy, Shopify, TikTok Shop and BigCommerce and optional for Amazon, Walmart and Google Shopping — see the category rule.
  • Send an Idempotency-Key header to make a retry safe.
{
  "RequestId": "6a73...",
  "ProductId": "6a73...",
  "Sku": "WIDGET-1",
  "ListingAction": "Create",
  "ExistingListingId": null,
  "JobId": "6a73..."
}

The marketplace write is asynchronous. A 200 means the catalogue is updated and a publish is queued — not that the listing is live. Follow JobId with GET /api/jobs/{id}.

Schema validation

Sending CategoryId or Attributes engages validation against the category's schema — the same contract GET /api/product-types/Schema advertises. Everything runs before anything is written: a rejected payload leaves no catalogue entry, no listing row and no queued job. The alternative — accepting a payload the marketplace will refuse — surfaces hours later as a feed error in the app, which is exactly what this validation exists to prevent.

Failures come back as one 400 naming every bad field, keyed Attributes.<code>:

{
  "type": "urn:marketplacehub:error:validation-failed",
  "title": "The request is not valid",
  "status": 400,
  "code": "validation-failed",
  "requestId": "6a73...",
  "errors": {
    "Attributes.item_name": ["Required."],
    "Attributes.fabric_type": ["Not an accepted value for this attribute. See the schema's AllowedValues."]
  }
}

What is checked:

  • Every attribute code must exist in the schema, and be one the API can set — an attribute managed in the app is refused, not silently dropped.
  • A closed-set attribute's value must be one of the schema's AllowedValues codes (matched case-insensitively), unless the attribute allows custom values.
  • Values must fit the attribute's shape — a whole number where one is demanded, a dot-decimal where a decimal is.
  • Every required attribute must be present. Values the listing already holds satisfy this, so an upsert on an existing SKU is a partial update — you do not re-send the whole schema to change one field.
  • CategoryId must be a category of that marketplace (a 404 otherwise), and ProductTypeViewId must be the one the schema endpoint returned for it — validating against one contract and publishing under another is refused.

Sending Attributes without a CategoryId is a 400: attributes only mean something against a category's schema.

On Amazon you can omit both fields, and the product is created with no attribute values, to be filled in the app. On the other marketplaces CategoryId is required regardless — see the category rule below.

Warnings: what the API cannot carry

Some required attributes cannot travel in this payload — images are media-library records, eBay business policies are account records picked in the app. A missing one never fails the create; it comes back as a warning:

{
  "Warnings": [
    "The marketplace requires 'main_image' and it cannot be set through the API — fill it in the app before the listing can publish."
  ]
}

Check Warnings on every create. A warned listing is saved and queued, but the marketplace will reject it until the named field is filled in the app.

The category rule: Amazon versus everyone else

Amazon takes its taxonomy from the product type — ProductTypeViewId carries the schema, so CategoryId is optional there (when you send one, it is stamped on the listing). Walmart works the same way, and Google Shopping files a listing under a category itself. On eBay, Etsy, Shopify and TikTok Shop the category travels on the product itself: it is what the listing is filed under, and a listing without one cannot list.

BigCommerce has one category, All products: POST /api/product-types/Search with no SearchText finds it. The product goes in the store's default category (Store defaults in the app), but the CategoryId is still required, because it checks the create against BigCommerce's schema. BigCommerce needs a package weight on every product (mh_package_weight, in kg), so a create without one is refused on this call.

So a create for eBay, Etsy, Shopify, TikTok Shop or BigCommerce without a CategoryId is refused up front with a per-field 400:

{
  "code": "validation-failed",
  "errors": {
    "CategoryId": ["Required for eBay — the listing cannot publish without a category. Find one with POST /api/product-types/Search."]
  }
}

The alternative — accepting the create and letting the publish reach the marketplace without a category — fails anyway, hours later, as a feed error in the app. We would rather refuse on this call, where the error names the fix.