Filter delivery options by stock
A shopper should never pick a delivery choice the warehouse can't honor. Set
inventory_check to true on POST /storefront/delivery-options and Carriyo matches the
basket against real-time stock at each option's eligible locations. Every returned entry then
carries two extra fields: items (every cart line, each marked with what this entry can
provide) and fulfillment_locations (where that stock sits). An option that can't provide a
single line is dropped from the response entirely. This recipe covers the stock-aware
behavior; for the base endpoint walkthrough, see
Show delivery options at checkout.
Scenario
A customer in London is checking out two items: a camera and a laptop. Your storefront
calls Carriyo with both line items carrying a product_id. Carriyo checks real-time stock
at each delivery option's eligible locations and returns the options that can provide at
least one of the items, each annotated with what it can provide and from which locations.
You can either let Carriyo check all configured locations automatically, or pass specific
candidate locations to narrow the scope. Either way, you persist the chosen option together
with its fulfillment_locations on the order, so allocation is constrained to a
fulfillable set.
Prerequisites
- API credentials: see Getting started for the one-time setup.
- Inventory management enabled for the tenant. Without it, the stock check is skipped
even when
inventory_checkis set, and the endpoint behaves as in the base recipe. See Fallback below. - At least one delivery option configured for the merchant, with associated fulfillment locations.
- Stock recorded against those
product_ids at one or more of those locations.
Step 1, send the checkout request with inventory_check set
Set inventory_check to true, and give every line item a product_id (the stock key).
Filtering is effective only when all three hold: inventory_check is true, inventory
management is enabled for the tenant, and the line items carry product_id. Miss any one and
the stock check is skipped.
product_id is Carriyo's internal product id, as returned by the
Products API; send your own product_ref instead if you don't hold it. An id
matching no product is marked NONE, indistinguishable from genuinely out of stock.
You have two options for how locations are resolved:
Option A: let Carriyo find eligible locations
Omit fulfillment_locations from the request entirely. Carriyo checks stock at all
locations configured on each delivery option. This is the simpler integration: your
storefront doesn't need to know anything about warehouse topology. Just send the cart
with product references and Carriyo handles the rest.
curl -X POST 'https://api.carriyo.com/storefront/delivery-options' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'tenant-id: YOUR_TENANT_ID' \
-H 'x-api-key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"merchant": "ACME",
"inventory_check": true,
"delivery_methods": ["DELIVERY"],
"payment": {
"currency": "GBP",
"order_total": 763.74
},
"line_items": [
{
"id": "line-1",
"product_id": "PDT5N8T3JZ7QWM2XK",
"quantity": 1,
"weight": { "value": 0.5, "unit": "kg" }
},
{
"id": "line-2",
"product_id": "PDT9B4L6YH1PRD8VC",
"quantity": 1,
"weight": { "value": 1.5, "unit": "kg" }
}
],
"customer": {
"country": "GB",
"city": "London",
"address1": "45 Gloucester Place",
"postcode": "W1U 8HU",
"contact_name": "Oliver Bennett"
}
}'
In this case, if Standard Delivery is configured with locations WH-UK-01,
WH-UK-02, and STORE-UK-LON-01, Carriyo checks stock at all three and returns the ones
that hold inventory for the basket.
Option B: scope to specific locations
Pass fulfillment_locations when your storefront already knows which warehouses or stores
should be considered (e.g. from a prior availability check, or because the merchant only
ships from certain locations for this customer). The locations you supply narrow the whole
evaluation: which options match, which locations the stock read considers, and the location
lists in the response.
curl -X POST 'https://api.carriyo.com/storefront/delivery-options' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'tenant-id: YOUR_TENANT_ID' \
-H 'x-api-key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"merchant": "ACME",
"inventory_check": true,
"delivery_methods": ["DELIVERY"],
"payment": {
"currency": "GBP",
"order_total": 763.74
},
"line_items": [
{
"id": "line-1",
"product_id": "PDT5N8T3JZ7QWM2XK",
"quantity": 1,
"weight": { "value": 0.5, "unit": "kg" }
},
{
"id": "line-2",
"product_id": "PDT9B4L6YH1PRD8VC",
"quantity": 1,
"weight": { "value": 1.5, "unit": "kg" }
}
],
"customer": {
"country": "GB",
"city": "London",
"address1": "45 Gloucester Place",
"postcode": "W1U 8HU",
"contact_name": "Oliver Bennett"
},
"fulfillment_locations": [
{ "partner_location_code": "WH-UK-01" },
{ "partner_location_code": "STORE-UK-LON-01" }
]
}'
Here you're telling Carriyo: "only consider these two locations." If Standard Delivery
is configured with WH-UK-01, WH-UK-02, and STORE-UK-LON-01, only WH-UK-01 and
STORE-UK-LON-01 are checked for stock. An option whose configured locations have no overlap
with your supplied list is excluded entirely, and an option the supplied locations cannot
serve at all is omitted rather than returned empty. Pass a single location and an option's
presence means that location can serve this cart. An entry referencing a location Carriyo
can't resolve is rejected with 400, and the message names the offending entry.
Which to choose
| Approach | When to use |
|---|---|
| Option A (omit locations) | Your storefront doesn't track inventory sourcing. Let Carriyo's configured locations drive the check. Simpler integration. |
| Option B (pass locations) | You've already determined which locations can source the order (e.g. from your OMS or a prior inventory lookup). Narrows the scope for a more precise result. |
The response shape is identical in both cases.
Step 2, read the stock-filtered response
Each returned option now carries items and fulfillment_locations:
[
{
"id": "012694a1-04af-42fd-ba4d-beeaca2034ed",
"delivery_method": "DELIVERY",
"code": "STANDARD_DELIVERY",
"name": "Standard Delivery",
"carrier_account_id": "782b9b83-6d95-4468-8998-38f28b5a7c11",
"description": "Delivered in 2-4 working days.",
"shipping_fee": { "amount": 4.99, "currency": "GBP" },
"estimated_arrival_from": "2026-05-09T09:00:00.000Z",
"estimated_arrival_to": "2026-05-11T18:00:00.000Z",
"items": [
{ "line_item_id": "line-1", "quantity": 1, "availability": "FULL", "fulfillable_quantity": 1 },
{ "line_item_id": "line-2", "quantity": 1, "availability": "FULL", "fulfillable_quantity": 1 }
],
"fulfillment_locations": [
{
"partner_location_id": "d4f5a6b7-1111-4c8d-9e0f-1a2b3c4d5e6f",
"partner_location_code": "WH-UK-01",
"partner_location_name": "UK Central Warehouse"
}
],
"availability": "FULL"
},
{
"id": "f29acd11-34c2-4874-96f6-4a2530a82886",
"delivery_method": "DELIVERY",
"code": "EXPRESS_DELIVERY",
"name": "Express Delivery",
"carrier_account_id": "1ee1b219-d64e-4e67-8bc4-ff1c8c955457",
"description": "Next-day delivery across the UK.",
"shipping_fee": { "amount": 9.99, "currency": "GBP" },
"estimated_arrival_from": "2026-05-08T09:00:00.000Z",
"estimated_arrival_to": "2026-05-08T18:00:00.000Z",
"items": [
{ "line_item_id": "line-1", "quantity": 1, "availability": "FULL", "fulfillable_quantity": 1 },
{ "line_item_id": "line-2", "quantity": 1, "availability": "NONE", "fulfillable_quantity": 0 }
],
"fulfillment_locations": [
{
"partner_location_id": "f6a7c8d9-3333-4e0f-1a2b-3c4d5e6f7a8b",
"partner_location_code": "STORE-UK-LON-01",
"partner_location_name": "ACME London Store"
}
],
"availability": "PARTIAL"
}
]
itemslists every cart line, not just the ones the entry can provide.items[].line_item_idechoes the request line'sid,quantityis what you asked for, andfulfillable_quantityis what this entry can give you. A line the entry can't provide is markedNONEwithfulfillable_quantity: 0, never dropped.availabilityon the entry is its verdict for the whole cart:FULL,PARTIAL, orUNKNOWN.fulfillment_locationslists where that stock sits, each as a{partner_location_id, partner_location_code, partner_location_name}object. Every location listed can on its own provide everything the entry marks as available; the list is never a union of locations that each cover a different part of the basket.
Each entry is a single-delivery fact: what it marks as available ships together, in one delivery, from any one of its locations, at that entry's fee.
The partial-stock case
Express Delivery above is a partial-stock entry. Its only eligible location
(the London store, f6a7c8d9-3333-4e0f-1a2b-3c4d5e6f7a8b) stocks the camera (line-1) but
not the laptop (line-2), so line-2 comes back as NONE and the entry reads PARTIAL.
The option is still returned, and it can ship part of the basket. Render it accordingly (e.g.
"1 of 2 items ships express"), or offer the shopper a split delivery — see
Handle partial availability at checkout.
The no-stock case
An option whose eligible locations stock none of the requested lines is dropped from
the response entirely. It never comes back with an all-NONE items array. In this scenario,
if a third option (say SAME_DAY) had no stock for either line at any of its locations,
it wouldn't appear in the list above. Only Standard (both items) and Express
(camera only) survive the filter.
The one exception is a failed inventory read. Carriyo can't tell "out of stock" from "couldn't
check", so it fails open: the option is returned with availability: UNKNOWN rather than
hidden. Treat UNKNOWN as "no answer", never as out of stock.
Fallback, turn off inventory_check and skip the stock check
Leave inventory_check off (or omit product_id) and inventory filtering is skipped, so the
endpoint returns every configured option that passes the standard eligibility checks. The same
is true when inventory management is disabled for the tenant. In that mode:
itemsechoes back all request line items that have a non-blankid, carrying no availability marks. An absentavailabilitymeans the line was not evaluated, never that it is unavailable.fulfillment_locationsreturns each option's configured fulfillment locations as{partner_location_id, partner_location_code, partner_location_name}objects, or is omitted when the option has no configured locations.- No options are dropped on stock grounds.
curl -X POST 'https://api.carriyo.com/storefront/delivery-options' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'tenant-id: YOUR_TENANT_ID' \
-H 'x-api-key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"merchant": "ACME",
"delivery_methods": ["DELIVERY"],
"payment": { "currency": "GBP", "order_total": 763.74 },
"line_items": [
{ "id": "line-1", "quantity": 1, "weight": { "value": 0.5, "unit": "kg" } },
{ "id": "line-2", "quantity": 1, "weight": { "value": 1.5, "unit": "kg" } }
],
"customer": {
"country": "GB",
"city": "London",
"address1": "45 Gloucester Place",
"postcode": "W1U 8HU",
"contact_name": "Oliver Bennett"
}
}'
Existing integrations that don't send product_id keep working unchanged. The feature is
fully back-compatible.
Step 3, persist the chosen option and its fulfillment locations
The customer picks Standard Delivery, the only option that ships the whole basket.
Persist the option's code on the order so allocation knows which option was sold, and
constrain allocation to that option's fulfillment_locations so it can only pick a
location that actually holds the stock.
{
"merchant": "ACME",
"partner_order_reference": "YOUR_ORDER_REF",
"delivery_option": { "code": "STANDARD_DELIVERY" },
"fulfillment_locations": [
{ "partner_location_id": "d4f5a6b7-1111-4c8d-9e0f-1a2b3c4d5e6f" }
]
// ...rest of the order create payload
}
For the full order create call and its allocated response, including how Carriyo splits the order into fulfillment orders, follow Place an order with a delivery option. That recipe picks up exactly where this one leaves off.
What just happened
- You sent the cart with
inventory_checkon and aproduct_idper line, so Carriyo checked real-time stock at each option's eligible locations instead of returning every configured option blind. - Each returned entry marked every cart line with what it can provide (
items) and reported where the stock is (fulfillment_locations). Standard can ship both items from the central warehouse; Express can ship only the camera, from the London store. - Any option that couldn't provide a single line was dropped before you ever saw it.
- Persisting the chosen option plus its
fulfillment_locationson the order keeps allocation inside a set you already know can fulfill the basket.
Pitfalls
inventory_checkis the trigger. Leave it off (or dropproduct_idfrom the lines) and the stock check never runs, so you get every configured option unfiltered. If you expect filtering and don't seeitemsreflecting stock, check thatinventory_checkistrue, theproduct_ids are present, and inventory management is enabled.- Partial-stock options still return. An entry that can provide some lines comes back
marked
PARTIAL, not as an error. Decide in your UI whether to show it, hide it, or split the basket. NONEand absent are different answers.NONEmeans evaluated and unavailable; an absentavailabilitymeans not evaluated. Code that treats absence as unavailable hides every option from tenants that don't run inventory management.- No-stock options vanish silently. They're omitted, never returned with an all-
NONEitemslist. Don't write code that expects every configured option to appear. fulfillment_locationsare objects, not bare IDs. Each carriespartner_location_id,partner_location_code, andpartner_location_name; pass thepartner_location_idback on the order create. Every location listed can provide the entry's available items on its own. Persist whatever the chosen entry returned rather than recomputing it yourself.- Currency must match across the delivery-options request, the option, and the order create payload.