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 option is 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 at one or more of those locations, against the
inventory_keyof each product being ordered. A product with noinventory_keyhas no stock to find, so it is markedNONErather than left unmarked, even when stock exists under a matching code.
Step 1, send the checkout request with inventory_check set
Set inventory_check to true, and identify every line item by product_id or
product_ref. Filtering is effective only when inventory_check is true, inventory management
is enabled for the tenant, and the line items resolve to catalog products. Miss any one and
the stock check is skipped, and the entries come back unmarked.
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. 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 each option's configured locations scope 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": 6 },
{ "line_item_id": "line-2", "quantity": 1, "availability": "FULL", "fulfillable_quantity": 3 }
],
"fulfillment_locations": [
{
"partner_location_id": "ACCOUNT_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": 2 },
{ "line_item_id": "line-2", "quantity": 1, "availability": "NONE", "fulfillable_quantity": 0 }
],
"fulfillment_locations": [
{
"partner_location_id": "ACCOUNT_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. On aFULLline it can exceedquantity: it is the most one of the entry's locations can deliver. Let the shopper raise the quantity up to it, but quote again after any cart change, because fees and options depend on the cart. 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, carry the chosen option into the order
The customer picks Standard Delivery, the only option that ships the whole basket.
Send the option's code so allocation knows which option was sold. To hold the order to
the location this quote proved has the stock, supply that location on a fulfillment order:
a fulfillment order that names a fulfillment_location is taken as given and skips the
allocation engine.
{
"merchant": "ACME",
"partner_order_reference": "YOUR_ORDER_REF",
"delivery_option": { "code": "STANDARD_DELIVERY" },
"line_items": [
{ "id": "line-1", "sku": "TSHIRT-BLK-M", "quantity": 1, "unit_price": 24.00 }
],
"fulfillment_orders": [
{
"partner_fulfillment_order_reference": "YOUR_ORDER_REF-1",
"fulfillment_location": { "partner_location_id": "ACCOUNT_d4f5a6b7-1111-4c8d-9e0f-1a2b3c4d5e6f" },
"delivery_method": "DELIVERY",
"line_items": [{ "id": "line-1", "quantity": 1 }]
}
]
// ...rest of the order create payload
}
There is no top-level fulfillment_locations field on order create. Sending one is
accepted and silently ignored, so the order allocates as if you had said nothing. Omit
fulfillment_orders instead if you want Carriyo to choose the location.
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.