MarketplaceHub Docs Docs

Inventory

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

Reading the catalogue and searching a marketplace's taxonomy needs InventoryRead; pushing price and stock, and creating products, needs InventoryWrite.

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.

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.

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 categoryPOST /api/product-types/Search searches a connected marketplace's own taxonomy.
  2. Read its schemaGET /api/product-types/Schema returns the attribute contract for that category and the ProductTypeViewId the create needs.
  3. CreatePOST /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.

POST /api/products/Create

Adds a SKU to your catalogue and queues it for publishing to a marketplace. Needs 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 and Shopify and optional for Amazon — 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). On eBay, Etsy and Shopify the category travels on the product itself: it is what the listing is filed under, and a listing without one cannot list.

So a create for those marketplaces 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.