Filter delivery options by stock

Updated August 12, 20268 min read

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_check is 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

ApproachWhen 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"
  }
]
  • items lists every cart line, not just the ones the entry can provide. items[].line_item_id echoes the request line's id, quantity is what you asked for, and fulfillable_quantity is what this entry can give you. A line the entry can't provide is marked NONE with fulfillable_quantity: 0, never dropped.
  • availability on the entry is its verdict for the whole cart: FULL, PARTIAL, or UNKNOWN.
  • fulfillment_locations lists 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:

  • items echoes back all request line items that have a non-blank id, carrying no availability marks. An absent availability means the line was not evaluated, never that it is unavailable.
  • fulfillment_locations returns 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_check on and a product_id per 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_locations on the order keeps allocation inside a set you already know can fulfill the basket.

Pitfalls

  • inventory_check is the trigger. Leave it off (or drop product_id from the lines) and the stock check never runs, so you get every configured option unfiltered. If you expect filtering and don't see items reflecting stock, check that inventory_check is true, the product_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.
  • NONE and absent are different answers. NONE means evaluated and unavailable; an absent availability means 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-NONE items list. Don't write code that expects every configured option to appear.
  • fulfillment_locations are objects, not bare IDs. Each carries partner_location_id, partner_location_code, and partner_location_name; pass the partner_location_id back 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.