Customer catalogue and prices
When your plugin works for one signed-in customer, it often needs two answers the plain product API cannot give: which products this customer may see, and what they pay. A B2B buyer can be confined to a restricted catalogue, and can have negotiated prices, group discounts and fixed prices on single variants. The customer catalogue view answers both questions the way the storefront does.
Use it for anything a trade customer builds from the shop's products: quotes, price lists, product sheets, reorder pads.
Scopes
Every route needs three scopes together: a product scope, customer:read and b2b-catalog:read. The answer includes the prices that customer pays, negotiated B2B prices included, and those are B2B catalogue data, so the merchant consents to them through b2b-catalog:read.
| Route | Scopes |
|---|---|
GET /v1/customers/{customerId}/catalog/products | product:list, customer:read and b2b-catalog:read |
GET /v1/customers/{customerId}/catalog/products/{productId} | product:read, customer:read and b2b-catalog:read |
POST /v1/customers/{customerId}/catalog/prices | product:read, customer:read and b2b-catalog:read |
A token missing any of the three gets 401, naming all three. A plugin or a store API key can hold them; an AI assistant connection cannot hold b2b-catalog:read, so it cannot use these routes. The store does not need the B2B feature: without it, every price is the regular price (source: "base"). A customer of another store gets 404, worded exactly as for a customer that does not exist.
Prices
Every price in this view has one shape:
{ "amount": 8000, "currency": "GBP", "taxInclusive": false, "source": "b2b" }amountis a whole number of minor units (pence for GBP), excluding VAT, in the store's primary currency. When the store enters its prices including VAT, the VAT is taken out at the rate of the product's tax class in the store's home region, worked out exactly as checkout works it (VAT rounded half up to the penny, net is the price less that VAT). A tax-exempt customer pays no VAT at checkout, so their prices are published as the price they pay, with nothing taken off.sourceisb2bwhen the customer's B2B catalogue sets the price (a catalogue price for the product or the variant, or a group pricing rule such as 10% off a category), andbasewhen the regular price applies. A running sale is part of the regular price, as it is at checkout.- A price is
nullwhen nothing prices the item.
These are the prices checkout charges this customer, not a separate list: when the shop changes a catalogue, the next call shows it.
Listing what a customer may see
GET /v1/customers/7c2f0d4e-1b6a-4c3d-9e8f-0a1b2c3d4e5f/catalog/products?keyword=oak%20desk&page=1&limit=20 HTTP/1.1
Host: api.myorbitcommerce.net
Authorization: Bearer <token>{
"statusCode": 200,
"message": "Success",
"data": {
"items": [
{
"id": "2b4e6f80-3c1d-4a5b-8e9f-1a2b3c4d5e6f",
"title": "Oak Desk",
"imageUrl": "https://cdn.myorbitcommerce.net/stores/.../oak-desk.jpg",
"variantCount": 2,
"fromPrice": { "amount": 8000, "currency": "GBP", "taxInclusive": false, "source": "b2b" }
}
],
"meta": { "page": 1, "limit": 20, "hasMore": false }
},
"timestamp": "2026-10-19T10:12:03.511Z"
}- Only active products are listed, and a customer on a restricted catalogue sees only that catalogue's products.
- With
keyword(up to 100 characters) the list is a storefront search: ranked by relevance, matching what the store's search box matches, and leaving out products the merchant hid from search. If the store's search settings keep this customer out of search, a keyword search answers an empty page. Without it, products are listed newest first. limitis 1 to 50 (default 20).meta.hasMoresays whether another page exists; there is no total count.fromPriceis the price the storefront product card shows this customer, net of VAT as above: their B2B catalogue price for the product when it sets one, else the regular from price. It is for display only, never a line price: a variant the catalogue prices on its own can cost more or less, so read the product or the prices route for what each variant costs.
One product
GET /v1/customers/7c2f0d4e-1b6a-4c3d-9e8f-0a1b2c3d4e5f/catalog/products/2b4e6f80-3c1d-4a5b-8e9f-1a2b3c4d5e6f HTTP/1.1
Host: api.myorbitcommerce.net
Authorization: Bearer <token>{
"statusCode": 200,
"message": "Success",
"data": {
"id": "2b4e6f80-3c1d-4a5b-8e9f-1a2b3c4d5e6f",
"title": "Oak Desk",
"imageUrl": "https://cdn.myorbitcommerce.net/stores/.../oak-desk.jpg",
"options": [{ "name": "Width", "values": ["120 cm", "140 cm"] }],
"variants": [
{
"id": "9d01a2b3-c4d5-4e6f-8a9b-0c1d2e3f4a5b",
"sku": "DESK-120",
"title": "120 cm",
"options": [{ "name": "Width", "value": "120 cm" }],
"imageUrl": "https://cdn.myorbitcommerce.net/stores/.../oak-desk-120.jpg",
"price": { "amount": 8000, "currency": "GBP", "taxInclusive": false, "source": "b2b" }
}
],
"truncated": false
},
"timestamp": "2026-10-19T10:12:04.020Z"
}- Up to 2,000 variants, in the order the storefront shows them.
truncatedistruewhen the product has more. - Pick a variant by matching its
optionsto what the customer chose. 404when the product is not active, belongs to another store, or is outside the customer's restricted catalogue.
Refreshing prices you saved
When you keep a customer's price (on a quote, say), refresh it later with one call rather than one per product:
POST /v1/customers/7c2f0d4e-1b6a-4c3d-9e8f-0a1b2c3d4e5f/catalog/prices HTTP/1.1
Host: api.myorbitcommerce.net
Authorization: Bearer <token>
Content-Type: application/json
{ "variantIds": ["9d01a2b3-c4d5-4e6f-8a9b-0c1d2e3f4a5b", "4a7c1e2f-3b4d-4c5e-9f6a-7b8c9d0e1f2a", "ffffffff-0000-4000-8000-000000000000"] }{
"statusCode": 200,
"message": "Success",
"data": {
"items": [
{
"variantId": "9d01a2b3-c4d5-4e6f-8a9b-0c1d2e3f4a5b",
"productId": "2b4e6f80-3c1d-4a5b-8e9f-1a2b3c4d5e6f",
"visible": true,
"price": { "amount": 8000, "currency": "GBP", "taxInclusive": false, "source": "b2b" }
},
{
"variantId": "4a7c1e2f-3b4d-4c5e-9f6a-7b8c9d0e1f2a",
"productId": "5e11c2d3-e4f5-4a6b-8c7d-9e0f1a2b3c4d",
"visible": false,
"price": null
},
{
"variantId": "ffffffff-0000-4000-8000-000000000000",
"productId": null,
"visible": false,
"price": null
}
]
},
"timestamp": "2026-10-19T10:12:05.007Z"
}- 1 to 200 ids; letter case does not matter: every id comes back in lower case, and a repeated id is answered once, at its first position. Items come back in the order you sent them.
visible: falsemeans the customer may no longer see the variant (for example its product is no longer active, or left their catalogue).productId: nullmeans the id is not a live variant of this store at all.- The call answers
200, writes nothing and counts against the read rate limit.
Errors
| Status | When |
|---|---|
400 | An id that is not a uuid; keyword over 100 characters; page not a whole number from 1 to 1,000,000,000; keyword sent more than once; limit not a whole number from 1 to 50; an empty variantIds, more than 200, or one that is not a uuid; any other query parameter or body field. |
401 | No token, or a token without all three scopes. |
404 | The customer is not in this store, or has been erased; on the product route, a product this customer may not see. |
Filing uploads in a media folder
POST /v1/media takes an optional text field folder beside file. The upload lands in that top-level folder of the store's media library, made the first time a name is used; the same name always means the same folder. Without folder the file lands in the library root, as before.
curl -X POST https://api.myorbitcommerce.net/v1/media \
-H "Authorization: Bearer <token>" \
-F folder="Customer logos" \
-F file=@logo.pngThe name is 1 to 100 characters (surrounding spaces are trimmed), may not contain / \ : * ? " < > |, and must be sent once. Anything else answers 400 and nothing is stored. The response is unchanged; its folderId names the folder. Needs media:create.