openapi: 3.1.0
info:
  title: Nucleo Delivery Promise API
  version: v1
  summary: Tell shoppers when an item or cart will arrive, before they buy.
  description: |
    The Delivery Promise API answers one question for your storefront: *if the shopper orders now,
    when will it arrive?* For a product or a whole cart, a destination country (and optionally a
    postcode) it returns, per shipping method, the order-by cutoff, the ship date, the delivery window,
    a ready-to-display sentence in the shopper's language and the stores where the items can be picked up.

    The promise is computed with the same pieces Nucleo uses to fulfil the real order: order routing
    and real stock per location, each location's working calendar, cutoffs and daily capacity, the
    service-level policies and carrier transit times. Merchandise not in stock but due in from a supplier
    is promised from its arrival date.

    It is called directly from the browser (product page, cart, checkout). Authentication is a
    **publishable store token** passed in the `token` query parameter; it is safe to embed in your theme.
  contact:
    name: Nucleo support
    url: https://nucleoplatform.com
x-nucleo:
  product: delivery-promise
  module: commerce
  audience: [storefront]
  stability: beta
  format: json
  order: 30
servers:
  - url: https://api-commerce.nucleoplatform.com/api/oms/v1
    description: Production
tags:
  - name: Delivery promise
    description: |
      Delivery dates and pickup options for a product or a cart, ready to show on the storefront.

      **Turning it on.** In Nucleo go to **Settings › Orders › Shipping and delivery › Delivery promise**.
      Switch the public API on, list the storefront origins allowed to call it, then copy the store token
      (`npk_…`) and the endpoint. The same page has a ready-made Shopify theme snippet and a preview
      ("try a SKU and a country") that also shows *why* (which location, cutoff and transit were used).

      **Rotating the token.** *Rotate token* on the same page issues a new token; the old one stops working
      immediately, so update your theme snippet at the same time.

      **Locking the promise on the order.** If the cart carries the attribute `_nucleo_promise`, Nucleo
      uses the date the shopper saw as the order's promised delivery date and measures how often it is kept
      (*promise accuracy* on the same settings page). The value is either a bare date (`2026-10-07`) or
      one date per method separated by `;` (`standard:2026-10-07;express:2026-10-06`); Nucleo picks the
      one matching the order's shipping method. The Shopify snippet does this for you with `/cart/update.js`.
paths:
  /public/promise:
    get:
      operationId: getDeliveryPromise
      summary: Get the delivery promise for a cart
      tags: [Delivery promise]
      security:
        - storeToken: []
      description: |
        Returns when the given product (`sku` + `qty`) or cart (`items`) arrives in `country`, for each
        shipping method (`standard`, `express`), plus up to three stores where it can be picked up today
        or on the next opening day.

        **How it is computed**
        - The fulfilling location is the one order routing would choose (a dry run, nothing is reserved);
          without routing rules, the first location fulfilling online orders that has everything
          (warehouses before stores), otherwise one location per line (split).
        - The ship day comes from that location's calendar (working days, holidays, closures, cutoff per
          method, preparation days), its daily capacity (a full day pushes the departure to the next working
          day) and the extra days of the matching service-level policy.
        - The delivery window adds the carrier's transit time (minimum and maximum, in working days of the
          destination country) plus the policy's buffer days.
        - Nothing in stock anywhere but stock due in within 120 days: the option is `incoming` and dates
          start from the arrival date.
        - If **any** SKU in the request is unknown to Nucleo, no shipping or pickup option is returned and
          `available` is `false`.

        **Pickup** lists stores in `country` with click & collect on and every item in stock, nearest first
        (by postcode prefix when `zip` is given), at most three. Pickup is omitted entirely if the merchant
        turned it off in the settings.

        **Messages** (`message` at top level and per option/pickup) are rendered at response time in `lang`,
        so the "order within" countdown is always current. Pickup times in messages use Italian time
        (Europe/Rome).

        **Caching.** The computed promise is cached server-side per store and request (country, postcode,
        channel, lines) for the number of seconds set by the merchant (default 60, 0 to 3600); the countdown
        and messages are still recomputed on every call. Responses carry `Cache-Control: public, max-age=30`.

        **CORS.** Callable from any browser origin. If the merchant set *allowed origins*, a request whose
        `Origin` header is not in the list is refused with `403 origin_not_allowed` (comparison is
        case-insensitive, trailing slash ignored). Requests without an `Origin` header (server-to-server)
        are not checked against the list.

        **Privacy.** The response never contains internal IDs, location names used for routing, carriers or
        the reasoning behind the dates.

        **Rate limits** (fixed one-minute windows):
        - 120 requests per minute per client IP (counted before the token is checked);
        - per store token: the merchant's limit (default 600 per minute, minimum 10).

        Over either limit the API answers `429` with a `Retry-After` header (seconds).

        This endpoint is read-only and idempotent.
      x-rate-limit:
        limit: 120
        window: 1m
        scope: per IP
      x-rate-limit-additional:
        - limit: 600
          window: 1m
          scope: per key
          note: Default; configurable by the merchant from 10 to 100000.
      parameters:
        - name: token
          in: query
          required: true
          description: |
            Publishable store token, `npk_{organization}_{40 alphanumerics}`. Copy it from
            **Settings › Orders › Shipping and delivery › Delivery promise**.
          schema:
            type: string
            pattern: '^npk_[0-9]{1,10}_[A-Za-z0-9]{40}$'
          example: npk_1042_Q9xT2bLm8RkV4sWz7yHn3cJd6fGp1aEu5oKi0MrX
        - name: country
          in: query
          required: true
          description: Destination country, ISO 3166-1 alpha-2 (letters only, case-insensitive).
          schema:
            type: string
            minLength: 2
            maxLength: 2
            pattern: '^[A-Za-z]{2}$'
          example: IT
        - name: sku
          in: query
          required: false
          description: |
            A single product variant SKU (max 120 characters). Use either `sku` (+ `qty`) or `items`;
            if `items` is present, `sku` and `qty` are ignored. One of the two is required.
          schema:
            type: string
            maxLength: 120
          example: TEE-BLK-M
        - name: qty
          in: query
          required: false
          description: Quantity for `sku`, 1 to 99. Defaults to 1.
          schema:
            type: integer
            minimum: 1
            maximum: 99
            default: 1
          example: 1
        - name: items
          in: query
          required: false
          description: |
            A cart: comma-separated `SKU:QUANTITY` pairs, 1 to 20 lines (quantity 1 to 99, defaults to 1
            when omitted). SKUs containing `,` or `:` cannot be expressed in this form.
          schema:
            type: string
          example: 'TEE-BLK-M:1,HOODIE-GRY-L:2'
        - name: zip
          in: query
          required: false
          description: |
            Destination postcode (max 12 characters: letters, digits, spaces, hyphens). Spaces are
            removed and the first 5 characters are used to rank pickup stores by proximity.
          schema:
            type: string
            maxLength: 12
            pattern: '^[A-Za-z0-9 -]*$'
          example: '20121'
        - name: channel
          in: query
          required: false
          description: |
            Sales channel the order would come from (lowercase letters, digits, `_`, `-`; max 30). It selects
            the routing rules, channel stock allocation and service-level policy. Defaults to the
            channel set in the merchant's settings (`shopify` unless changed).
          schema:
            type: string
            maxLength: 30
            pattern: '^[a-z0-9_-]*$'
          example: shopify
        - name: lang
          in: query
          required: false
          description: Language of the `message` fields. Anything else falls back to `en`.
          schema:
            type: string
            enum: [it, en, de, fr, es]
            default: en
          example: en
      responses:
        '200':
          description: The delivery promise.
          headers:
            Cache-Control:
              description: Always `public, max-age=30`.
              schema:
                type: string
              example: public, max-age=30
            Access-Control-Allow-Origin:
              $ref: '#/components/headers/AccessControlAllowOrigin'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Promise'
              examples:
                inStock:
                  summary: In stock, before today's cutoff, with pickup
                  value:
                    computed_at: '2026-10-05T09:46:00+00:00'
                    country: IT
                    items:
                      - sku: TEE-BLK-M
                        quantity: 1
                        known: true
                    available: true
                    options:
                      - method: standard
                        status: in_stock
                        available_from: null
                        order_by: '2026-10-05T12:00:00+00:00'
                        ships_on: '2026-10-05'
                        delivery_from: '2026-10-06'
                        delivery_to: '2026-10-07'
                        order_within_minutes: 134
                        message: Order within 2 h 14 min, get it between Tuesday, 6 October and Wednesday, 7 October
                      - method: express
                        status: in_stock
                        available_from: null
                        order_by: '2026-10-05T13:30:00+00:00'
                        ships_on: '2026-10-05'
                        delivery_from: '2026-10-06'
                        delivery_to: '2026-10-06'
                        order_within_minutes: 224
                        message: Order within 3 h 44 min, get it Tuesday, 6 October
                    pickup:
                      - location_code: MI01
                        name: Acme Apparel Milano
                        city: Milano
                        address: Via Roma 1
                        ready_at: '2026-10-05T14:00:00+00:00'
                        message: Pick up today at Acme Apparel Milano from 16:00
                    message: Order within 2 h 14 min, get it between Tuesday, 6 October and Wednesday, 7 October
                incoming:
                  summary: Out of stock, due in from a supplier
                  value:
                    computed_at: '2026-10-05T09:46:00+00:00'
                    country: DE
                    items:
                      - sku: HOODIE-GRY-L
                        quantity: 2
                        known: true
                    available: false
                    options:
                      - method: standard
                        status: incoming
                        available_from: '2026-10-12'
                        order_by: null
                        ships_on: '2026-10-12'
                        delivery_from: '2026-10-14'
                        delivery_to: '2026-10-16'
                        order_within_minutes: null
                        message: Available from Monday, 12 October, get it between Wednesday, 14 October and Friday, 16 October
                    pickup: []
                    message: Available from Monday, 12 October, get it between Wednesday, 14 October and Friday, 16 October
                unknownSku:
                  summary: An SKU Nucleo does not know
                  value:
                    computed_at: '2026-10-05T09:46:00+00:00'
                    country: IT
                    items:
                      - sku: TEE-BLK-M
                        quantity: 1
                        known: true
                      - sku: GIFT-CARD
                        quantity: 1
                        known: false
                    available: false
                    options: []
                    pickup: []
                    message: Currently unavailable
        '401':
          description: |
            `invalid_token`: the token is malformed, unknown, has been rotated, or the public API is switched
            off for this store.
          headers:
            Access-Control-Allow-Origin:
              $ref: '#/components/headers/AccessControlAllowOrigin'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalidToken:
                  value:
                    error: invalid_token
        '403':
          description: '`origin_not_allowed`: the request `Origin` is not among the allowed origins set by the merchant.'
          headers:
            Access-Control-Allow-Origin:
              $ref: '#/components/headers/AccessControlAllowOrigin'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                originNotAllowed:
                  value:
                    error: origin_not_allowed
        '422':
          description: |
            `invalid_request`: one or more parameters are invalid. `fields` lists the offending inputs
            (`country`, `zip`, `channel`, `lines` when no `sku`/`items` was given or more than 20 lines,
            `lines.{n}.sku`, `lines.{n}.quantity`).
          headers:
            Access-Control-Allow-Origin:
              $ref: '#/components/headers/AccessControlAllowOrigin'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                badCountry:
                  value:
                    error: invalid_request
                    fields: [country]
                badQuantity:
                  value:
                    error: invalid_request
                    fields: [lines.1.quantity]
        '429':
          description: '`rate_limited`: too many requests from this IP or for this store token.'
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
              example: 37
            Access-Control-Allow-Origin:
              $ref: '#/components/headers/AccessControlAllowOrigin'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                rateLimited:
                  value:
                    error: rate_limited
components:
  securitySchemes:
    storeToken:
      type: apiKey
      in: query
      name: token
      description: |
        Publishable store token (`npk_{organization}_{random}`). It identifies the store and is meant to live
        in storefront code; restrict who can use it with *allowed origins* and the per-token rate limit.
        Nucleo stores it encrypted and compares a hash. Get or rotate it in
        **Settings › Orders › Shipping and delivery › Delivery promise**.
  headers:
    AccessControlAllowOrigin:
      description: CORS header; `*` or the calling origin.
      schema:
        type: string
      example: https://shop.acme.example
  schemas:
    Promise:
      type: object
      required: [computed_at, country, items, available, options, pickup, message]
      properties:
        computed_at:
          type: string
          format: date-time
          description: When the dates were computed (may be up to the cache duration in the past).
        country:
          type: string
          description: Destination country, upper case.
          example: IT
        items:
          type: array
          description: The requested lines, in order.
          items:
            type: object
            required: [sku, quantity, known]
            properties:
              sku:
                type: [string, 'null']
                example: TEE-BLK-M
              quantity:
                type: integer
                minimum: 1
                maximum: 99
              known:
                type: boolean
                description: Whether Nucleo knows this SKU. One unknown SKU means no options at all.
        available:
          type: boolean
          description: '`true` if at least one shipping option is `in_stock` or at least one pickup store is listed.'
        options:
          type: array
          description: One entry per shipping method that could be evaluated, `standard` first.
          items:
            $ref: '#/components/schemas/ShippingOption'
        pickup:
          type: array
          maxItems: 3
          description: Stores where the whole cart can be collected, best first.
          items:
            $ref: '#/components/schemas/PickupOption'
        message:
          type: string
          description: The message of the first option that is not `unavailable`, or the "unavailable" sentence.
          example: Order within 2 h 14 min, get it between Tuesday, 6 October and Wednesday, 7 October
    ShippingOption:
      type: object
      required: [method, status, available_from, order_by, ships_on, delivery_from, delivery_to, order_within_minutes, message]
      properties:
        method:
          type: string
          enum: [standard, express]
        status:
          type: string
          enum: [in_stock, incoming, unavailable]
          description: |
            `in_stock` ships from current stock; `incoming` ships once stock due in arrives (`available_from`);
            `unavailable` cannot be promised (all date fields are `null`).
        available_from:
          type: [string, 'null']
          format: date
          description: For `incoming`, the date the stock is expected.
        order_by:
          type: [string, 'null']
          format: date-time
          description: Cutoff to beat to ship on `ships_on`. Only for `in_stock`.
        ships_on:
          type: [string, 'null']
          format: date
          description: Day the parcel leaves the location (the latest one when the cart is split).
        delivery_from:
          type: [string, 'null']
          format: date
          description: Earliest delivery day.
        delivery_to:
          type: [string, 'null']
          format: date
          description: Latest delivery day (includes the policy's buffer days). Store this in `_nucleo_promise`.
        order_within_minutes:
          type: [integer, 'null']
          description: Minutes left until `order_by`; `null` when the cutoff is past or not applicable.
        message:
          type: string
          description: Ready-to-display sentence in `lang`.
    PickupOption:
      type: object
      required: [location_code, name, city, address, ready_at, message]
      properties:
        location_code:
          type: string
          description: The store's code in Nucleo.
          example: MI01
        name:
          type: string
          example: Acme Apparel Milano
        city:
          type: [string, 'null']
          example: Milano
        address:
          type: [string, 'null']
          example: Via Roma 1
        ready_at:
          type: string
          format: date-time
          description: When the order would be ready for collection, from the store's opening hours and preparation time.
        message:
          type: string
          example: Pick up today at Acme Apparel Milano from 16:00
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          enum: [invalid_token, origin_not_allowed, invalid_request, rate_limited]
        fields:
          type: array
          description: Only for `invalid_request`.
          items:
            type: string
