Handle partial availability at checkout
POST /storefront/delivery-options answers one question per entry: what can be delivered
together, in one delivery, from one location, at that entry's fee. When a cart is
bigger than any single location can supply, the endpoint tells you so rather than quietly
promising a split. This recipe covers how to read that answer and how to ask for every
alternative. It then covers when to requote, and what the order allocation engine does with
your choice.
For the base walkthrough of the endpoint, start with Show delivery options at checkout.
Scenario
A customer in London is buying a camera and two laptops. The merchant fulfills from a
Manchester warehouse (WH-UK-01) and a London store (STORE-UK-LON-01). The camera is only
in Manchester; the laptops are only in London. No single location can ship the whole cart, so
your checkout has to either sell a partial cart or offer two deliveries.
Prerequisites
- API credentials: see Getting started for the one-time setup.
- Inventory management enabled for the tenant, with stock recorded against each
product_id. Without it the stock check is skipped and every entry comes back unmarked. - At least one delivery option configured for the merchant, with fulfillment locations attached.
- Each cart line identified by
product_idorproduct_ref. A line that resolves to nothing is tolerated here — it just gets no stock behavior.
Step 1, quote the cart with a stock check
Send the cart, the customer's location, and inventory_check: true. merchant and
line_items are required; a request without line items is rejected with 400.
Two request fields are worth setting from the start. customer.coords is what makes
radius-covered locations eligible at all, so send it whenever the merchant fulfills from
stores. sales_channel lets channel-scoped allocation rules match the same way they will
when you create the order.
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",
"sales_channel": "web",
"inventory_check": true,
"delivery_methods": ["DELIVERY"],
"payment": {
"currency": "GBP",
"order_total": 2439.97
},
"customer": {
"country": "GB",
"city": "London",
"address1": "45 Gloucester Place",
"postcode": "W1U 8HU",
"contact_name": "Oliver Bennett",
"coords": [51.5194, -0.158]
},
"line_items": [
{
"id": "line-1",
"product_id": "PDT5N8T3JZ7QWM2XK",
"quantity": 1,
"weight": { "value": 0.5, "unit": "kg" }
},
{
"id": "line-2",
"product_id": "PDT9B4L6YH1PRD8VC",
"quantity": 2,
"weight": { "value": 1.5, "unit": "kg" }
}
]
}'
The stock check runs only when three things hold: inventory_check is true, inventory
management is enabled for the tenant, and each line resolves to a product. Miss any one and
you get the unfiltered result described in
Show delivery options at checkout.
Step 2, read the availability marks
By default each option appears once: the entry with the best coverage. Carriyo picks it by most fully covered items, then most covered quantity, then the option's configured location order.
[
{
"id": "f29acd11-34c2-4874-96f6-4a2530a82886",
"delivery_method": "DELIVERY",
"code": "EXPRESS_DELIVERY",
"name": "Express Delivery",
"carrier_account_id": "1ee1b219-d64e-4e67-8bc4-ff1c8c955457",
"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": 2, "availability": "NONE", "fulfillable_quantity": 0 }
],
"fulfillment_locations": [
{
"partner_location_id": "d4f5a6b7-1111-4c8d-9e0f-1a2b3c4d5e6f",
"partner_location_code": "WH-UK-01",
"partner_location_name": "ACME Manchester Warehouse"
}
],
"availability": "PARTIAL"
},
{
"id": "012694a1-04af-42fd-ba4d-beeaca2034ed",
"delivery_method": "DELIVERY",
"code": "STANDARD_DELIVERY",
"name": "Standard Delivery",
"carrier_account_id": "782b9b83-6d95-4468-8998-38f28b5a7c11",
"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": "NONE", "fulfillable_quantity": 0 },
{ "line_item_id": "line-2", "quantity": 2, "availability": "FULL", "fulfillable_quantity": 2 }
],
"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"
}
]
Three rules make this readable.
- Every cart line is listed. Items an entry cannot supply come back as
NONEwithfulfillable_quantity: 0, never dropped.quantityis what you asked for;fulfillable_quantityis what this entry can give you. availabilityon the entry is the verdict for the whole cart —FULL,PARTIAL, orUNKNOWN. Both entries above readPARTIAL: each can ship one line and not the other.- Every location listed can do the job on its own.
fulfillment_locationsis never a union of locations that each cover a different piece. That is what makes each entry a single delivery.
Absence means something specific. An absent availability means the line or the entry
was not evaluated — never that it is unavailable. UNKNOWN means the check ran and could not
answer, because an inventory read failed. The option is returned rather than hidden, so treat
UNKNOWN as "no answer" and never as out of stock.
Entries are always availability-banded before anything else: entries that cover the whole
cart come first, then partial entries ordered by how many lines they can at least partly
supply. Within a band, sort_by applies (SPEED by default, COST for cheapest first).
Render the list in the order Carriyo returns it. Nothing here is FULL, so nothing is
promoted, and SPEED puts Express first.
Step 3, ask for every variation
The default response hid something: Standard Delivery can also ship the camera from
Manchester. Set include_variations: true to get every maximal single-delivery variation
of each option as its own entry — same code, different items and fulfillment_locations.
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",
"sales_channel": "web",
"inventory_check": true,
"include_variations": true,
"delivery_methods": ["DELIVERY"],
"payment": { "currency": "GBP", "order_total": 2439.97 },
"customer": {
"country": "GB",
"city": "London",
"address1": "45 Gloucester Place",
"postcode": "W1U 8HU",
"contact_name": "Oliver Bennett",
"coords": [51.5194, -0.158]
},
"line_items": [
{ "id": "line-1", "product_id": "PDT5N8T3JZ7QWM2XK", "quantity": 1, "weight": { "value": 0.5, "unit": "kg" } },
{ "id": "line-2", "product_id": "PDT9B4L6YH1PRD8VC", "quantity": 2, "weight": { "value": 1.5, "unit": "kg" } }
]
}'
Standard Delivery now returns twice. The Express entry is unchanged, so it is abbreviated here:
[
{ "code": "EXPRESS_DELIVERY", "availability": "PARTIAL" /* camera from WH-UK-01, as above */ },
{
"id": "012694a1-04af-42fd-ba4d-beeaca2034ed",
"delivery_method": "DELIVERY",
"code": "STANDARD_DELIVERY",
"name": "Standard Delivery",
"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": "NONE", "fulfillable_quantity": 0 },
{ "line_item_id": "line-2", "quantity": 2, "availability": "FULL", "fulfillable_quantity": 2 }
],
"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"
},
{
"id": "012694a1-04af-42fd-ba4d-beeaca2034ed",
"delivery_method": "DELIVERY",
"code": "STANDARD_DELIVERY",
"name": "Standard Delivery",
"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": 2, "availability": "NONE", "fulfillable_quantity": 0 }
],
"fulfillment_locations": [
{
"partner_location_id": "d4f5a6b7-1111-4c8d-9e0f-1a2b3c4d5e6f",
"partner_location_code": "WH-UK-01",
"partner_location_name": "ACME Manchester Warehouse"
}
],
"availability": "PARTIAL"
}
]
Two entries sharing a code are not duplicates. They are distinct single-delivery
alternatives of the same option. Group them by code in your UI and present them as delivery
groups: "laptops arrive 11 May from London, camera arrives 11 May from Manchester". Entries
whose coverage is contained in another entry's are dropped, so you never have to de-duplicate
them yourself. There is at most one entry per candidate location.
Composing several entries into a multi-delivery checkout is your explicit act. Carriyo never returns an entry that silently spans two locations.
Step 4, requote each delivery the shopper accepted
A quote is priced against the cart you sent. Once the shopper accepts a split, each
consignment is a smaller cart. Requote each subset with its own payment to get the fee you
will actually charge. Send only that subset's line items and pin the location with
fulfillment_locations.
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",
"sales_channel": "web",
"inventory_check": true,
"delivery_methods": ["DELIVERY"],
"payment": { "currency": "GBP", "order_total": 489.99 },
"customer": {
"country": "GB",
"city": "London",
"address1": "45 Gloucester Place",
"postcode": "W1U 8HU",
"contact_name": "Oliver Bennett",
"coords": [51.5194, -0.158]
},
"fulfillment_locations": [{ "partner_location_code": "WH-UK-01" }],
"line_items": [
{ "id": "line-1", "product_id": "PDT5N8T3JZ7QWM2XK", "quantity": 1, "weight": { "value": 0.5, "unit": "kg" } }
]
}'
The camera-only cart is worth £489.99, so a tiered fee lands on a different tier than the full basket did:
[
{
"code": "STANDARD_DELIVERY",
"name": "Standard Delivery",
"shipping_fee": { "amount": 6.99, "currency": "GBP" },
"items": [
{ "line_item_id": "line-1", "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": "ACME Manchester Warehouse"
}
],
"availability": "FULL"
}
]
Repeat for the laptop consignment. Beyond splits, requote whenever the answer could have moved:
- The cart changed: an item added, removed, or its quantity edited.
- The delivery address changed.
- Time passed on the checkout page. A quote reads stock, it does not reserve it, and fees can depend on working days and blackout days.
The endpoint is stateless, so requoting is free of side effects. Call it as often as your checkout needs.
What allocation decides, and what it does not
The quote is a read. It tells you what could ship together right now. Assigning items to locations happens later, when you create the order and the allocation engine runs.
Persist the chosen entry's code on the order, plus its fulfillment_locations, so
allocation is constrained to a set you already know holds stock:
{
"merchant": "ACME",
"partner_order_reference": "YOUR_ORDER_REF",
"sales_channel": "web",
"delivery_option": { "code": "STANDARD_DELIVERY" },
"fulfillment_locations": [
{ "partner_location_id": "f6a7c8d9-3333-4e0f-1a2b-3c4d5e6f7a8b" }
]
// ...rest of the order create payload
}
Two boundaries matter:
- Order splitting is an allocation setting, not a quote field. Whether Carriyo may split an order across locations is configured for the merchant in the Dashboard. It never changes what this endpoint returns. Variations are alternatives you compose; splitting is what allocation does after the order exists.
allocation_checkpreviews the routing rules. It defaults to the value ofinventory_check, so omitting it changes nothing: withinventory_check: truethe merchant's allocation rules are already applied on top of the stock read, and the cart sees the same routing decisions the order will. Setallocation_check: falsealongsideinventory_check: truefor a pure stock answer with no routing policy. Sendingallocation_check: truewithoutinventory_check: trueis rejected with400— the rules operate on the stock read, so the combination has no meaning.
Send sales_channel on both calls, and set it to the same value. Channel-scoped allocation
rules do not match when it is omitted, which is how a cart and its order end up disagreeing.
Items an option is not allowed to deliver
An option can be scoped per item with an item_product_categories condition — a chilled
same-day option that only carries chilled and frozen goods, for example. Items outside the
scope are marked availability: NONE with fulfillable_quantity: 0 in every entry of that
option.
This is the one exception to "absent means not evaluated". The mark appears without any stock
read, even when the stock check is off, and it overrides UNKNOWN. Excluded items count
toward the verdict, so a scoped option on a mixed cart reads PARTIAL. An option whose
condition excludes every item in the cart is not returned at all.
Configure the condition on the delivery option itself; see Configure delivery options.
Narrow the answer to one location
When your storefront already knows where the order should be sourced, pass a single entry in
fulfillment_locations. It narrows the whole evaluation: the stock read, the returned
location lists, and the location used for the fee and the arrival estimate all consider only
what you asked for. The option's presence in the response then means exactly one thing —
that location can serve this cart.
"fulfillment_locations": [{ "partner_location_code": "STORE-UK-LON-01" }]
Use partner_location_code (your own code) rather than partner_location_id (Carriyo's
internal ID); codes are stable across environments. An entry that references a location
Carriyo cannot resolve is rejected with 400. The message names the offending entry, for
example fulfillment_locations[1] ('XYZ') does not match any location.
Pitfalls
NONEand absent are different answers.NONEmeans evaluated and unavailable. An absentavailabilitymeans not evaluated. Code that treats absence as unavailable will hide every option from tenants that don't run inventory management.UNKNOWNis not out of stock. An inventory read failed. The option is returned rather than dropped precisely so you can still sell it; hiding it converts a transient failure into lost revenue.- Fees are quoted against the request you sent. A tiered fee on a
PARTIALentry is looked up against the whole cart'spayment.order_total, not the value of the subset that entry can supply. Requote each subset before you charge. - An empty
shipping_fee({}) is not free shipping. It means no price could be resolved — no tier matched, no pricing entry matched the currency, or a carrier rate lookup failed. Treat it as "no price available". - Radius-covered locations need
customer.coords. Without coordinates such a location is never eligible, and its stock is invisible to this endpoint. - Options that can supply nothing are omitted entirely. They never come back with an
all-
NONEitem list. The exception is a failed inventory read, which returns the option markedUNKNOWN. include_variationsneeds an effective stock check. Without one, each option yields a single entry anyway and the flag does nothing.