openapi: 3.1.0
info:
  title: Nucleo Commerce API
  version: v1
  summary: Read orders, returns, availability and locations from Nucleo Orders, and send in orders — for your ERP, BI, B2B and customer service tools.
  description: |
    The API for the systems that work **next to** Nucleo Orders: your ERP, your data warehouse, your customer
    service desk, your own apps — and for the ones that sell outside the sales channel (an ERP, a B2B portal, a
    smaller marketplace) and send their orders to Nucleo. One API key per integration: the key belongs to one
    company and only does what its permissions (scopes) allow.

    Warehouses that fulfil orders use the [Fulfillment API](/developers/reference/fulfillment) instead.

    **Conventions**

    - JSON in UTF-8, `snake_case` field names, dates in ISO 8601 UTC (`2026-10-13T08:30:00Z`).
    - Amounts are decimal strings with their currency: `{"amount": "180.00", "currency": "EUR"}`.
    - Lists are paginated with an opaque cursor: `limit` (1–200, default 50), answer `{data, next_cursor}`.
      Follow `next_cursor` until it is `null`. Lists are sorted by last update, oldest first: keep the
      `updated_at` of the last item you read and pass it as `updated_since` next time to read only what changed.
    - Every response carries `X-Request-Id`. Errors always have the same shape:
      `{"error": {"code", "message", "request_id", "details"}}`.
    - Order numbers are the merchant's (`#1042`). In paths you can omit the `#` or encode it as `%23`, or use
      the order `id`.
    - Reads never change anything. The only write is `POST /orders`, which creates an order and needs the
      `orders:write` scope; it accepts an `Idempotency-Key`.
    - Optional signed [webhooks](#tag-webhooks) tell you when an order or a return changes, so you read it
      right away instead of waiting for your next sync.
  contact:
    name: Nucleo support
    url: https://nucleoplatform.com
x-nucleo:
  product: commerce
  module: commerce
  audience: [server]
  stability: stable
  format: json
  order: 31
servers:
  - url: https://api-commerce.nucleoplatform.com/api/commerce/v1
    description: Production
security:
  - apiKey: []
tags:
  - name: Connection
    description: |
      Check that your API key works and see which company it belongs to and what it can read.

      **Getting a key.** In Nucleo, go to **Commerce › Orders › Channels and logistics**, click **New connection** and
      choose **Integration (Commerce API)** under *Integrations*. The endpoint, the API key (`nck_…`) and the
      webhook secret (`whsec_…`) are shown **once**: keep them in your secret store. A new connection is a draft and answers
      `403 connection_not_active`: in its **Status**, choose **Active** and save. **Rotate credentials** in the
      connection's **More actions** replaces the key and the webhook secret; the old ones stop working at once. A new key can use every
      read scope below; `orders:write` is never given by default: ask Nucleo support to enable it on your key.

      | Scope | Opens |
      | --- | --- |
      | `orders:read` | Orders, with shipments, refunds and timeline |
      | `returns:read` | Returns, with outcome and refunds |
      | `inventory:read` | Availability per item and location |
      | `locations:read` | Warehouses and stores |
      | `orders:write` | Create orders (`POST /orders`) — only on request |
  - name: Orders
    description: |
      Orders as the customer and your back office see them, with their lines, shipments, refunds made on the
      sales channel and the returns opened on them.

      | Status | Meaning |
      | --- | --- |
      | `received` | Arrived from the sales channel or through this API, not yet sent to fulfilment. |
      | `on_hold` | Stopped by the merchant or by a rule (for example a payment check). |
      | `in_fulfillment` | Assigned to a warehouse or store, which is preparing it. |
      | `shipped` | Every parcel has left. |
      | `delivered` | The carrier delivered it. |
      | `cancellation_requested` | Cancelled after the warehouse took it over: waiting for the warehouse to stop it. |
      | `cancelled` | Cancelled. |

      Fields that stay inside Nucleo — internal notes, carrier costs, routing decisions, holds' details,
      customer tags and payment transactions — are not part of the API.

      **Sending orders.** With `orders:write` your system creates orders with `POST /orders`. They go through
      the same steps as the sales channel's: Nucleo's routing rules choose the location, the warehouse
      receives them, and `order.created` is sent to every key that listens. Send orders ready to ship: they
      are treated as paid.
  - name: Returns
    description: |
      Returns come from the sales channel, where the customer opens them. Nucleo follows them to the warehouse
      and records what arrived and in which condition.

      | Status | Meaning |
      | --- | --- |
      | `requested` | Opened on the sales channel. |
      | `awaiting_receipt` | The warehouse knows it is coming. |
      | `in_transit` | The parcel is travelling back. |
      | `received` | Arrived at the warehouse, being checked. |
      | `compliant` | Every item arrived, in sellable condition. |
      | `not_compliant` | Something is missing or not sellable: the merchant's customer service is alerted. |
      | `closed` | Done. |
      | `cancelled` | Cancelled. |
  - name: Inventory
    description: |
      What you can sell now, per item and location: stock on hand, minus what is reserved for open orders and
      the safety stock, never below zero. With `channel`, the sales channel's allocation rules apply too: only
      the locations it sells from, minus the pieces held for other channels, within its caps. Pre-order
      quantities are not included.
  - name: Locations
    description: Warehouses and stores of the company, with the codes used in shipments and availability.
  - name: Webhooks
    description: |
      Nucleo can call your server when something changes. In the connection's **Settings**, write your
      **Webhook URL** (`https` only, on a public address) and save. Your key receives the events of its scopes:

      | Event | Scope | When |
      | --- | --- | --- |
      | `order.created` | `orders:read` | A new order arrives from the sales channel or through `POST /orders`, or the merchant creates a replacement order. |
      | `order.status_changed` | `orders:read` | The order's public status changes (internal steps that keep the same public status send nothing). |
      | `shipment.shipped` | `orders:read` | A shipment leaves: one event per shipment, also when an order ships in several parcels. |
      | `return.created` | `returns:read` | A return is opened, or the warehouse receives one nobody announced. |
      | `return.status_changed` | `returns:read` | The return's public status changes. |

      Webhooks are hints, not data: read the order or the return with the API to get its current state.
      Events can arrive out of order and, after a retry, more than once: skip an `id` you already handled.
      Keep your periodic sync with `updated_since` as a safety net. `GET /ping` tells whether webhooks are on
      and which events your key receives.
paths:
  /ping:
    get:
      operationId: ping
      summary: Check your API key
      tags: [Connection]
      description: |
        The company and connection your key belongs to, the scopes it has (`orders:write` only when enabled)
        and the webhooks it receives.
        Needs no scope.
      x-rate-limit: &rateLimit
        limit: 600
        window: 1m
        scope: per connection
        note: Default limit; ask Nucleo support if you need another one.
      responses:
        '200':
          description: The key works.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Ping'
              example:
                api_version: v1
                company:
                  id: 1042
                  name: Acme Apparel
                connection:
                  id: 6a1f2c3d-4e5b-4c6d-8e7f-9a0b1c2d3e4f
                  name: Acme ERP
                  status: active
                scopes: [orders:read, returns:read, inventory:read, locations:read]
                webhooks:
                  enabled: true
                  events: [order.created, order.status_changed, shipment.shipped, return.created, return.status_changed]
                server_time: '2026-10-13T08:30:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /orders:
    get:
      operationId: listOrders
      summary: List orders
      tags: [Orders]
      description: |
        Orders sorted by last update, oldest first. Scope `orders:read`.

        To keep a copy in sync, read every page once, then call again with `updated_since` set to the
        `updated_at` of the last order you stored. A change to a return does not update its order: sync returns
        with `GET /returns`.
      x-rate-limit: *rateLimit
      parameters:
        - name: status
          in: query
          description: Only orders in this status.
          schema:
            $ref: '#/components/schemas/OrderStatus'
        - $ref: '#/components/parameters/UpdatedSince'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: A page of orders.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderListPage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      operationId: createOrder
      summary: Create an order
      tags: [Orders]
      description: |
        Sends an order from your system to Nucleo. Scope `orders:write`.

        - **No duplicates.** `channel_order_ref` is your own id of the order. Sending the same
          `channel_order_ref` again with the same key creates nothing: the answer is `200` with the order
          created the first time, unchanged. The body is still validated, but its content is not applied.
          Use it to recover an answer you lost. References are per key: another key can use the same one.
        - **Number.** Nucleo shows the order as `API-` followed by your `number` (`B2B-871` becomes
          `API-B2B-871`), so it never clashes with the sales channel's numbers; read it with
          `GET /orders/API-B2B-871`. `number` may use letters, digits, `.`, `-` and `_`, at most 40 characters.
          Without it, `channel_order_ref` is used and must follow the same rule (`400 validation_failed` on
          `number` otherwise). A number another order already has answers `409 number_taken` — for example two
          keys sending the same `channel_order_ref` without a `number`.
        - **Items.** Every line needs the `sku` or the `barcode` of an item Nucleo knows, with a barcode;
          with both, they must be the same item. Unknown or mismatched items reject the whole order with
          `400 unknown_item`, one message per line in `details` (`lines.0`, `lines.1`, …).
        - **Amounts** are in EUR, tax included; `currency` can only be `EUR`. Every line needs its
          `tax_rate`, and `shipping.tax_rate` is required with `shipping.price`. Nucleo computes `subtotal`
          (sum of `unit_price × quantity`), `tax` (the tax of the lines and of the shipping), `total`
          (`subtotal` plus `shipping.price`); `discounts` is `0`: send prices already discounted.
        - **Dates.** `placed_at` can carry any offset; Nucleo stores and returns it in UTC.
        - **What happens next.** The order is treated as paid. Nucleo's routing rules choose the location.
          If no rule covers the order, or no location of its rule has all the items in stock, it stays
          `on_hold` until the merchant adds or fixes the rule and routes it again. `notes` reaches the
          merchant and the warehouse with the order. `order.created` is sent to every key that receives
          webhooks, yours included, always with `status: received`; the status it reaches next comes with
          `order.status_changed`.

        Orders created here cannot be changed or cancelled through the API yet: ask the merchant.
      x-rate-limit: *rateLimit
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCreate'
            example:
              channel_order_ref: SO-2026-00871
              number: B2B-871
              placed_at: '2026-10-13T10:12:00+02:00'
              customer:
                name: Anna Rossi
                email: buyer@example.com
              shipping_address:
                first_name: Anna
                last_name: Rossi
                company: Rossi Boutique
                address1: Via Emilia 120
                city: Modena
                postal_code: '41121'
                province_code: MO
                country_code: IT
                phone: '+39 059 000000'
              shipping:
                method_code: standard
                method_name: Standard
                price: '5.00'
                tax_rate: 22
              lines:
                - sku: TEE-BLK-M
                  quantity: 2
                  unit_price: '29.90'
                  tax_rate: 22
                - barcode: '8000000000017'
                  quantity: 1
                  unit_price: '59.00'
                  tax_rate: 22
      responses:
        '201':
          description: The order was created.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '200':
          description: An order with this `channel_order_ref` already exists for your key; it is returned unchanged.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /orders/{order}:
    get:
      operationId: getOrder
      summary: Get an order
      tags: [Orders]
      description: One order, by number or id. Scope `orders:read`.
      x-rate-limit: *rateLimit
      parameters:
        - $ref: '#/components/parameters/OrderRef'
      responses:
        '200':
          description: The order.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /orders/{order}/timeline:
    get:
      operationId: getOrderTimeline
      summary: Get an order's timeline
      tags: [Orders]
      description: |
        What happened to the order, oldest first: status changes, holds, address and line changes,
        shipments, returns and refunds. Internal events (routing, notes, customer care) are not listed.
        Scope `orders:read`.
      x-rate-limit: *rateLimit
      parameters:
        - $ref: '#/components/parameters/OrderRef'
      responses:
        '200':
          description: The order's public events.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TimelineEvent'
              example:
                data:
                  - type: order.received
                    from_status: null
                    to_status: null
                    occurred_at: '2026-10-12T09:14:00Z'
                  - type: order.status_changed
                    from_status: received
                    to_status: in_fulfillment
                    occurred_at: '2026-10-12T09:15:02Z'
                  - type: shipment.shipped
                    from_status: null
                    to_status: null
                    occurred_at: '2026-10-12T16:40:11Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /returns:
    get:
      operationId: listReturns
      summary: List returns
      tags: [Returns]
      description: Returns sorted by last update, oldest first. Scope `returns:read`.
      x-rate-limit: *rateLimit
      parameters:
        - name: status
          in: query
          description: Only returns in this status.
          schema:
            $ref: '#/components/schemas/ReturnStatus'
        - $ref: '#/components/parameters/UpdatedSince'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: A page of returns.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReturnListPage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /returns/{return}:
    get:
      operationId: getReturn
      summary: Get a return
      tags: [Returns]
      description: One return, by number or id. Scope `returns:read`.
      x-rate-limit: *rateLimit
      parameters:
        - name: return
          in: path
          required: true
          description: The return's number (as shown in Nucleo) or its `id`.
          schema:
            type: string
          example: R-1042-1
      responses:
        '200':
          description: The return.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Return'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /inventory:
    get:
      operationId: getInventory
      summary: Get availability
      tags: [Inventory]
      description: |
        Availability for up to 100 items per call, by SKU or barcode (or both). Only locations with stock or
        reservations for the item are listed; an SKU or barcode Nucleo does not know is left out of the answer.
        With `channel`, that sales channel's allocation rules apply. Scope `inventory:read`.
      x-rate-limit: *rateLimit
      parameters:
        - name: sku
          in: query
          description: Comma-separated SKUs.
          schema:
            type: string
          example: TEE-BLK-M,TEE-BLK-L
        - name: barcode
          in: query
          description: Comma-separated barcodes (EAN/UPC).
          schema:
            type: string
          example: '8056377587370'
        - name: channel
          in: query
          description: A sales channel type (for example `shopify`) or the id of a sales channel connection.
          schema:
            type: string
      responses:
        '200':
          description: Availability per item.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inventory'
              example:
                channel: null
                data:
                  - sku: TEE-BLK-M
                    barcode: '8056377587370'
                    available: 5
                    incoming: 0
                    locations:
                      - location_code: WH1
                        on_hand: 6
                        reserved: 1
                        available: 5
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /locations:
    get:
      operationId: listLocations
      summary: List locations
      tags: [Locations]
      description: Every warehouse and store of the company, by code. Scope `locations:read`.
      x-rate-limit: *rateLimit
      responses:
        '200':
          description: The locations.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Location'
              example:
                data:
                  - code: MI01
                    name: Milan flagship
                    kind: store
                    fulfills_online: true
                  - code: WH1
                    name: Main warehouse
                    kind: warehouse
                    fulfills_online: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
webhooks:
  order.created:
    post:
      operationId: orderCreatedWebhook
      summary: A new order arrived
      tags: [Webhooks]
      security:
        - webhookSignature: []
      description: |
        Sent when an order arrives from the sales channel or through `POST /orders` (also to the key that
        created it), or when the merchant creates a replacement order (for example to resend lost goods).
        Needs `orders:read`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEvent'
            example:
              id: 5b7e1c2d-3a4f-4b6c-8d9e-0f1a2b3c4d5e
              type: order.created
              created_at: '2026-10-13T08:29:51Z'
              data:
                order_id: 3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b
                order_number: '#1042'
                status: received
      responses:
        '200':
          $ref: '#/components/responses/WebhookAccepted'
  order.status_changed:
    post:
      operationId: orderStatusChangedWebhook
      summary: An order changed status
      tags: [Webhooks]
      security:
        - webhookSignature: []
      description: |
        Sent when the order's public status changes, with the new `status` and the `previous_status`. Needs
        `orders:read`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEvent'
            example:
              id: 8c9d0e1f-2a3b-4c4d-9e5f-6a7b8c9d0e1f
              type: order.status_changed
              created_at: '2026-10-13T16:02:14Z'
              data:
                order_id: 3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b
                order_number: '#1042'
                status: shipped
                previous_status: in_fulfillment
      responses:
        '200':
          $ref: '#/components/responses/WebhookAccepted'
  shipment.shipped:
    post:
      operationId: shipmentShippedWebhook
      summary: A shipment left
      tags: [Webhooks]
      security:
        - webhookSignature: []
      description: |
        Sent for each shipment that leaves, also when the order ships in several parcels from different
        locations. `shipment_id` is the `id` of one of the order's `shipments`, with carrier and tracking.
        An order the customer collects in a store sends no `shipment.shipped`: it goes straight to `delivered`.
        Needs `orders:read`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEvent'
            example:
              id: 1d2e3f4a-5b6c-4d7e-8f9a-0b1c2d3e4f5a
              type: shipment.shipped
              created_at: '2026-10-13T16:02:15Z'
              data:
                order_id: 3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b
                order_number: '#1042'
                shipment_id: 7c6b5a4f-3e2d-4c1b-9a0f-8e7d6c5b4a3f
      responses:
        '200':
          $ref: '#/components/responses/WebhookAccepted'
  return.created:
    post:
      operationId: returnCreatedWebhook
      summary: A return was opened
      tags: [Webhooks]
      security:
        - webhookSignature: []
      description: |
        Sent when a return is opened on the sales channel, or when the warehouse receives a return nobody
        announced. Needs `returns:read`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEvent'
            example:
              id: 0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d
              type: return.created
              created_at: '2026-10-18T16:20:03Z'
              data:
                return_id: 9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b
                return_number: R1042-1
                order_id: 3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b
                order_number: '#1042'
                status: requested
      responses:
        '200':
          $ref: '#/components/responses/WebhookAccepted'
  return.status_changed:
    post:
      operationId: returnStatusChangedWebhook
      summary: A return changed status
      tags: [Webhooks]
      security:
        - webhookSignature: []
      description: |
        Sent when the return's public status changes, with the new `status` and the `previous_status`. Needs
        `returns:read`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEvent'
            example:
              id: 2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e
              type: return.status_changed
              created_at: '2026-10-21T09:41:27Z'
              data:
                return_id: 9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b
                return_number: R1042-1
                order_id: 3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b
                order_number: '#1042'
                status: compliant
                previous_status: received
      responses:
        '200':
          $ref: '#/components/responses/WebhookAccepted'
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: 'The connection''s API key (`nck_…`): `Authorization: Bearer <api_key>`.'
    webhookSignature:
      type: apiKey
      in: header
      name: X-Nucleo-Signature
      description: |
        Webhooks Nucleo sends to your webhook URL. `X-Nucleo-Signature: t=<unix time>,v1=<signature>`,
        where the signature is the lowercase hex HMAC-SHA256 of `<t>.<raw body>` keyed with the connection's
        webhook secret (`whsec_…`). Verify it on the exact bytes you received, in constant time, and reject a
        `t` more than 5 minutes away from your clock. The event type is also in `X-Nucleo-Event`.
  parameters:
    OrderRef:
      name: order
      in: path
      required: true
      description: The order number (`1042`, `%231042`) or the order `id`.
      schema:
        type: string
      example: '1042'
    Cursor:
      name: cursor
      in: query
      description: The `next_cursor` of the previous page. Keep `status` and `updated_since` the same; a different `status` answers `400 invalid_cursor`.
      schema:
        type: string
    Limit:
      name: limit
      in: query
      description: Items per page.
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    UpdatedSince:
      name: updated_since
      in: query
      description: Only items updated at or after this time (ISO 8601). Encode `+` as `%2B` if you send an offset.
      schema:
        type: string
        format: date-time
      example: '2026-10-13T08:00:00Z'
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        Any unique string you generate (1–255 printable ASCII characters, no spaces), for example a UUID.
        For 48 hours, the same key with the same body returns the stored answer without doing the work again
        (header `Idempotent-Replayed: true`; the replayed body is the original one). The same key with a
        different body answers `409 idempotency_key_reused`; while the first request is still running,
        `409 idempotency_key_in_progress`. A key left in progress for more than 2 minutes (an interrupted
        call) is free again. Keys are per connection. The body is compared byte by byte: resend exactly
        the same bytes. Server errors (`5xx`) and `429` are not stored: retry them with the same key. Other
        errors (`4xx`) are stored too: after fixing the request, send it with a new key. Without a key,
        `channel_order_ref` still prevents duplicates.
      schema:
        type: string
        maxLength: 255
  headers:
    XRequestId:
      description: The id of this call. Quote it if you contact support; errors repeat it as `request_id`.
      schema:
        type: string
    RetryAfter:
      description: Seconds to wait before the next call.
      schema:
        type: integer
  responses:
    WebhookAccepted:
      description: |
        Answer with any `2xx` within 10 seconds; the body is ignored. Any other answer, a redirect or a
        timeout is retried with exponential backoff (after about 30 seconds, then 1, 2, 4, 8, 16 and 32
        minutes): 8 attempts in about an hour, then Nucleo gives up and alerts the merchant. Every attempt
        carries the same `id` (also in `X-Nucleo-Webhook-Id`) and its number in `X-Nucleo-Attempt`.
        While the connection is paused or still a draft no webhook is sent, and the events of that time are
        not sent later: catch up with `updated_since`.
    BadRequest:
      description: |
        The request is not valid: `validation_failed` (with `details` per parameter or field), `invalid_cursor`,
        `invalid_json` (the body is not a JSON object) or `unknown_item` (with `details` per line, for
        example `lines.0`).
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: validation_failed
              message: The request is not valid.
              request_id: 1f7e3c2a-9b8d-4e6f-a5c4-3b2a1d0e9f8c
              details:
                status: ['Must be one of: received, on_hold, in_fulfillment, shipped, delivered, cancellation_requested, cancelled.']
    Unauthorized:
      description: '`unauthorized`: the key is missing or wrong, or the call comes from an IP outside the connection''s allowlist.'
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: unauthorized
              message: Missing or invalid API key.
              request_id: 1f7e3c2a-9b8d-4e6f-a5c4-3b2a1d0e9f8c
    Forbidden:
      description: |
        The key is right but cannot do this: `insufficient_scope` (the key lacks the scope of the call),
        `connection_paused` or `connection_not_active` (the connection is paused or not turned on yet).
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: insufficient_scope
              message: This API key does not have the orders:read scope.
              request_id: 1f7e3c2a-9b8d-4e6f-a5c4-3b2a1d0e9f8c
    PayloadTooLarge:
      description: The body is larger than 5 MB (`payload_too_large`). Nothing was read.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Conflict:
      description: |
        `number_taken` (another order has this `number`), `idempotency_key_reused` or
        `idempotency_key_in_progress` (with `Retry-After`).
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: number_taken
              message: 'Another order is already numbered API-B2B-871: send a different number.'
              request_id: 1f7e3c2a-9b8d-4e6f-a5c4-3b2a1d0e9f8c
    NotFound:
      description: '`order_not_found` or `return_not_found`.'
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: order_not_found
              message: Order 1042 not found.
              request_id: 1f7e3c2a-9b8d-4e6f-a5c4-3b2a1d0e9f8c
    TooManyRequests:
      description: '`rate_limited`: wait for the seconds in `Retry-After`.'
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: rate_limited
              message: 'Too many requests: retry after the number of seconds in Retry-After.'
              request_id: 1f7e3c2a-9b8d-4e6f-a5c4-3b2a1d0e9f8c
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code:
              type: string
              description: Machine-readable code. Branch on this, never on `message`.
            message:
              type: string
            request_id:
              type: string
            details:
              type: object
              description: For `validation_failed`, the messages per parameter.
              additionalProperties:
                type: array
                items:
                  type: string
    Money:
      type: object
      required: [amount, currency]
      properties:
        amount:
          type: string
          description: Decimal string with two decimals.
          example: '180.00'
        currency:
          type: string
          description: ISO 4217 code.
          example: EUR
    Ping:
      type: object
      required: [api_version, company, connection, scopes, webhooks, server_time]
      properties:
        api_version:
          type: string
        company:
          type: object
          properties:
            id:
              type: integer
            name:
              type: [string, 'null']
        connection:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
            status:
              type: string
        scopes:
          type: array
          items:
            type: string
            enum: [orders:read, returns:read, inventory:read, locations:read, orders:write]
        webhooks:
          type: object
          required: [enabled, events]
          properties:
            enabled:
              type: boolean
              description: |
                A webhook URL is set and the key has a webhook secret. A key created before webhooks existed
                has no secret: rotate its credentials to get one.
            events:
              type: array
              description: The events this key receives (empty when webhooks are off).
              items:
                $ref: '#/components/schemas/WebhookEventType'
        server_time:
          type: string
          format: date-time
    OrderStatus:
      type: string
      enum: [received, on_hold, in_fulfillment, shipped, delivered, cancellation_requested, cancelled]
    ReturnStatus:
      type: string
      enum: [requested, awaiting_receipt, in_transit, received, compliant, not_compliant, closed, cancelled]
    OrderListPage:
      type: object
      required: [data, next_cursor]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Order'
        next_cursor:
          type: [string, 'null']
    ReturnListPage:
      type: object
      required: [data, next_cursor]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Return'
        next_cursor:
          type: [string, 'null']
    Address:
      type: object
      properties:
        first_name:
          type: [string, 'null']
        last_name:
          type: [string, 'null']
        company:
          type: [string, 'null']
        address1:
          type: [string, 'null']
        address2:
          type: [string, 'null']
        city:
          type: [string, 'null']
        postal_code:
          type: [string, 'null']
        province_code:
          type: [string, 'null']
        country_code:
          type: [string, 'null']
          description: ISO 3166-1 alpha-2.
        phone:
          type: [string, 'null']
    Order:
      type: object
      required: [id, number, status, lines]
      properties:
        id:
          type: string
          format: uuid
        number:
          type: string
          example: '#1042'
        status:
          $ref: '#/components/schemas/OrderStatus'
        channel:
          type: [string, 'null']
          description: Where the order comes from, for example `shopify`; `nucleo_api` for orders created with `POST /orders`.
        placed_at:
          type: [string, 'null']
          format: date-time
        updated_at:
          type: [string, 'null']
          format: date-time
        payment_status:
          type: [string, 'null']
          enum: [pending, authorized, paid, captured, partially_refunded, refunded, voided, expired, null]
        on_hold:
          type: boolean
        customer:
          type: object
          properties:
            name:
              type: [string, 'null']
            email:
              type: [string, 'null']
            phone:
              type: [string, 'null']
        shipping_address:
          $ref: '#/components/schemas/Address'
        billing_address:
          oneOf:
            - $ref: '#/components/schemas/Address'
            - type: 'null'
        shipping:
          type: object
          description: The shipping method the customer chose.
          properties:
            method_code:
              type: [string, 'null']
            method_name:
              type: [string, 'null']
        currency:
          type: string
        totals:
          type: object
          description: '`current_total` is the total after edits and cancellations; `refunded` what was refunded on the sales channel.'
          properties:
            subtotal:
              $ref: '#/components/schemas/Money'
            discounts:
              $ref: '#/components/schemas/Money'
            shipping:
              $ref: '#/components/schemas/Money'
            tax:
              $ref: '#/components/schemas/Money'
            total:
              $ref: '#/components/schemas/Money'
            current_total:
              $ref: '#/components/schemas/Money'
            refunded:
              $ref: '#/components/schemas/Money'
        lines:
          type: array
          items:
            $ref: '#/components/schemas/OrderLine'
        shipments:
          type: array
          items:
            $ref: '#/components/schemas/Shipment'
        refunds:
          type: array
          items:
            $ref: '#/components/schemas/Refund'
        returns:
          type: array
          description: The returns opened on the order; read each one with `GET /returns/{return}`.
          items:
            type: object
            properties:
              id:
                type: string
              number:
                type: [string, 'null']
              status:
                $ref: '#/components/schemas/ReturnStatus'
        cancellation:
          description: Present when the order is cancelled or its cancellation is requested.
          oneOf:
            - type: object
              properties:
                reason:
                  type: [string, 'null']
                cancelled_at:
                  type: [string, 'null']
                  format: date-time
            - type: 'null'
    AddressInput:
      type: object
      required: [address1, city, country_code]
      properties:
        first_name:
          type: string
          maxLength: 120
        last_name:
          type: string
          maxLength: 120
        company:
          type: string
          maxLength: 120
        address1:
          type: string
          maxLength: 255
        address2:
          type: string
          maxLength: 255
        city:
          type: string
          maxLength: 120
        postal_code:
          type: string
          maxLength: 20
        province_code:
          type: string
          maxLength: 10
        country_code:
          type: string
          description: ISO 3166-1 alpha-2 (`IT`; lowercase is accepted).
          minLength: 2
          maxLength: 2
        phone:
          type: string
          maxLength: 40
    OrderCreate:
      type: object
      required: [channel_order_ref, customer, shipping_address, lines]
      properties:
        channel_order_ref:
          type: string
          maxLength: 120
          pattern: '^[!-~]+$'
          description: Your id of the order (printable ASCII, no spaces). Unique per key; sending it again returns the existing order.
        number:
          type: string
          maxLength: 40
          pattern: '^[A-Za-z0-9][A-Za-z0-9._-]*$'
          description: Shown in Nucleo and to the warehouse as `API-` plus this value. Default `channel_order_ref`, which must then follow the same pattern.
        placed_at:
          type: string
          format: date-time
          description: When the customer placed it, with any offset; stored in UTC. Default now.
        currency:
          type: string
          enum: [EUR]
          description: Only `EUR`. Default `EUR`.
        customer:
          type: object
          required: [email]
          properties:
            name:
              type: string
              maxLength: 255
              description: Default the shipping address's first and last name.
            email:
              type: string
              format: email
              maxLength: 255
            phone:
              type: string
              maxLength: 40
              description: Default the shipping address's phone.
        shipping_address:
          $ref: '#/components/schemas/AddressInput'
        billing_address:
          $ref: '#/components/schemas/AddressInput'
        shipping:
          type: object
          properties:
            method_code:
              type: string
              maxLength: 120
              description: The shipping method, as configured in the merchant's carrier rules.
            method_name:
              type: string
              maxLength: 255
            price:
              type: [string, number]
              minimum: 0
              maximum: 100000
              description: Shipping price in EUR, tax included. Default `0`.
            tax_rate:
              type: number
              minimum: 0
              maximum: 100
              description: Tax percentage of the shipping, for example `22`. Required with `price`.
        notes:
          type: string
          maxLength: 2000
          description: A note for the merchant and the warehouse; it is not returned by the API.
        lines:
          type: array
          minItems: 1
          maxItems: 250
          items:
            type: object
            required: [quantity, unit_price, tax_rate]
            description: '`sku` or `barcode` is required; with both, they must be the same item.'
            properties:
              sku:
                type: string
                maxLength: 255
              barcode:
                type: string
                maxLength: 255
              title:
                type: string
                maxLength: 255
                description: Default the item's title in Nucleo.
              quantity:
                type: integer
                minimum: 1
                maximum: 100000
              unit_price:
                type: [string, number]
                minimum: 0
                maximum: 1000000
                description: Price of one piece in EUR, tax included, after discounts.
              tax_rate:
                type: number
                minimum: 0
                maximum: 100
                description: Tax percentage, for example `22`.
    OrderLine:
      type: object
      properties:
        line_id:
          type: string
        sku:
          type: [string, 'null']
        barcode:
          type: [string, 'null']
        title:
          type: [string, 'null']
        quantity:
          type: integer
        quantity_cancelled:
          type: integer
        quantity_shipped:
          type: integer
        unit_price:
          description: Tax included.
          $ref: '#/components/schemas/Money'
        total:
          description: Tax included.
          $ref: '#/components/schemas/Money'
        tax_rate:
          type: string
          description: Percentage as a decimal string, for example `22.00`.
    Shipment:
      type: object
      properties:
        id:
          type: string
        status:
          type: string
          enum: [label_created, confirmed, shipped, delivered, voided]
        location_code:
          type: [string, 'null']
          description: The location that shipped it.
        carrier:
          type: object
          properties:
            code:
              type: [string, 'null']
            name:
              type: [string, 'null']
            service:
              type: [string, 'null']
        tracking_url:
          type: [string, 'null']
        tracking_numbers:
          type: array
          items:
            type: string
        shipped_at:
          type: [string, 'null']
          format: date-time
        delivered_at:
          type: [string, 'null']
          format: date-time
        lines:
          type: array
          items:
            type: object
            properties:
              line_id:
                type: [string, 'null']
              quantity:
                type: integer
    Refund:
      type: object
      description: A refund made on the sales channel.
      properties:
        id:
          type: string
        return_id:
          type: [string, 'null']
          description: The return it belongs to, if any.
        refunded_at:
          type: [string, 'null']
          format: date-time
        total:
          $ref: '#/components/schemas/Money'
        shipping:
          $ref: '#/components/schemas/Money'
        lines:
          type: array
          items:
            type: object
            properties:
              line_id:
                type: [string, 'null']
              sku:
                type: [string, 'null']
              barcode:
                type: [string, 'null']
              quantity:
                type: integer
              subtotal:
                $ref: '#/components/schemas/Money'
    Return:
      type: object
      required: [id, status, lines]
      properties:
        id:
          type: string
        number:
          type: [string, 'null']
        order_id:
          type: [string, 'null']
        order_number:
          type: [string, 'null']
        status:
          $ref: '#/components/schemas/ReturnStatus'
        reason_code:
          type: [string, 'null']
        reason_text:
          type: [string, 'null']
        customer_note:
          type: [string, 'null']
        currency:
          type: string
        total:
          $ref: '#/components/schemas/Money'
        created_at:
          type: [string, 'null']
          format: date-time
        updated_at:
          type: [string, 'null']
          format: date-time
        received_at:
          type: [string, 'null']
          format: date-time
        closed_at:
          type: [string, 'null']
          format: date-time
        cancelled_at:
          type: [string, 'null']
          format: date-time
        lines:
          type: array
          items:
            type: object
            properties:
              sku:
                type: [string, 'null']
              barcode:
                type: [string, 'null']
              title:
                type: [string, 'null']
              quantity_requested:
                type: integer
              quantity_received:
                type: integer
              reason_code:
                type: [string, 'null']
              outcome:
                type: [string, 'null']
                description: The condition the warehouse found — `sellable`, `damaged`, or `mixed` when the pieces differ; `null` until checked.
        refunds:
          type: array
          items:
            $ref: '#/components/schemas/Refund'
    TimelineEvent:
      type: object
      properties:
        type:
          type: string
          enum:
            - order.received
            - order.status_changed
            - order.hold_placed
            - order.hold_released
            - order.address_changed
            - order.shipping_method_changed
            - order.lines_changed
            - order.lines_cancelled
            - order.item_replaced
            - order.reship_created
            - order.refunded_on_channel
            - shipment.confirmed
            - shipment.shipped
            - shipment.tracking_updated
            - shipment.voided
            - return.requested
            - return.status_changed
            - store.ready_for_pickup
            - store.collected
        from_status:
          type: [string, 'null']
          description: For status changes, the public status before (an order status, or a return status for `return.status_changed`).
        to_status:
          type: [string, 'null']
        occurred_at:
          type: [string, 'null']
          format: date-time
    Inventory:
      type: object
      required: [channel, data]
      properties:
        channel:
          type: [string, 'null']
        data:
          type: array
          items:
            type: object
            properties:
              sku:
                type: [string, 'null']
              barcode:
                type: string
              available:
                type: integer
                description: Sum of `available` over the listed locations.
              incoming:
                type: integer
                description: Pieces on inbound shipments expected in the next 365 days, not yet received.
              locations:
                type: array
                items:
                  type: object
                  properties:
                    location_code:
                      type: [string, 'null']
                    on_hand:
                      type: integer
                    reserved:
                      type: integer
                      description: Held for open orders.
                    available:
                      type: integer
    Location:
      type: object
      properties:
        code:
          type: string
        name:
          type: string
        kind:
          type: string
          enum: [warehouse, store, marketplace]
        fulfills_online:
          type: boolean
          description: Whether it ships online orders.
    WebhookEventType:
      type: string
      enum: [order.created, order.status_changed, shipment.shipped, return.created, return.status_changed]
    WebhookEvent:
      type: object
      required: [id, type, created_at, data]
      properties:
        id:
          type: string
          format: uuid
          description: Unique per event and the same on every retry. Skip an `id` you already handled.
        type:
          $ref: '#/components/schemas/WebhookEventType'
        created_at:
          type: string
          format: date-time
          description: When the change happened in Nucleo.
        data:
          type: object
          description: |
            Orders and shipments: `order_id`, `order_number`; `order.created` and `order.status_changed` add
            `status` (an [order status](#tag-orders)), `order.status_changed` also `previous_status`;
            `shipment.shipped` adds `shipment_id`. Returns: `return_id`, `return_number`, `order_id`,
            `order_number` and `status` (a [return status](#tag-returns)); `return.status_changed` also
            `previous_status`.
          additionalProperties: true
