Orbiio Product Catalogue API · v1
An organisation-specific, read-only feed for integration with that organisation's website. Orbiio is a shared platform, not a catalogue tied to any particular company, product or customer, and remains the master system. This API reads a separate, administrator-approved catalogue; it does not offer inventory writes, reservations, orders, supplier data, costs or company records.
Quick start
- An Orbiio administrator opens Commercial → Store → Catalogue Manager.
- On a catalogue item, switch List live on to publish it immediately to this organisation's API and Catalogue Manager; switch it off to withdraw it. Publication still requires valid product details and an available source. Switching List live on includes public retail pricing. Exact stock quantities remain hidden unless explicitly enabled in Catalogue Manager. Previously published items are not changed automatically by this update. Alternatively, add items as drafts in Catalogue Manager, review website content, retail pricing, stock visibility and any variant relationships, then choose Approve & publish. Existing website content and visibility settings are preserved when relisting. Nothing is published automatically when the gateway is first installed.
- In Website access, create a dedicated key. Pass the key securely to the website developer, who stores it in their server's secret/environment settings as ORBIIO_CATALOGUE_KEY. No Orbiio user account or database credential is needed.
- Make HTTPS GET requests from the website backend. Present the approved responses through the website's own backend to its visitors. Never embed a key in frontend JavaScript, a public repository, a query string or a mobile app bundle.
Base URL: https://orbiio.co.uk/functions
All endpoints require the X-Catalogue-Key header. An organisation's administrator generates a shared secret in Orbiio and securely gives it to their web developer for this integration. Each key is scoped to the organisation active when it is generated and expires on the configured date. The key determines which organisation's approved catalogue is returned; the developer does not supply or choose an organisation ID. Keys are individually revocable. Only their SHA-256 fingerprints are stored by Orbiio.
Endpoint reference
- GET /catalogueProductsV1 — Published top-level products; optional variants and filters.
- GET /catalogueProductV1?id=PRODUCT_UUID — One published product, with a paginated array of its published variants.
- GET /catalogueCategoriesV1 — Published category names.
- GET /catalogueCategoriesV1?category=CATEGORY — Published subcategory names for that category.
- GET /catalogueStockV1 — Public availability; optional id for a single product.
- GET /cataloguePricesV1 — Public retail prices; optional id for a single product.
- GET /catalogueConfiguratorV1?id=PRODUCT_UUID — Opt-in interactive configurator, retail option prices and scoped downloads for one published product.
These are six separate, versioned HTTP endpoints, not subpaths under an /api route. POST, PUT, PATCH, DELETE and HEAD return 405. OPTIONS is used only for CORS preflight. Future breaking changes will use a new endpoint version.
Requests
curl --get 'https://orbiio.co.uk/functions/catalogueProductsV1' --header 'X-Catalogue-Key: YOUR_SERVER_SIDE_CATALOGUE_KEY' --data-urlencode 'limit=25' --data-urlencode 'category=Example category'
Replace the illustrative key with the dedicated catalogue key, never with a Base44 personal token or service credential.
Filters and pagination
- Listings, prices and stock: limit (1–50, default 25), cursor, category, subcategory, sku, updated_since (ISO 8601), include_variants (true/false, default false).
- Product listings additionally accept q, a literal case-insensitive search over approved name and SKU.
- Stock and price endpoints optionally accept id. Do not combine id and cursor. A matching variant can be retrieved by id regardless of include_variants.
- Product details require id, and accept limit and variant_cursor for the embedded variant page.
- Categories accept limit, cursor and optional category to list subcategories.
- All filtering is applied on the server after organisation and publication checks. Arbitrary fields, raw filters, repeated parameters and unknown parameters are rejected.
- Read pagination.has_more and pass pagination.next_cursor unchanged to the next request. Treat cursors as endpoint-specific tokens and URL-encode them. Keep the filters unchanged. Product details use variants_pagination and variant_cursor instead.
- There is no fixed maximum catalogue size: keep requesting pages until has_more is false. Results use stable product-ID ordering (category names use ascending ordering).
- updated_since is a convenience filter, not a change-event stream: withdrawn products are not emitted as tombstones. Reconcile complete listings periodically and remove a known product if its details return 404. Pages are not a transactional snapshot; deduplicate by product_id if publication changes during a crawl.
Response example
Examples use generic placeholders such as Company Name, John Smith, John Doe, Product X and Location X. They never identify a real organisation, product or customer. The following product, SKU, category, quantity and timestamps are illustrative, not live catalogue data.
{
"api_version": "1",
"data": [
{
"product_id": "11111111-1111-4111-8111-111111111111",
"sku": "EXAMPLE-001",
"name": "Product X",
"category": "Example category",
"subcategory": "Example subcategory",
"description": "Approved plain-text website description.",
"images": [],
"variant_of": null,
"price": {
"visible": true,
"excluding_vat": 123.45,
"including_vat": 148.14,
"vat_rate": 20,
"currency": "GBP"
},
"availability": {
"status": "in_stock",
"is_current": true,
"checked_at": "2026-11-04T10:00:00.000Z",
"expires_at": "2026-11-04T10:15:00.000Z",
"reason": "inventory_snapshot",
"quantity": 12
},
"lead_time_days": 5,
"publication_status": "published",
"updated_at": "2026-11-04T10:00:00.000Z"
}
],
"pagination": { "has_more": false, "next_cursor": null, "limit": 25 }
}
Details return a single data object, plus data.variants and variants_pagination. A variant is a separately approved product with its own public UUID, SKU, inventory link, retail price and visibility controls. variant_of is the parent's external UUID, never an internal inventory ID. Publishing a parent does not publish its variants. A separately published variant remains independently addressable if its parent is later withdrawn.
Categories return data: [{ "name": "Example category" }]. Subcategory entries also contain parent_category. Stock entries contain product_id, sku, availability, lead_time_days and updated_at. Price entries contain product_id, sku, price and updated_at.
Retail price rules
- Prices are retail amounts, never supplier cost or internal BOM cost.
- price.visible=false means do not display a price. The reason is hidden, stale_or_unavailable, configuration_not_synced, or unavailable. No numeric price or currency is returned in that case.
- Catalogue-based prices use the linked Catalogue item's sell price, currency and tax rate, plus the configurator's automatic required/default selections, dependent sub-options and quantities. price.type and pricing.type are from for configurable items, otherwise fixed. Prefix the displayed current price with From when type is from. These use the same starting-price calculation as the Catalogue Items cards; they are not a mathematical minimum over every possible configuration.
- Website-specific pricing changes only the external catalogue. VAT-inclusive price is calculated from the approved rate, rounded to two decimal places. No automatic currency conversion is performed.
- Products, details and price responses include pricing alongside the backwards-compatible price. When hidden or stale, pricing contains only visible=false and a reason: no numeric retail values, discounts or tiers are exposed.
- pricing.base is the regular sell price before options. pricing.starting includes the automatic starting configuration before any website sale. pricing.list adds the same starting options to the source catalogue Was price/RRP when higher, otherwise to the base price. pricing.current is the effective starting price now. Each money object contains excluding_vat, including_vat, currency and vat_rate. The existing price field always reflects current, with type to distinguish From and fixed prices. Website-specific overrides are complete fixed prices and never have catalogue options added.
- pricing.sale contains active, price (null when inactive), starts_at and ends_at. In Catalogue Manager, edit the product, enable retail prices, set Website sale pricing and optional dates, then choose Publish changes. Sale prices must be below the regular base price. Empty dates mean immediately/no end date; the API evaluates the dates in UTC at read time without waiting for a scheduled job. The editor displays dates in the operator's device timezone. Sales affect only website pricing, not quotes or master sell prices.
- An existing catalogue sell price below its Was price also reports sale.active=true. Explicit scheduled sales override the base portion during their active window; the automatic option total is added once, not discounted again. Volume tier prices likewise include that same option total. List/Was prices are reference prices, not a promise that the item was previously sold at that amount.
- pricing.quantity_breaks contains min_qty, label and a price money object. Use the highest qualifying min_qty for the quantity ordered, then charge the lower of that tier's unit price and pricing.current. Never add the two discounts together. Volume tiers follow the catalogue's currency and VAT; they are omitted for website-specific pricing to avoid mixing price sources. Set source tiers in the catalogue item's Volume price breaks editor and use Sync now to refresh them.
- The normal price response publishes the default starting configuration. Opt-in interactive definitions and the live embed publish selectable options separately, so other combinations can use the same rules and component prices. Never label pricing.base as a complete configured price, add the option total a second time, or invent prices for other bundles. Sync now refreshes existing published records without changing their price-visibility settings; configuration_not_synced means a historical base-only record still needs that refresh.
- Description is plain text. Render it as text, never inject it as HTML. Images contain public HTTPS URLs and captions only.
Interactive configurator and live embed
In Catalogue Manager, edit a product and enable Publish selectable options, retail prices and compatibility rules under Configurator broadcast & embed. Choose visible catalogue-based pricing, then Publish changes. This is opt-in: existing visibility settings and older share links are not changed. Fixed website overrides cannot be combined with selectable catalogue prices.
GET /catalogueConfiguratorV1?id=PRODUCT_UUID uses the same server-side X-Catalogue-Key, organisation scope and quota as the other catalogue endpoints. Product listing and detail responses advertise configurator.enabled, endpoint and embed_path; this endpoint returns the full configurator definition alongside the approved product and downloads. An unavailable definition returns enabled=false with a reason, never hidden numeric prices.
The definition includes version, groups, base_price, starting_price, currency, vat_rate (percent), downloads, expires_at and price_rule. Each group has an opaque name used as its selection key, a human-readable label, is_required, qty_enabled, options and subGroups. Options include an opaque part_id choice key, public part_name/part_number, per-unit price excluding VAT, is_default, default_qty, mandatory_with_part_ids, incompatible_with_part_ids, paired_part_ids and paired_requirements (part_id and optional qty). Optional mount_group/mounts_for refer to opaque group keys. These are external references, never inventory record IDs.
Use labels for display and keys for rules. Optional groups start at None; required groups select their available default or first choice. Enforce dependencies, incompatibilities, paired sub-options and quantity constraints when a choice changes. Reuse the provided live embed when the website does not already implement these rules. The embedded UI uses Orbiio's existing shop selection engine, not an independent simplified calculator.
The current configuration total is base_price plus each selected top-level option price multiplied by its selected quantity, plus each selected sub-option price once. Clamp the final ex-VAT total at zero, round it to two decimal places, then calculate VAT. base_price already includes any currently active base sale: do not apply the sale twice or use the starting total as the base. Never silently treat a missing/unavailable price as zero. These are retail estimates, not payment authorization; a checkout must validate its own payable amount server-side. Respect expires_at and refresh before continuing to show expired prices.
After publishing, Copy embed supplies an iframe containing the public configurator link, never a website API key. It displays choices, changing totals, product downloads and matching scoped files without sign-in. It is read-only: it does not place orders, reserve stock or submit customer details. Use a responsive full-width iframe; adjust its height for the host page. The hosting site's framing policy must permit the embed.
The embed path is relative to the Orbiio app origin, not the website's origin. Disabling broadcasting or withdrawing the product blocks future reads. Re-enabling broadcasting issues a new share link; replace old iframe code. Anyone who has this public link can read its approved content, and already downloaded public files cannot be recalled.
Option-specific and configuration-specific files
First attach the public customer file to the catalogue item and approve it under Published documentation & downloads. For each approved file, select Whole product, Selected option, or Matching seller preset, then publish changes. Option associations select one current group/choice. Configuration associations select an active, non-customer seller preset belonging to the same catalogue item; customer submissions, contact data and internal saved-state fields are never broadcast.
Whole-product files remain in product.downloads. Scoped files appear only in configurator.downloads, with the usual name/url/type/file_size plus scope and an array of conditions. All conditions must match before showing the link. A condition's key is the opaque group name, or parentGroupName + '::' + subGroupName for a sub-option; value is an opaque choice key or none, and optional quantity is an exact top-level quantity match. Configuration-specific files match the preset's complete effective selection, including omitted choices; they do not publish the preset record or its private metadata. An unavailable option, removed source attachment, archived preset or customer-submission preset is excluded at the next successful sync.
Selection conditions control display, not authorization to download: all approved URLs are already public. Do not use these associations for confidential, paid or entitlement-restricted files. No private engineering attachments are auto-published.
Documentation and downloads
- Product listings and details include downloads, an array of name, url, type and optional file_size (bytes). Types are user_guide, technical, software, datasheet and other. Documentation and downloadable software use this same array. An empty array means no files were approved for this product.
- Attach customer-facing files using the existing catalogue item editor, then open the product in Catalogue Manager. Under Published documentation & downloads, explicitly select the files and publish changes. Adding an attachment alone never publishes it to the API.
- Only permanent public HTTPS attachment URLs belonging to that catalogue item can be selected. Private URLs, expiring/signed links and URLs containing query strings or fragments are rejected. Internal engineering attachments, supplier files and other private documents are never discovered or copied by the API.
- This API returns download metadata and links, not file bytes. Website backends can render these as download links. Fetch only the approved URL and treat downloaded files as untrusted content; the catalogue does not scan software for malware.
- Removing a source attachment removes it from the next successful sync; unchecking and publishing removes it from the catalogue immediately (subject to the short response cache). A previously public file URL is not revoked by unpublishing and may still be held by previous visitors.
Availability and freshness
Public status codes: in_stock, low_stock, made_to_order, pre_order, enquire.
- Automatic mode uses the linked master record's recorded stock quantity. A zero balance becomes Enquire; a positive balance at or below the approved low-stock threshold becomes Low Stock.
- In Stock/Low Stock overrides still require a positive master balance. They cannot invent stock. Made to Order and Pre-Order are administrator settings, not allocation promises.
- quantity is omitted entirely unless actual quantities were explicitly enabled and a fresh inventory reading is available. A missing quantity is not zero.
- is_current reports whether a linked inventory snapshot is verified within the freshness window. It does not promise realtime allocation or reservable stock. These are recorded on-hand quantities; reservations and checkout commitments must be handled separately by the business.
- Stock snapshots expire after 15 minutes. A failed sync does not refresh the timestamp. Missing, changed, inactive or stale sources fail safely: no quantity, no stale price, and stock shown as Enquire.
- A manually sourced catalogue item has no verified inventory quantity. It may show an approved Made to Order/Pre-Order message while its catalogue snapshot is fresh, but is_current remains false and quantity is absent.
- The website must also check expires_at before presenting cached stock as current. On an outage, timeout, expired snapshot or absent verification, display Enquire rather than an old In Stock label.
- The API is informational: it does not reserve, allocate or decrement inventory, and lead time is an estimate.
Synchronisation and publication
Inventory quantity/status changes and catalogue changes trigger synchronisation of selected products. A five-minute reconciliation also handles updates that bypass entity events (including bulk updates) and renews unchanged inventory snapshots. Synchronisation is processed in batches of 50, oldest snapshots first. A background run is bounded to 1,000 records or three minutes; any remainder is handled on the next five-minute reconciliation, without extending its freshness window.
Stock, source retail snapshots and explicitly enabled configurator broadcasts refresh automatically. Approved website names, categories, descriptions, imagery, lead times and publication choices remain independent. Source content changes are flagged for review. If the inventory link changes, administrators must review and republish before stock can be confirmed again. Drafts and archived products never appear externally.
In Catalogue Manager, use Sync now for a manual refresh and Activity to review results. Failed background jobs leave previous timestamps intact; availability becomes unknown when their freshness window expires.
Cache behaviour
- Successful responses use Cache-Control: private, max-age=0..15, must-revalidate. Respect the returned lifetime; it shrinks near stock expiry. Do not store credential-bearing responses in a shared CDN cache.
- Responses have ETag. Send If-None-Match on a later request; a 304 has no response body and retains your cached representation. Authentication and freshness checks run before conditional responses.
- Revalidation counts towards the request quota. Website backends should consolidate requests and serve their own short-lived cache instead of calling the gateway per visitor.
- Errors use no-store. Do not extend the lifetime of an old successful availability response after an error.
- Revoked keys stop new requests. Previously received responses can remain cached for their remaining lifetime, never longer than 15 seconds.
- Withdrawn products disappear on the next uncached/revalidated request; already delivered content cannot be recalled.
Authentication, CORS and limits
Keep one key per website integration. Rotation: create a replacement, update the website's secret, verify its connection, then revoke the old key. Store no Orbiio login password or general platform API token on the website.
CORS is denied by default. Server-to-server requests without Origin work with a valid key. If a browser-origin request is needed, configure exact HTTPS origins on that key; no wildcards, credentials-in-cookies, paths or trailing slashes. Both preflight and GET enforce the allowlist. CORS is not authentication: a valid key is always required and must remain server-side.
Keys default to 30 requests per UTC minute, configurable by an administrator from 5 to 60 across all six keyed endpoints. The counter uses an atomic shared database update, not an in-memory per-instance counter. Inspect X-RateLimit-Limit and X-RateLimit-Reset. A 429 includes Retry-After. Platform capacity limits may instead return 503 with Retry-After: 60. Back off; never retry in a tight loop.
Error responses
{
"error": {
"code": "not_found",
"message": "Published product not found.",
"request_id": "request-correlation-uuid"
}
}
- 400: Invalid ID, cursor, filter or page size. Correct the request.
- 401: Missing, invalid, expired or revoked key. Check the website's server secret.
- 403: Origin not allowed. Use the website backend or correct the exact origin allowlist.
- 404: Product absent or not published. Remove/withhold it; do not assume zero stock.
- 405: Write or unsupported HTTP method. Use GET; no writes are available.
- 429: Key quota reached. Wait for Retry-After.
- 500 / 503: Temporary service failure. Back off and show Enquire, not old availability.
X-Request-Id correlates a response with audit activity. Authenticated requests, publication actions, key changes and successful/partial sync runs are recorded without secrets or payload bodies for 30 days. Unknown-key attempts and unexpected failures go to sanitised runtime logs; they are not attributed to a catalogue organisation.
Website backend example (JavaScript)
This runs on the website server, not in a browser. Feed its returned data into the existing website template or the website's own read-only proxy route. The key is a website-server environment variable.
const base = 'https://orbiio.co.uk/functions';
export async function getCataloguePage(cursor) {
const params = new URLSearchParams({ limit: '25' });
if (cursor) params.set('cursor', cursor);
const response = await fetch(base + '/catalogueProductsV1?' + params, {
headers: { 'X-Catalogue-Key': process.env.ORBIIO_CATALOGUE_KEY },
signal: AbortSignal.timeout(10000),
cache: 'no-store'
});
if (!response.ok) {
// Honour Retry-After for 429/503 and show Enquire on the website.
throw new Error('Catalogue unavailable: ' + response.status);
}
return response.json();
}
export function displayStock(product) {
const stock = product.availability;
if (stock.is_current && Date.parse(stock.expires_at) > Date.now()) {
return stock.status;
}
// Fresh merchant messages have no inventory verification.
if (stock.reason === 'inventory_not_linked' &&
['made_to_order', 'pre_order'].includes(stock.status)) return stock.status;
return 'enquire';
}
For a complete catalogue, repeatedly call getCataloguePage with pagination.next_cursor until has_more is false. Schedule crawls sensibly, respect the quota, and bound any locally cached merchant messages to the HTTP cache lifetime as well. Never return the key to visitors.
Website backend example (PHP)
<?php
$ch = curl_init('https://orbiio.co.uk/functions/catalogueProductsV1?limit=25');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['X-Catalogue-Key: ' . getenv('ORBIIO_CATALOGUE_KEY')]
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200 || $body === false) {
// Back off, honour Retry-After where supplied, and show Enquire.
throw new RuntimeException('Catalogue temporarily unavailable');
}
$catalogue = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
?>
Security boundary
The public endpoints only read ExternalCatalogProduct and their dedicated key records; they write request counters and audit events only. They never read or write the master inventory, BOMs, supplier records, costs, contacts or company data. The sync/publishing functions are separate, administrator-authenticated operations. All new storage is restricted to administrators (including Orbiio's existing super-admin role); ordinary users and anonymous callers cannot read drafts or credential records through the entity API.