Show a delivery promise on product listings

Updated July 23, 20263 min read

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:

ProductFulfillment location
PROD-TS-001 (t-shirt)STORE-UK-LON-01 (Marylebone)
PROD-SH-002 (shoes)STORE-UK-LON-02 (Camden)

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 also get available_quantity per product.

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": "PROD-TS-001",
        "sku": "TSHIRT-BLK-M",
        "quantity": 1,
        "unit_price": 24.99,
        "category": "apparel",
        "fulfillment_locations": [
          { "partner_location_code": "STORE-UK-LON-01" }
        ]
      },
      {
        "product_id": "PROD-SH-002",
        "sku": "SHOES-RUN-42",
        "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 (product, resolved fulfillment location). Here each product resolves to a single location, so one entry per product. Each entry carries its resolved location and the delivery options sorted by sort_by:

[
  {
    "product_id": "PROD-TS-001",
    "quantity": 1,
    "location": {
      "partner_location_id": "loc-uk-marylebone",
      "partner_location_code": "STORE-UK-LON-01",
      "partner_location_name": "ACME Marylebone"
    },
    "available_quantity": 24,
    "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"
      },
      {
        "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": 0, "currency": "GBP" },
        "estimated_arrival_from": "2026-05-07T09:00:00.000Z",
        "estimated_arrival_to": "2026-05-09T18:00:00.000Z"
      }
    ]
  },
  {
    "product_id": "PROD-SH-002",
    "quantity": 1,
    "location": {
      "partner_location_id": "loc-uk-camden",
      "partner_location_code": "STORE-UK-LON-02",
      "partner_location_name": "ACME Camden"
    },
    "available_quantity": 6,
    "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"
      }
    ]
  }
]

Step 4, render the fastest option per product

sort_by: SPEED puts the fastest option first in each product's delivery_options, so the delivery promise is [i].delivery_options[0] on the returned array. For the t-shirt that's Express 45 min; for the shoes it's standard delivery.

// Delivery promise per product card
"PROD-TS-001" → "Get it in 45 minutes"      // delivery_options[0] = Express 45 min
"PROD-SH-002" → "Delivery in 2-4 days"      // delivery_options[0] = Standard Delivery

The value each product carries into option conditions is unit_price × quantity. The available_quantity field is present because inventory_check was effective. Use it to show low-stock nudges ("Only 6 left").

Pitfalls

  • 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.
  • Rows are kept even when empty. A product whose location can't offer any option (or can't meet the requested quantity under inventory_check) comes back with an empty delivery_options, so every requested product is represented in the response array. Don't assume every row has an option.
  • PASS_THROUGH options 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; duplicate product_id values are rejected.
  • available_quantity only when inventory_check is effective. It appears only when inventory_check is true and inventory management is enabled for the tenant.