Skip to content

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.

RouteScopes
GET /v1/customers/{customerId}/catalog/productsproduct: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/pricesproduct: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:

JSON
{ "amount": 8000, "currency": "GBP", "taxInclusive": false, "source": "b2b" }
  • amount is 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.
  • source is b2b when 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), and base when the regular price applies. A running sale is part of the regular price, as it is at checkout.
  • A price is null when 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

HTTP
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>
JSON
{
  "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.
  • limit is 1 to 50 (default 20). meta.hasMore says whether another page exists; there is no total count.
  • fromPrice is 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

HTTP
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>
JSON
{
  "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. truncated is true when the product has more.
  • Pick a variant by matching its options to what the customer chose.
  • 404 when 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:

HTTP
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"] }
JSON
{
  "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: false means the customer may no longer see the variant (for example its product is no longer active, or left their catalogue). productId: null means 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

StatusWhen
400An 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.
401No token, or a token without all three scopes.
404The 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.

Shell
curl -X POST https://api.myorbitcommerce.net/v1/media \
  -H "Authorization: Bearer <token>" \
  -F folder="Customer logos" \
  -F file=@logo.png

The 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.