Show a delivery promise on product listings
A delivery promise on a product listing ("Get it in 45 minutes", "Free next-day delivery")
turns a browsing shopper into a buyer. POST /storefront/product-delivery-options returns
the delivery options for each product in a list, so you can render the fastest one under each
card.
Unlike the cart endpoint, which evaluates one basket as a whole, this endpoint evaluates each product on its own, from its own fulfillment location. This recipe walks the three-step flow: find the nearest eligible locations for the customer, pick each product's location from your own stock data, then fetch the per-product options.
Scenario
A customer in London is browsing a product listing page with two products: a t-shirt and a pair of running shoes. You want a delivery promise under each card. You first find the nearest stores that can fulfill for this customer, decide which store each product ships from, then ask Carriyo for the delivery options per product and render the fastest.
Prerequisites
- API credentials: see Getting started for the one-time setup.
- Locations configured for the merchant, with fulfillment coverage (zones or radius) that reaches the customer.
- At least one delivery option configured for the merchant. Options priced
PASS_THROUGH(a live carrier quote) are not returned by this endpoint. - Your own stock data to decide which location each product ships from.
Step 1, find the nearest eligible locations
Call POST /storefront/locations with the customer's location to get the stores or warehouses
that can serve them, nearest first. Supply customer.coords so Carriyo can compute distance
and select the nearest.
curl -X POST 'https://api.carriyo.com/storefront/locations' \
-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",
"functions": ["FULFILLMENT"],
"location_type": "STORE",
"customer": {
"country": "GB",
"city": "London",
"area": "Marylebone",
"coords": [51.5194, -0.1547]
},
"max_distance": { "value": 25, "unit": "km" },
"max_locations": 10
}'
The response is a capped array of eligible locations, nearest first, with a distance on each:
[
{
"location_id": "loc-uk-marylebone",
"location_code": "STORE-UK-LON-01",
"location_name": "ACME Marylebone",
"location_type": "STORE",
"functions": ["FULFILLMENT", "COLLECTION"],
"address1": "12 Marylebone High Street",
"city": "London",
"country": "GB",
"postcode": "W1U 4RY",
"coords": [51.5194, -0.1547],
"distance": { "value": 0.3, "unit": "km" }
},
{
"location_id": "loc-uk-camden",
"location_code": "STORE-UK-LON-02",
"location_name": "ACME Camden",
"location_type": "STORE",
"functions": ["FULFILLMENT"],
"address1": "5 Camden High Street",
"city": "London",
"country": "GB",
"postcode": "NW1 7JE",
"coords": [51.5346, -0.1416],
"distance": { "value": 2.1, "unit": "km" }
}
]
For the eligibility rules (zones versus radius) and the sorting and paging options, see Find eligible locations.
Step 2, pick each product's fulfillment location
Carriyo returned the locations that can serve this customer. Now decide, from your own stock data, which location each product ships from. This is your call, not Carriyo's. You know your inventory.
Say the Marylebone store has the t-shirt in stock and Camden has the running shoes. You pair
each product with its partner_location_code:
| Product | Fulfillment location |
|---|---|
PDT7K2M9QX4RWJ8ZT (t-shirt) | STORE-UK-LON-01 (Marylebone) |
PDT3M6R9WK2QDXV5H (shoes) | STORE-UK-LON-02 (Camden) |
Those are Carriyo's own product_id values, as returned by the
Products API. You can send your own product_ref instead — see below.
Step 3, fetch the per-product delivery options
Call POST /storefront/product-delivery-options with the products and the location each ships
from. Set the location per product under products[].fulfillment_locations; anything you set
at the top level applies as the default. Set inventory_check to true to have each option
marked with an availability verdict and a fulfillable_quantity.
Identify each product by product_id or product_ref — one of the two is required. A
product that is not in Carriyo's catalog yet does not fail the request: the other products are
answered normally and the missing one comes back as an error row — "delivery_options": []
plus "error": { "code": "PRODUCT_NOT_FOUND", ... } — so only that product loses its promise.
curl -X POST 'https://api.carriyo.com/storefront/product-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",
"currency": "GBP",
"inventory_check": true,
"sort_by": "SPEED",
"sort_direction": "ASC",
"customer": {
"country": "GB",
"city": "London",
"area": "Marylebone",
"coords": [51.5194, -0.1547]
},
"products": [
{
"product_id": "PDT7K2M9QX4RWJ8ZT",
"quantity": 1,
"unit_price": 24.99,
"category": "apparel",
"fulfillment_locations": [
{ "partner_location_code": "STORE-UK-LON-01" }
]
},
{
"product_id": "PDT3M6R9WK2QDXV5H",
"quantity": 1,
"unit_price": 89.99,
"category": "footwear",
"weight": { "value": 0.9, "unit": "kg" },
"fulfillment_locations": [
{ "partner_location_code": "STORE-UK-LON-02" }
]
}
]
}'
The response is a bare array, one entry per requested product, carrying the requested
quantity and that product's delivery_options. Each option appears once, as a single
delivery fact: one availability verdict, one delivery window, and the
fulfillment_locations that can each on their own provide exactly that:
[
{
"product_id": "PDT7K2M9QX4RWJ8ZT",
"quantity": 1,
"delivery_options": [
{
"id": "opt-express-lon",
"delivery_method": "DELIVERY",
"code": "EXPRESS",
"name": "Express 45 min",
"description": "Delivered within the hour",
"image_url": "https://cdn.example.com/icons/express.png",
"carrier_account_id": "e7f8a9b0-1234-4c5d-8e6f-7a8b9c0d1e2f",
"shipping_fee": { "amount": 4.99, "currency": "GBP" },
"estimated_arrival_from": "2026-05-05T14:00:00.000Z",
"estimated_arrival_to": "2026-05-05T14:45:00.000Z",
"availability": "FULL",
"fulfillable_quantity": 1,
"fulfillment_locations": [
{
"partner_location_id": "loc-uk-marylebone",
"partner_location_code": "STORE-UK-LON-01",
"partner_location_name": "ACME Marylebone"
}
]
},
{
"id": "opt-std-lon",
"delivery_method": "DELIVERY",
"code": "STD",
"name": "Standard Delivery",
"description": "Delivered in 2-4 working days",
"carrier_account_id": "d6e7f8a9-5678-4b0c-9d1e-2f3a4b5c6d7e",
// A free option carries no `currency` unless the option configures a default one
"shipping_fee": { "amount": 0 },
"estimated_arrival_from": "2026-05-07T09:00:00.000Z",
"estimated_arrival_to": "2026-05-09T18:00:00.000Z",
"availability": "FULL",
"fulfillable_quantity": 1,
"fulfillment_locations": [
{
"partner_location_id": "loc-uk-marylebone",
"partner_location_code": "STORE-UK-LON-01",
"partner_location_name": "ACME Marylebone"
}
]
}
]
},
{
"product_id": "PDT3M6R9WK2QDXV5H",
"quantity": 1,
"delivery_options": [
{
"id": "opt-std-lon",
"delivery_method": "DELIVERY",
"code": "STD",
"name": "Standard Delivery",
"description": "Delivered in 2-4 working days",
"carrier_account_id": "d6e7f8a9-5678-4b0c-9d1e-2f3a4b5c6d7e",
"shipping_fee": { "amount": 3.99, "currency": "GBP" },
"estimated_arrival_from": "2026-05-07T09:00:00.000Z",
"estimated_arrival_to": "2026-05-09T18:00:00.000Z",
"availability": "FULL",
"fulfillable_quantity": 1,
"fulfillment_locations": [
{
"partner_location_id": "loc-uk-camden",
"partner_location_code": "STORE-UK-LON-02",
"partner_location_name": "ACME Camden"
}
]
}
]
}
]
Absolute stock counts are never returned. fulfillable_quantity is capped at the quantity you
asked for, so FULL with "quantity": 1 means "at least one", never "exactly one left".
Asking by your own reference instead
Swap product_id for product_ref and Carriyo resolves it against the catalog first, so the
options come back identical:
"products": [
{
"product_ref": "TSHIRT-BLK-M",
"quantity": 1,
"unit_price": 24.99,
"category": "apparel",
"fulfillment_locations": [{ "partner_location_code": "STORE-UK-LON-01" }]
}
]
Each response row then carries both — the resolved product_id and the product_ref you sent —
so you can map rows back to your own catalog.
If a product_ref (or product_id) matches nothing in Carriyo, the request still succeeds.
That product's row keeps its place with no options and an error, and has no product_id
when nothing could be resolved:
{
"product_ref": "HOODIE-GRY-L",
"quantity": 1,
"delivery_options": [],
"error": { "code": "PRODUCT_NOT_FOUND", "message": "No product found for product_ref HOODIE-GRY-L" }
}
Render such a row as "no promise available" — it was not evaluated, so it is not out of stock.
Step 4, render the fastest option per product
Each product's delivery_options is availability-banded first — FULL, then PARTIAL,
then UNKNOWN, then NONE — with sort_by: SPEED ordering within each band. So
[i].delivery_options[0] is the fastest option the product can actually be delivered by, not
merely the fastest option on paper. That is the delivery promise. For the t-shirt it's
Express 45 min; for the shoes it's standard delivery.
// Delivery promise per product card
"PDT7K2M9QX4RWJ8ZT" → "Get it in 45 minutes" // delivery_options[0] = Express 45 min
"PDT3M6R9WK2QDXV5H" → "Delivery in 2-4 days" // delivery_options[0] = Standard Delivery
The value each product carries into option conditions is unit_price × quantity. Because the
banding puts deliverable options first, reading [0] is safe on its own; filter on
availability only if you also want to exclude PARTIAL entries from the promise.
Pitfalls
- A missing product is an error row, not a
400and notNONE. One ofproduct_idorproduct_refis required. A product Carriyo's catalog does not hold comes back on its own row with"delivery_options": []anderror.code: PRODUCT_NOT_FOUND, while every other product is answered. Checkerrorbefore readingdelivery_options[0], and never render an error row as out of stock. - Locations are your decision. Carriyo returns the eligible locations; you pick which one each product ships from. This endpoint does not choose a location for you.
- Out-of-stock options are marked, not dropped. An option that can't meet the requested
quantity still appears, marked
availability: NONEwithfulfillable_quantity: 0. An option is absent only when it doesn't match the product's conditions or serves none of its fulfillment locations. - Products are kept even when empty. A product no option matches comes back with
"delivery_options": [], so every requested product is represented. Don't assume every entry has an option. - Availability marks need an effective stock check.
availabilityandfulfillable_quantityappear only wheninventory_checkistrueand inventory management is enabled for the tenant. Absent marks mean "not evaluated", never "unavailable" — andUNKNOWNmeans the stock read failed, which must never be rendered as out of stock. PASS_THROUGHoptions are omitted. Options priced from a live carrier quote don't appear on this endpoint. Use the cart delivery-options endpoint when you need those.- Caps apply. At most 50 products per request and 5 locations per product. A product may not be requested twice.