Products
Products are read-only over the API - they are authored in ibakepro. There is no create, update, or delete endpoint. The API returns a safe catalog subset: never costs, recipes, margins, wholesale ladders, or any other internal data. Use it to power a headless storefront and to discover the product_id, variant_id, choice_id, and option_id values you need when building order lines.
Products endpoints accept both secret (ibp_sk_) and publishable
(ibp_pk_) keys, and return CORS headers, so a browser storefront can read the
catalog directly. Only products that are active, storefront-enabled, and
not deleted are returned - drafts and archived products never appear, in
either endpoint.
List products
List catalog products, cursor-paginated. Each entry is a light list item, not the full product - fetch the product by id for variants, choices, custom fields, and allergens.
Required scope: products:read
Query parameters
- Name
limit- Type
- integer
- Description
Page size. Default
50, maximum100. Values above 100 are clamped.
- Name
cursor- Type
- string
- Description
Opaque cursor from
pagination.next_cursor. A cursor whose product no longer exists returns400with codeinvalid_cursorrather than silently restarting at page 1 - restart the list without a cursor.
- Name
category- Type
- string
- Description
Exact-match filter on the product's
category.
- Name
search- Type
- string
- Description
Case-insensitive "name contains" filter. Applied after the query, so it narrows the page rather than the index - use
sort/cursornormally.
- Name
updated_after- Type
- string
- Description
ISO 8601 timestamp. Returns products whose
updated_atis greater than or equal to this value. An unparseable value returns400with fieldupdated_after. Supplying it forcessort=updatedAtregardless of what you pass insort.
- Name
sort- Type
- string
- Description
nameorupdatedAt. Defaultname. Directions are fixed per field:nameis always ascending,updatedAtis always descending. A leading-is stripped and any other value falls back toname. Note the value isupdatedAt(camelCase) even though the response field isupdated_at.
Incremental sync
updated_after returns only the products written since a given timestamp:
GET /api/v2/products?updated_after=<last_sync_iso>&sort=updatedAt
Page through with cursor. Two things the API does here:
- The comparison is inclusive (
>=), so a product whoseupdated_atequals the value supplied is returned again. updated_atreflects any write to the product, including internal recalculations, so a delta pull can return a product whose visible data did not change.
A delta pull only ever returns visible products. A product that is archived, deleted, or storefront-disabled disappears from the list rather than arriving with a tombstone, and the detail endpoint returns 404 for it.
Request
curl -G https://au.api.ibakepro.com/api/v2/products \
-H "Authorization: Bearer {api_key}" \
-d limit=10
Response
{
"data": [
{
"id": "prod_ze9HI0w0LpVbemUI",
"name": "Chocolate Birthday Cake",
"slug": "chocolate-birthday-cake",
"status": "active",
"starting_from": 85,
"image": "https://storage.googleapis.com/ibakepro-media/products/chocolate-birthday-cake.png",
"category": "Cakes",
"featured": false,
"updated_at": "2026-06-04T07:17:34.854Z"
}
],
"pagination": {
"limit": 10,
"has_more": true,
"next_cursor": "eyJpZCI6InByb2R1Y3QtMTc4MDMxNzI0ODQxMy0yOWNldmtiNm0ifQ",
"prev_cursor": null
}
}
Retrieve a product
Read one product (full subset). Returns 404 if the product is missing, soft-deleted, not active, or storefront-disabled. The body is the product object itself, not wrapped in data.
Required scope: products:read
Field notes
- Name
status- Type
- string
- Description
Always
activefor anything the API returns. Other statuses404.
- Name
slug- Type
- string
- Description
SEO slug, falling back to the product id when none is set.
- Name
images- Type
- array
- Description
Plain URL strings, in author order. Storage paths and original filenames are never exposed.
- Name
taxable- Type
- boolean
- Description
trueunless explicitly turned off.
- Name
starting_from- Type
- number
- Description
The advertised "from" price: the cheapest variant price plus the unavoidable upcharge any required choice forces (its cheapest option). It is a display headline, not a line price - never send it as a price. Per-variant
priceis the raw base.
- Name
pricing_currency- Type
- string
- Description
ISO currency code for every amount on the product, resolved from the account's settings. Not present on list items.
- Name
has_variable_pricing- Type
- boolean
- Description
trueonly when the product has more than one variant and those variants have more than one distinct price. A multi-variant product priced identically across variants reportsfalse.
- Name
sale_unit- Type
- object
- Description
How the product is sold:
mode(eachorweight),label,min,step. Ineachmode quantities are whole numbers with a minimum of 1. Inweightmode quantities are fractional in units oflabel(for examplekg), clamped tominand stepped bystep, and the variantpriceis the price per unit oflabel- the line total isprice x quantity.
- Name
variants- Type
- array
- Description
id,name,price,servings.idis what you send as the variant/size on an order line.priceis the raw base for that variant and excludes every choice and custom-field upcharge.servingsdefaults to1.
- Name
fulfilment_methods- Type
- array
- Description
The methods this product can be fulfilled by, a subset of
pickup,delivery,dispatch, in that canonical order. A product that declares none falls back to["pickup", "delivery"].pickup_available,delivery_available, anddispatch_availableare booleans derived from this list, so they can never disagree with it.
- Name
custom_fields- Type
- array
- Description
id,name,type,required,options(value,label,price_modifier),default_value. Supply ordercustom_fieldskeyed byid. Back-of-house fields are never exposed here.
- Name
default_value- Type
- any
- Description
The resolved prefill for a custom field, or
null. A field the product defers to the customer emitsnulleven when a default is configured, because prefilling it would satisfy the field at checkout and the deferred collection step would never fire. Treatnullas "leave empty".
- Name
created_at- Type
- string
- Description
ISO 8601, or
nullon a document with no stored timestamp. Same forupdated_at.
Choices
min_selections, max_selections, available, and depends_on are enforced server-side. The same rules run on POST /api/v2/checkout and on order creation, so a submission that breaks any of them is rejected with 400.
- Name
selection_type- Type
- string
- Description
singleormulti.
- Name
min_selections- Type
- integer
- Description
Minimum options to select.
singlechoices report1. Formulti, the minimum is clamped down to the number of options currently visible and available, so a "pick at least 3" choice with only 2 valid options requires 2.
- Name
max_selections- Type
- integer
- Description
Maximum options to select, or
nullfor unbounded (again clamped to the visible option count).singlechoices reportnull; submit at most one option regardless.
- Name
required- Type
- boolean
- Description
When
true, the choice must be answered - unless it is currently hidden by its dependency or has no available options, in which case it is skipped.
- Name
default_option_id- Type
- string
- Description
The option the baker configured as the default, or
null. Also flagged asis_defaulton the option itself. It is not applied server-side: pricing resolves only the selections you send, so an optional choice you omit adds no upcharge, whether or not it has a default.
- Name
depends_on- Type
- object
- Description
null, or{ choice_id, option_mappings }.choice_idis another choice on this same product (already resolved to its product-level id, so it matches achoices[].iddirectly).option_mappingsmaps a parent option id to the array of this choice's option ids allowed for that pick. Semantics: this choice does not apply until the parent is answered; a parent option absent from the map allows all of this choice's options; a parent option mapped to an empty array means this choice is not offered for that pick. Dependencies are single-level - a child is never itself a parent.
- Name
options[].price_modifier- Type
- number
- Description
Upcharge added to the line when that option is chosen, resolved for the product's default variant. Amounts can differ per variant on size-laddered options, and order create resolves the applicable amount server-side from the variant on the line.
- Name
options[].available- Type
- boolean
- Description
falsemeans the option is disabled. Submitting it is rejected with400.
- Name
options[].is_default- Type
- boolean
- Description
Whether this option is flagged as the default.
- Name
options[].allergens- Type
- array
- Description
Allergens the option itself introduces, in the same
{ allergen, level }shape as the product list, always at levelcontains. They are not merged into the product-levelallergensarray - union them with it when the option is selected.
Submitting a selection for a choice that is hidden by its dependency, has no available options, or does not exist on the product is rejected, as are duplicate option ids within one choice.
Allergens
allergens is a single flat array of { allergen, level }, where level is one of contains, may_contain, or processed_in. Names are canonical catalog names, so an author's "Nuts" surfaces as "Tree Nuts". The array is the union of everything declared on the product, kept at its strictest declared level.
This is declared data. An empty array means nothing has been declared for the product, not that the product is free of allergens. Do not present it as an allergen-free claim, and remember to add the allergens carried by any selected choice options.
Request
curl https://au.api.ibakepro.com/api/v2/products/prod_ze9HI0w0LpVbemUI \
-H "Authorization: Bearer {api_key}"
Response
{
"id": "prod_ze9HI0w0LpVbemUI",
"name": "Chocolate Birthday Cake",
"description": "Rich chocolate sponge layered with ganache.",
"status": "active",
"slug": "chocolate-birthday-cake",
"images": [ "https://storage.googleapis.com/ibakepro-media/products/chocolate-birthday-cake.png" ],
"category": "Cakes",
"featured": false,
"taxable": true,
"allergens": [
{ "allergen": "Peanuts", "level": "contains" },
{ "allergen": "Sesame", "level": "may_contain" }
],
"starting_from": 85,
"pricing_currency": "AUD",
"has_variable_pricing": false,
"sale_unit": { "mode": "each", "label": "", "min": 1, "step": 1 },
"variants": [
{ "id": "base", "name": "Chocolate Birthday Cake", "price": 85, "servings": 12 }
],
"choices": [
{
"id": "cho_hoFEGdk3rJ2waVe3",
"name": "Flavour",
"required": true,
"selection_type": "single",
"min_selections": 1,
"max_selections": null,
"default_option_id": "opt_rep1BQgeu8dFF6FF",
"depends_on": null,
"options": [
{ "id": "opt_rep1BQgeu8dFF6FF", "name": "Classic chocolate", "price_modifier": 0,
"allergens": [], "available": true, "is_default": true },
{ "id": "opt_JiK6AYdC3lARutKE", "name": "Salted caramel", "price_modifier": 12,
"allergens": [], "available": true, "is_default": false }
]
},
{
"id": "cho_A2Mh6IsNeDR3JPCa",
"name": "Filling",
"required": false,
"selection_type": "multi",
"min_selections": 1,
"max_selections": 2,
"default_option_id": null,
"depends_on": {
"choice_id": "cho_hoFEGdk3rJ2waVe3",
"option_mappings": { "opt_rep1BQgeu8dFF6FF": [ "opt_B4TrgBOI2vmwQzKW" ] }
},
"options": [
{ "id": "opt_B4TrgBOI2vmwQzKW", "name": "Raspberry jam", "price_modifier": 0,
"allergens": [], "available": true, "is_default": false }
]
}
],
"custom_fields": [
{ "id": "7RAxjEhG0Y7mGUKzxa5Q", "name": "Icing colour", "type": "select",
"required": false,
"options": [
{ "value": "white", "label": "White", "price_modifier": 0 },
{ "value": "gold", "label": "Gold leaf", "price_modifier": 15 }
],
"default_value": null }
],
"fulfilment_methods": [ "pickup", "delivery" ],
"pickup_available": true,
"delivery_available": true,
"dispatch_available": false,
"created_at": "2026-06-01T12:34:10.143Z",
"updated_at": "2026-06-04T07:17:34.854Z"
}
For instant, type-as-you-go search from the browser, mint a scoped search token instead of paging this endpoint. See Product Search.