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.
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
PriceorQuantitymeans "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..."]
}
| Result | Means |
|---|---|
Updated | Matched, and at least one value differed and was written. |
Unchanged | Matched, but everything you sent already equalled what we hold. Not an error — this is what a nightly full-catalogue push looks like. |
NotFound | No product carries that SKU. Reported, never silently dropped. |
Ambiguous | More than one product carries that SKU and none matched its exact casing. |
Failed | Matched, 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:
- Find the category — POST /api/product-types/Search searches a connected marketplace's own taxonomy.
- Read its schema — GET /api/product-types/Schema returns the attribute contract for that category and the
ProductTypeViewIdthe create needs. - 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.
POST /api/product-types/Search
Searches a connected marketplace's categories for one to list a product under. Needs InventoryRead.
{
"MarketplaceId": "6a73...",
"SearchText": "running shoes",
"Limit": 50,
"Offset": 0
}
MarketplaceIdis 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".SearchTextmatches 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.Limitdefaults to 50, maximum 200. Page withOffsetagainst the returnedTotalCount.
{
"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
}
]
}
ProductTypeViewIdis 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.ProductTypeCodeis the marketplace's own code behind this category — Amazon's product type (e.g.SHOES), orcommonon the marketplaces whose attributes hang off the category rather than a product type.- Per attribute:
Codeis the key to send in the create'sAttributes;Typeis the value shape (Text,Integer,Decimal,Date,Boolean) — descriptive, closed sets are expressed byAllowedValues, not here; for a closed-set attribute send anAllowedValuesentry'sValueand display itsTitle. WhenAllowsCustomValuesis true the list is suggestions, not a hard enum. AllowedValuesis 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
ListingActiontells you whether the marketplace will get a new listing (Create) or a revision of the one it already has (Revise). - Omitting
Quantityleaves stock untouched — it never means "set to zero". Send the field only when you are changing it. ProductTypeViewIdmust belong to the marketplace inMarketplaceId— one from another marketplace's taxonomy is a per-field400, not an id that fails hours later inside the publish.CategoryIdis required for eBay, Etsy and Shopify and optional for Amazon — see the category rule.- Send an
Idempotency-Keyheader 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
AllowedValuescodes (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.
CategoryIdmust be a category of that marketplace (a404otherwise), andProductTypeViewIdmust 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.