openapi: 3.1.0
info:
  title: Nucleo Fulfillment API
  version: v1
  summary: Connect any warehouse to Nucleo Orders — orders to fulfil, picking, shipments, stock levels and returns.
  description: |
    The native API for warehouse management systems (WMS) and logistics providers that fulfil orders for a
    merchant on Nucleo Commerce. One API key per warehouse: the key identifies the merchant and the
    locations your warehouse serves, so every call only sees **your** orders, lines and returns.

    **The warehouse is the caller.** You read the orders to fulfil, accept them, report the picking and the
    shipment, keep stock levels up to date and report the returns you receive. Optional webhooks tell you
    when something changed, so you do not have to poll as often.

    **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`.
    - Every response carries `X-Request-Id`. Errors always have the same shape:
      `{"error": {"code", "message", "request_id", "details"}}`.
    - `POST` and `PUT` accept an `Idempotency-Key`: a retry with the same key and body returns the first
      answer without doing the work twice.
    - Order numbers are the merchant's (`#1042`). In paths you can omit the `#` or encode it as `%23`.
  contact:
    name: Nucleo support
    url: https://nucleoplatform.com
x-nucleo:
  product: fulfillment
  module: commerce
  audience: [warehouse]
  stability: beta
  format: json
  order: 30
servers:
  - url: https://api-commerce.nucleoplatform.com/api/fulfillment/v1
    description: Production
security:
  - apiKey: []
tags:
  - name: Connection
    description: |
      Check that your API key works and see which company, connection and locations it opens.

      **Getting a key.** Nucleo sets up a *Fulfillment API* connection for your warehouse with the merchant
      and links the locations you serve to it. The endpoint, the API key (`nfk_…`) and the webhook secret
      (`whsec_…`) are shown **once**; rotating the credentials replaces both at once. A new connection is a
      draft and answers `403 connection_not_active` until it is turned on.

      **Base URL.** Call the endpoint shown with the credentials: it can be the connection's own host instead of
      the default server below.
  - name: Orders
    description: |
      The life of an order in your warehouse:

      | Status | Meaning | Your next call |
      | --- | --- | --- |
      | `to_fulfill` | Ready for your warehouse and not yet accepted. | `accept` or `reject` |
      | `accepted` | You took it over. | `picked` |
      | `processed` | Picking reported. | `shipments` |
      | `shipped` | Your part has left the warehouse. | — |
      | `cancel_requested` | Cancelled by the merchant after you accepted it, with pieces still to ship: stop if you still can. Only warehouses that accepted it see this status; for the others it is `cancelled`. | `cancel` to confirm you stopped, or `shipments` if the parcel already left |
      | `cancelled` | Cancelled. | — |
      | `rejected` | You refused it; the merchant fixes it and it can come back as `to_fulfill`. | — |
      | `not_ready` | Assigned to you but waiting (payment, hold, documents). | wait |
      | `closed` | Nothing left for your warehouse to do — for example you gave up your part of a split order with `cancel`. | — |

      **Orders split across warehouses (v1).** An order can be split between your warehouse and another one
      — by line, or even within a line (for example 2 pieces from you, 1 from the other warehouse).

      - What works: each warehouse sees only its own lines and its own status. Every quantity you see and
        report — ordered, open, picked, unfulfillable, shipped, cancelled — is **your share** of the line;
        your picking and shipments never change the other warehouse's share. Until **you** accept the order,
        your part stays reserved out of the stock you report.
      - `cancel` gives up only your part: it becomes `closed`, what is left of your share becomes
        unfulfillable (refunded once, if the merchant enabled automatic refunds), and `picked` and
        `shipments` on it answer `409 invalid_state`. This works also when the other warehouse already
        shipped its part.
      - **Cancelled by the merchant**: in v1 a split order is not closed or refunded automatically. It stays
        `cancel_requested` for every warehouse that accepted it and still has something to ship; each one
        confirms with `cancel` (then sees `cancelled`) or reports the shipment if its parcel already left (it
        stays shipped). The merchant completes the cancellation and the refund in Nucleo. A warehouse that
        never accepted it sees it as `cancelled` and has nothing to confirm (`409 order_not_accepted`).

      **Nothing happens on read.** Listing or reading orders never takes them over: only `accept` does.
      Until you accept an order, Nucleo keeps its items reserved out of the stock you report.
  - name: Labels and documents
    description: |
      When the merchant has Nucleo create the carrier labels (through the shipping platform connected to
      Nucleo), you download them here instead of making your own. `shipping.labels_by` on the order tells
      you who makes them: `nucleo` or `warehouse`. The carrier and service come from the order
      (`shipping.carrier_code`, `shipping.service_code`), the tracking numbers from the labels. At most 10
      labels (parcels) per order and warehouse. Documents (invoices, customs documents, inserts) are
      available for every order.
  - name: Stock
    description: |
      Absolute stock levels per item, warehouse location code and classification. Send everything or only
      what changed: items you do not send keep their last level. Report stock **net of the orders you have
      accepted**; Nucleo subtracts the orders you have not accepted yet before publishing availability to
      the sales channels.
  - name: Returns
    description: |
      Returns the customer is sending back to your warehouse, the confirmation that you expect them, what
      actually arrived and in which condition, and parcels nobody announced.

      A return is **compliant** when every expected item arrived in the expected quantity and in `sellable`
      condition; otherwise it is **not compliant** and the merchant's customer service is alerted.
paths:
  /ping:
    get:
      operationId: ping
      summary: Check your API key
      tags: [Connection]
      description: |
        The company, the connection and the locations your key opens, who makes the carrier labels and
        whether webhooks are on. Safe to call at any time.
      x-rate-limit: &rateLimit
        limit: 600
        window: 1m
        scope: per connection
        note: Default limit; the merchant can set another one on the connection.
      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 Logistics Verona
                  status: active
                locations:
                  - code: VRN
                    name: Verona warehouse
                    stock_location_codes: [VR1]
                labels_by: warehouse
                webhooks:
                  enabled: true
                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 of your warehouse in one status, with all the details of [Get an order](#get-order).

        - `to_fulfill` (default): ready for you and not yet accepted or rejected, **oldest first**. This is
          the queue to poll. An order appears here only when everything it needs is ready (payment, carrier,
          item codes, customs documents when required).
        - `accepted`, `processed`, `shipped`, `cancelled`: sorted by last update, oldest first. Use
          `updated_since` to sync only what changed. `cancelled` also includes `cancel_requested` orders.

        Reading never changes an order. A `to_fulfill` page can hold fewer than `limit` orders — even none —
        while `next_cursor` is not `null`: keep following `next_cursor` until it is `null`.
      x-rate-limit: *rateLimit
      parameters:
        - name: status
          in: query
          required: false
          description: Which orders to list.
          schema:
            type: string
            enum: [to_fulfill, accepted, processed, shipped, cancelled]
            default: to_fulfill
        - $ref: '#/components/parameters/UpdatedSince'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: One page of orders.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Order'
                  next_cursor:
                    type: [string, 'null']
                    description: Pass it as `cursor` to get the next page; `null` on the last page.
              examples:
                page:
                  $ref: '#/components/examples/OrderListPage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /orders/{number}:
    get:
      operationId: getOrder
      summary: Get an order
      tags: [Orders]
      description: |
        One order of your warehouse, in any status: customer and shipping address, shipping method and
        carrier, priority, notes for the warehouse, the lines assigned to you with their quantities, weights
        and prices (for customs), and the status of your part.

        An order of another warehouse answers `404`, exactly like an order that does not exist.
      x-rate-limit: *rateLimit
      parameters:
        - $ref: '#/components/parameters/OrderNumber'
      responses:
        '200':
          description: The order.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
              examples:
                toFulfill:
                  $ref: '#/components/examples/OrderToFulfill'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /orders/{number}/accept:
    post:
      operationId: acceptOrder
      summary: Accept an order
      tags: [Orders]
      description: |
        Takes the order over: it leaves `to_fulfill` and becomes `accepted`. Optionally store your own
        reference for it. Accepting an order you already accepted changes nothing (it only updates
        `wms_reference` if you send one).

        When Nucleo makes the labels, the label of the first parcel is created now, ready for
        [Get labels](#list-labels).

        `409` when the order is not ready yet (`order_not_ready`), cancelled (`order_cancelled`,
        `order_cancel_requested`), already picked (`invalid_state`) or already shipped by your warehouse
        (`order_already_shipped`).
      x-rate-limit: *rateLimit
      parameters:
        - $ref: '#/components/parameters/OrderNumber'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AcceptRequest'
            example:
              wms_reference: VR-2026-004211
      responses:
        '200':
          $ref: '#/components/responses/OrderUpdated'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /orders/{number}/reject:
    post:
      operationId: rejectOrder
      summary: Reject an order
      tags: [Orders]
      description: |
        Refuses an order you have **not** accepted, with a reason. The merchant is alerted, fixes the order
        (for example the address) and releases it again: it then comes back as `to_fulfill`.

        For an order you already accepted use [Cancel an order](#cancel-order): rejecting it answers
        `409 order_already_accepted`.
      x-rate-limit: *rateLimit
      parameters:
        - $ref: '#/components/parameters/OrderNumber'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RejectRequest'
            example:
              reason_code: ADDRESS_INCOMPLETE
              message: Street number missing.
      responses:
        '200':
          $ref: '#/components/responses/OrderUpdated'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /orders/{number}/picked:
    post:
      operationId: reportPicking
      summary: Report the picking
      tags: [Orders]
      description: |
        What you picked for an accepted order, with lots, expiry dates and serial numbers, and the parcels you
        packed. **All or nothing**: every line must be an item of this order assigned to your warehouse
        (by `sku` or `barcode`), otherwise nothing is recorded (`422 unknown_item`).

        Report the quantity actually picked for every item. A quantity lower than ordered marks the missing
        pieces as **unfulfillable**: the merchant is alerted and, if configured, the customer is refunded
        for them. An item you do not list counts as picked 0. You can send it again to correct it until you
        ship: the **last** report counts. With automatic refunds, a later report with more pieces missing
        refunds only the difference; a refund already made is never taken back, so a correction with fewer
        pieces missing is flagged to the merchant. Report the picking when it is complete.

        **Labels made by Nucleo.** Each parcel you list gets its own label (parcel 1, 2, …): your reference,
        weight and sizes are added to it and the label's tracking number is kept.

        `409 order_not_accepted` before you accept the order; `409 order_already_shipped` after your
        shipment; `409 order_cancelled` or `order_cancel_requested` when the merchant cancelled it.
      x-rate-limit: *rateLimit
      parameters:
        - $ref: '#/components/parameters/OrderNumber'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PickedRequest'
            examples:
              full:
                summary: Everything picked, two parcels
                value:
                  picked_at: '2026-10-13T09:12:00Z'
                  lines:
                    - sku: TEE-BLK-M
                      quantity: 1
                    - barcode: '2000000000022'
                      quantity: 2
                      lot_number: L2026-41
                      lot_expiration: '2028-12-31'
                  parcels:
                    - reference: PK-1
                      weight_g: 640
                      height_mm: 80
                      width_mm: 300
                      depth_mm: 400
                    - reference: PK-2
                      weight_g: 980
              short:
                summary: One hoodie missing
                value:
                  lines:
                    - sku: TEE-BLK-M
                      quantity: 1
                    - sku: HOOD-GRY-L
                      quantity: 1
      responses:
        '200':
          $ref: '#/components/responses/OrderUpdated'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /orders/{number}/shipments:
    post:
      operationId: createShipment
      summary: Report a shipment
      tags: [Orders]
      description: |
        Your part of the order has left the warehouse: carrier, service, one tracking number per parcel.
        Nucleo marks the order shipped and creates the fulfilment on the sales channel, so the customer gets
        the tracking.

        - Without `lines`, the shipment contains what you reported as picked (or, without a picking report,
          everything still open on your lines). With `lines`, only those quantities: use it for partial
          shipments, then send another shipment for the rest. Once a shipment exists, `picked` and `cancel`
          answer `409 order_already_shipped`: if the rest cannot leave, the merchant sees the order as
          partly shipped and closes it in Nucleo.
        - **Idempotent on the tracking number**: the same first `tracking_number` again answers `200` with
          the shipment already recorded, without a second fulfilment. A different tracking number when
          nothing is left open answers `409 order_already_shipped`.
        - When Nucleo makes the labels, `carrier_code` and `parcels` are optional and `parcels` takes at most
          10 items: without `parcels` the shipment has one parcel per ready label, with the tracking numbers of
          the labels and the carrier and service of the order, and is the same shipment as the labels
          (sending it again answers `200`). With no ready label it answers `409 labels_not_ready`: create the
          labels first, or send the parcels with their tracking numbers.
        - The order status is checked first: a cancelled order answers `409 order_cancelled`, a part you gave
          up `409 invalid_state`, an order you have not accepted `409 order_not_accepted`.
        - On a split order the shipment contains only your share, and "nothing left open" means nothing left
          of **your** share.

        `201` for a new shipment, `200` for a repeated one.
      x-rate-limit: *rateLimit
      parameters:
        - $ref: '#/components/parameters/OrderNumber'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ShipmentRequest'
            example:
              carrier_code: EXAMPLE
              carrier_name: Example Express
              service_code: EXPRESS
              shipped_at: '2026-10-13T15:40:00Z'
              tracking_url: https://tracking.example.com/064152003391
              parcels:
                - tracking_number: '064152003391'
                  reference: PK-1
                  weight_g: 640
                - tracking_number: '064152003392'
                  reference: PK-2
                  weight_g: 980
              return_carrier_code: EXAMPLE
              return_tracking_number: '064152003399'
      responses:
        '200':
          description: The same shipment was already recorded; nothing changed.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Shipment'
        '201':
          description: Shipment recorded.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Shipment'
              example:
                id: 2c9d4e1f-7a3b-4c5d-9e8f-0a1b2c3d4e5f
                order_number: '#1042'
                order_status: shipped
                carrier_code: EXAMPLE
                carrier_name: Example Express
                service_code: EXPRESS
                tracking_url: https://tracking.example.com/064152003391
                shipped_at: '2026-10-13T15:40:00Z'
                parcels:
                  - number: 1
                    reference: PK-1
                    tracking_number: '064152003391'
                    weight_g: 640
                  - number: 2
                    reference: PK-2
                    tracking_number: '064152003392'
                    weight_g: 980
                lines:
                  - line_id: 0f1e2d3c-4b5a-4968-8776-5a4b3c2d1e0f
                    sku: TEE-BLK-M
                    barcode: '2000000000015'
                    quantity: 1
                  - line_id: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
                    sku: HOOD-GRY-L
                    barcode: '2000000000022'
                    quantity: 2
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /orders/{number}/cancel:
    post:
      operationId: cancelOrder
      summary: Cancel an order
      tags: [Orders]
      description: |
        Your warehouse cannot fulfil an order it accepted (stock damaged or missing, address the carrier
        refuses…). The order becomes `cancelled` and the merchant is alerted to cancel and refund it on the
        sales channel (or Nucleo does it, if the merchant configured so). Calling it again changes nothing.

        - **Order split with another warehouse** (the other part still open, or already shipped): `cancel`
          gives up only your part. What is left of your share becomes unfulfillable (refunded once, if the merchant enabled
          automatic refunds), your part becomes `closed` and `picked` and `shipments` answer
          `409 invalid_state`; the other warehouse goes on with its own lines.
        - **Order in `cancel_requested`** (the merchant cancelled it after you accepted it): `cancel` confirms
          that you stopped. An order handled only by your warehouse is then closed as `cancelled`; if part of
          it already left, that part stays shipped and only the unshipped pieces are cancelled and refunded.
          A split order stays `cancel_requested` until the merchant completes the cancellation in Nucleo.
          A warehouse that never accepted it gets `409 order_not_accepted`.

        `409 order_not_accepted` for an order you have not accepted — use [Reject an order](#reject-order);
        `409 order_already_shipped` after your shipment.
      x-rate-limit: *rateLimit
      parameters:
        - $ref: '#/components/parameters/OrderNumber'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CancelRequest'
            example:
              reason_code: OUT_OF_STOCK
              message: Last piece found damaged during picking.
      responses:
        '200':
          $ref: '#/components/responses/OrderUpdated'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /orders/{number}/labels:
    get:
      operationId: listLabels
      summary: Get labels
      tags: [Labels and documents]
      description: |
        The carrier labels Nucleo created for the order, one per parcel, as base64 content (PDF, 10×15 cm,
        or ZPL). A label that could not be created has `status: failed` and no content: create it again with
        [Create labels](#create-labels). After your shipment it shows the labels the shipment left with.
        Reading never creates anything.

        Only when Nucleo makes the labels (`shipping.labels_by: nucleo`); otherwise `422 labels_not_enabled`.
      x-rate-limit: *rateLimit
      parameters:
        - $ref: '#/components/parameters/OrderNumber'
      responses:
        '200':
          description: Labels of the order.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LabelList'
              example:
                data:
                  - parcel: 1
                    status: ready
                    carrier_code: EXAMPLE
                    carrier_name: Example Express
                    tracking_number: '1234567890'
                    format: pdf
                    content_type: application/pdf
                    content_base64: JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFI+PgpzdHJlYW0K
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      operationId: createLabels
      summary: Create labels
      tags: [Labels and documents]
      description: |
        Makes sure there is one label for each parcel from 1 to `parcels` (at most 10): creates the missing
        ones and retries the failed ones; labels already created are kept. Answers with all the labels of
        the order. Only once **your** part is accepted (`409 order_not_accepted` before), and while you still
        have something to ship: after a partial shipment it creates new labels for the rest, and the next
        shipment without `parcels` takes them. Not for a cancelled part (`409 order_cancelled`,
        `409 order_cancel_requested`), nor when your part is shipped in full or given up (`409 invalid_state`).
      x-rate-limit: *rateLimit
      parameters:
        - $ref: '#/components/parameters/OrderNumber'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [parcels]
              properties:
                parcels:
                  type: integer
                  minimum: 1
                  maximum: 10
                  description: Number of parcels the order ships in.
            example:
              parcels: 2
      responses:
        '201':
          description: Labels of the order, including the new ones.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LabelList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /orders/{number}/documents:
    get:
      operationId: listDocuments
      summary: Get documents
      tags: [Labels and documents]
      description: |
        Documents to print and put in the parcel, as base64 PDFs: the order's documents (invoice, customs
        documents) with `kind: order_document`, and the merchant's inserts in the customer's language
        (return instructions, leaflets) with `kind: insert`. `position` says whether a document goes
        `inside` the parcel (`internal`) or in the document pouch outside (`external`).

        When the order needs customs documents that are not ready yet, `409 documents_not_ready`: retry
        later. Such orders do not appear in `to_fulfill` until their documents are ready.
      x-rate-limit: *rateLimit
      parameters:
        - $ref: '#/components/parameters/OrderNumber'
      responses:
        '200':
          description: Documents of the order (possibly none).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Document'
              example:
                data:
                  - id: 7d1c2b3a-4e5f-4a6b-9c8d-1e2f3a4b5c6d
                    name: Invoice-2026-001042
                    kind: order_document
                    position: external
                    page_format: A4
                    content_type: application/pdf
                    content_base64: JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFI+PgpzdHJlYW0K
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /stock:
    put:
      operationId: updateStock
      summary: Update stock levels
      tags: [Stock]
      description: |
        **Absolute** levels (not increments) per item, location code and classification, at most 500 items
        per call (the merchant can set a different limit; above it nothing is processed and the answer is
        `422 too_many_items`: split the update in batches).

        - Identify each item by `sku` or `barcode`.
        - `location_code` is one of the `stock_location_codes` returned by [Ping](#ping). It can be omitted
          when your connection has a single code.
        - `classification` defaults to `sellable`; only `sellable` stock is published as available to the
          sales channels. Other classifications are stored and, if the merchant chooses so, published as
          damaged (not sellable) stock.
        - `counted_at`: when the level was counted; without it, the time Nucleo receives the call. A level
          older than the last one applied for the same item, location code and classification is not applied
          and is listed in `ignored` with reason `stale`, so late retries cannot roll stock back.

        Unknown items are **ignored** (listed in `ignored` with reason `unknown_item`, and the merchant is
        alerted); the others are applied and counted in `accepted`. A location code not linked to a Nucleo location yet is saved but not published, and listed
        in `warnings`. Validation errors (`400`) reject the whole call.
      x-rate-limit: *rateLimit
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StockRequest'
            example:
              items:
                - sku: TEE-BLK-M
                  quantity: 42
                  counted_at: '2026-10-13T06:00:00Z'
                - barcode: '2000000000022'
                  location_code: VR1
                  quantity: 3
                  classification: damaged
                - sku: SOCK-WHT-S
                  quantity: 0
      responses:
        '200':
          description: Levels applied.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StockResult'
              example:
                accepted: 2
                ignored:
                  - index: 2
                    sku: SOCK-WHT-S
                    barcode: null
                    reason: unknown_item
                warnings: []
        '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'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /returns:
    get:
      operationId: listReturns
      summary: List returns
      tags: [Returns]
      description: |
        Returns coming to your warehouse: those whose return location is yours or, without a return
        location, those of orders your warehouse fulfilled. Sorted by last update, oldest first.

        - `expected` (default): announced and not received yet. `accepted_at` tells whether you confirmed it.
        - `received`: received, with the outcome.
        - `cancelled`: cancelled by the customer or the merchant: do not wait for it.
      x-rate-limit: *rateLimit
      parameters:
        - name: status
          in: query
          required: false
          description: Which returns to list.
          schema:
            type: string
            enum: [expected, received, cancelled]
            default: expected
        - $ref: '#/components/parameters/UpdatedSince'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: One page of returns.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Return'
                  next_cursor:
                    type: [string, 'null']
              examples:
                page:
                  $ref: '#/components/examples/ReturnListPage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /returns/{id}:
    get:
      operationId: getReturn
      summary: Get a return
      tags: [Returns]
      description: One return of your warehouse, by return number or ID.
      x-rate-limit: *rateLimit
      parameters:
        - $ref: '#/components/parameters/ReturnId'
      responses:
        '200':
          description: The return.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Return'
              examples:
                expected:
                  $ref: '#/components/examples/ReturnExpected'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /returns/{id}/accept:
    post:
      operationId: acceptReturn
      summary: Confirm an expected return
      tags: [Returns]
      description: |
        Confirms that your warehouse expects the return; the merchant sees it as *awaited at warehouse*.
        Repeating it changes nothing. Only for `expected` returns (`409 invalid_state` otherwise).
      x-rate-limit: *rateLimit
      parameters:
        - $ref: '#/components/parameters/ReturnId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          $ref: '#/components/responses/ReturnUpdated'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /returns/{id}/receipt:
    post:
      operationId: receiveReturn
      summary: Report what arrived
      tags: [Returns]
      description: |
        What arrived, item by item, with its condition. On the first receipt, expected items you do not list
        count as received 0; items that were not expected are added and make the return not compliant.
        Several lines of the same item in different conditions are allowed. A later receipt updates only the
        items it lists (the others keep what you reported before): to correct, list every item again.

        The return becomes `received` with an `outcome`: `compliant` when quantities match and everything
        is `sellable`, `not_compliant` otherwise (the merchant's customer service is alerted). Sending a new
        receipt re-evaluates the outcome. A receipt for a cancelled or closed return is recorded, its status
        does not change and the merchant is alerted.
      x-rate-limit: *rateLimit
      parameters:
        - $ref: '#/components/parameters/ReturnId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReceiptRequest'
            example:
              received_at: '2026-10-20T10:05:00Z'
              lines:
                - sku: TEE-BLK-M
                  quantity: 1
                  condition: sellable
                - sku: HOOD-GRY-L
                  quantity: 1
                  condition: damaged
                  note: Stain on the left sleeve.
      responses:
        '200':
          $ref: '#/components/responses/ReturnUpdated'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /returns/unannounced:
    post:
      operationId: createUnannouncedReturn
      summary: Report an unannounced return
      tags: [Returns]
      description: |
        A parcel arrived for an order but nobody announced a return. Nucleo creates the return on the order,
        records what arrived (as in [Report what arrived](#receive-return)) and alerts the merchant.

        Only for orders your warehouse fulfilled or expects returns of: any other order answers `404` (and
        the merchant is alerted that goods are in the wrong warehouse). Sending the same content again —
        same order, `reference` and lines — returns the return already created (`200`) instead of a new one.
        `reference` (for example the number written on the parcel) becomes the return number when it is
        free.
      x-rate-limit: *rateLimit
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UnannouncedRequest'
            example:
              order_number: '1042'
              reference: PKG-55120
              lines:
                - sku: TEE-BLK-M
                  quantity: 1
                  condition: sellable
      responses:
        '200':
          $ref: '#/components/responses/ReturnUpdated'
        '201':
          description: Return created and received.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Return'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/TooManyRequests'
webhooks:
  order.ready:
    post:
      operationId: orderReadyWebhook
      summary: An order is ready for your warehouse
      tags: [Orders]
      security:
        - webhookSignature: []
      description: |
        Sent when an order assigned to your warehouse becomes ready to fulfil. Read it with
        [Get an order](#get-order) or poll `status=to_fulfill`. It is a hint, not the queue: keep polling as
        a fallback (for example every 15 minutes), because an order can also become ready later than this
        event (customs documents) and a webhook can fail.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEvent'
            example:
              id: 5b7e1c2d-3a4f-4b6c-8d9e-0f1a2b3c4d5e
              type: order.ready
              created_at: '2026-10-13T08:29:51Z'
              data:
                order_id: 3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b
                order_number: '#1042'
      responses:
        '200':
          $ref: '#/components/responses/WebhookAccepted'
  order.cancelled:
    post:
      operationId: orderCancelledWebhook
      summary: An order was cancelled
      tags: [Orders]
      security:
        - webhookSignature: []
      description: |
        Sent once when an order that was ready for your warehouse is cancelled by the merchant or on the
        sales channel, with the status **your part** has: `cancelled` when you had not accepted it, or
        `cancel_requested` when you had: stop picking if you still can, then [cancel it](#cancel-order) — or
        report the shipment if the parcel already left. Not sent to a warehouse that already shipped all its
        part or gave it up.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEvent'
            example:
              id: 8c9d0e1f-2a3b-4c4d-9e5f-6a7b8c9d0e1f
              type: order.cancelled
              created_at: '2026-10-13T10:02:14Z'
              data:
                order_id: 3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b
                order_number: '#1042'
                status: cancel_requested
      responses:
        '200':
          $ref: '#/components/responses/WebhookAccepted'
  return.expected:
    post:
      operationId: returnExpectedWebhook
      summary: A return is coming to your warehouse
      tags: [Returns]
      security:
        - webhookSignature: []
      description: |
        Sent when a customer return addressed to your warehouse is approved. Read it with
        [Get a return](#get-return) and confirm it with [Confirm an expected return](#accept-return).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEvent'
            example:
              id: 0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d
              type: return.expected
              created_at: '2026-10-18T16:20:03Z'
              data:
                return_id: 9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b
                return_number: R1042-1
                order_number: '#1042'
      responses:
        '200':
          $ref: '#/components/responses/WebhookAccepted'
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: nfk_…
      description: |
        `Authorization: Bearer <api_key>`. One key per warehouse connection, issued by Nucleo and shown
        once. A missing or wrong key — or a call from an IP outside the connection's
        allowlist, when the merchant set one — answers `401`. After 30 failed attempts in 5 minutes the
        calling IP is blocked for the rest of the window.
    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.
  parameters:
    OrderNumber:
      name: number
      in: path
      required: true
      description: The merchant's order number. The leading `#` can be omitted, or encoded as `%23`.
      schema:
        type: string
      example: '1042'
    ReturnId:
      name: id
      in: path
      required: true
      description: Return number (as in `number`) or return ID.
      schema:
        type: string
      example: R1042-1
    Cursor:
      name: cursor
      in: query
      required: false
      description: '`next_cursor` of the previous page. A cursor only works with the same `status`.'
      schema:
        type: string
    Limit:
      name: limit
      in: query
      required: false
      description: Items per page.
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    UpdatedSince:
      name: updated_since
      in: query
      required: false
      description: Only items updated at or after this time (ISO 8601).
      schema:
        type: string
        format: date-time
      example: '2026-10-13T06: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 request — same method, same path written the same way
        (`/orders/1042` and `/orders/%231042` differ) and same body — returns the stored answer without doing
        the work again (header `Idempotent-Replayed: true`; the replayed body keeps the original
        `request_id`, the `X-Request-Id` header is the new call's). The same key with a different request
        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. Server errors (`5xx`) and `429` are not stored: retry
        them with the same key.
      schema:
        type: string
        maxLength: 255
      example: 2f0c9a1e-6b4d-4f3a-9e7c-5d1b8a2c4e6f
  headers:
    XRequestId:
      description: Identifier of the call. Quote it when you contact support; it is also `error.request_id`.
      schema:
        type: string
        format: uuid
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
    IdempotentReplayed:
      description: '`true` when the answer is the stored one of an earlier request with the same `Idempotency-Key`.'
      schema:
        type: string
        enum: ['true']
  responses:
    OrderUpdated:
      description: The order after the change.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        Idempotent-Replayed:
          $ref: '#/components/headers/IdempotentReplayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Order'
    ReturnUpdated:
      description: The return after the change.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        Idempotent-Replayed:
          $ref: '#/components/headers/IdempotentReplayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Return'
          examples:
            received:
              $ref: '#/components/examples/ReturnReceived'
    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 by polling.
    BadRequest:
      description: |
        The request is not valid: `validation_failed` (with `details` per field), `invalid_json`,
        `invalid_cursor`. Nothing was changed.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            validation:
              value:
                error:
                  code: validation_failed
                  message: The request is not valid.
                  request_id: 1f7e3c2a-9b8d-4e6f-a5c4-3b2a1d0e9f8c
                  details:
                    lines.0.quantity: [The lines.0.quantity field must be at least 0.]
                    reason_code: [The reason code field is required.]
    Unauthorized:
      description: Missing or invalid API key (`unauthorized`).
      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: 0b6c3e4f-1a2d-4c5b-8e7f-9a0b1c2d3e4f
    Forbidden:
      description: |
        The connection is paused (`connection_paused`) or not active yet (`connection_not_active`). Nothing
        was read or changed: retry later.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: connection_paused
              message: 'This connection is paused: nothing was changed.'
              request_id: 3c2b1a0f-9e8d-4c7b-a6f5-4e3d2c1b0a9f
    NotFound:
      description: |
        `order_not_found` or `return_not_found` — it does not exist **or it belongs to another warehouse**;
        `not_found` for an unknown path.
      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: 6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a
    Conflict:
      description: |
        The request conflicts with the current state: `order_not_ready`, `order_not_accepted`,
        `order_already_accepted`, `order_cancelled`, `order_cancel_requested`, `order_already_shipped`,
        `invalid_state`, `documents_not_ready`, `labels_not_ready`; or with an `Idempotency-Key`: `idempotency_key_reused`,
        `idempotency_key_in_progress` (with `Retry-After`). Read the order or return again before retrying.
      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: order_not_accepted
              message: Accept order #1042 before reporting the picking.
              request_id: 9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d
    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'
    Unprocessable:
      description: |
        Valid JSON that cannot be applied: `unknown_item` (an item that is not on the order or not known,
        with `details` per line), `too_many_items`, `labels_not_enabled`. Nothing was changed.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: unknown_item
              message: Some lines do not match an item of this order assigned to your warehouse. Nothing was changed.
              request_id: 2e1d0c9b-8a7f-4e6d-9c5b-4a3f2e1d0c9b
              details:
                lines.1: [Not an item of this order assigned to your warehouse.]
    TooManyRequests:
      description: |
        `rate_limited`: more calls in a minute than the connection allows (600 by default, all endpoints
        together; the merchant can set another limit), or too
        many failed authentications from your IP. Wait 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: 4f3e2d1c-0b9a-4f8e-8d7c-6b5a4f3e2d1c
  examples:
    OrderToFulfill:
      summary: An order to fulfil
      value: &orderToFulfill
        id: 3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b
        number: '#1042'
        status: to_fulfill
        placed_at: '2026-10-13T08:12:40Z'
        updated_at: '2026-10-13T08:29:51Z'
        channel: shopify
        customer:
          name: Giulia Verdi
          email: giulia.verdi@example.com
          phone: '+39 333 0000000'
        shipping_address:
          first_name: Giulia
          last_name: Verdi
          company: null
          address1: Via Roma 12
          address2: Scala B
          city: Milano
          postal_code: '20121'
          province_code: MI
          country_code: IT
          phone: '+39 333 0000000'
        shipping:
          method_code: express
          method_name: Express 24h
          carrier_code: EXAMPLE
          service_code: EXPRESS
          type: express
          labels_by: warehouse
        priority: 1
        notes: 'Gift wrap, please.'
        documents:
          external_required: false
          external_ready: null
        currency: EUR
        total_weight_g: 1540
        lines:
          - line_id: 0f1e2d3c-4b5a-4968-8776-5a4b3c2d1e0f
            sku: TEE-BLK-M
            barcode: '2000000000015'
            item_key: '2000000000015'
            title: Organic Cotton Tee · Black · M
            quantity_ordered: 1
            quantity_open: 1
            quantity_picked: 0
            quantity_unfulfillable: 0
            quantity_shipped: 0
            quantity_cancelled: 0
            weight_g: 220
            unit_price:
              amount: '29.00'
              currency: EUR
          - line_id: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
            sku: HOOD-GRY-L
            barcode: '2000000000022'
            item_key: '2000000000022'
            title: Fleece Hoodie · Grey · L
            quantity_ordered: 2
            quantity_open: 2
            quantity_picked: 0
            quantity_unfulfillable: 0
            quantity_shipped: 0
            quantity_cancelled: 0
            weight_g: 660
            unit_price:
              amount: '75.50'
              currency: EUR
        accepted_at: null
        wms_reference: null
        cancellation: null
    ReturnExpected:
      summary: An expected return
      value: &returnExpected
        id: 9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b
        number: R1042-1
        status: expected
        outcome: null
        unannounced: false
        order_number: '#1042'
        reason:
          code: size
          text: Too small
        customer_note: null
        return_tracking:
          carrier_code: EXAMPLE
          tracking_number: '064152003399'
        created_at: '2026-10-18T16:20:03Z'
        updated_at: '2026-10-18T16:20:03Z'
        accepted_at: null
        received_at: null
        cancelled_at: null
        lines:
          - line_id: 7a6b5c4d-3e2f-4a1b-9c0d-8e7f6a5b4c3d
            order_line_id: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
            sku: HOOD-GRY-L
            barcode: '2000000000022'
            item_key: '2000000000022'
            title: Fleece Hoodie · Grey · L
            quantity_expected: 1
            quantity_received: null
            condition: null
            reason_code: size
            note: null
    OrderListPage:
      summary: One page of orders to fulfil
      value:
        data:
          - *orderToFulfill
        next_cursor: WyJvcmRlcnM6dG9fZnVsZmlsbCIsIjIwMjYtMTAtMTMgMDg6MTI6NDEiLCIzZjJhMWI0YyJd
    ReturnListPage:
      summary: One page of expected returns
      value:
        data:
          - *returnExpected
        next_cursor: null
    ReturnReceived:
      summary: A received return
      value:
        id: 9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b
        number: R1042-1
        status: received
        outcome: not_compliant
        unannounced: false
        order_number: '#1042'
        reason:
          code: size
          text: Too small
        customer_note: null
        return_tracking:
          carrier_code: EXAMPLE
          tracking_number: '064152003399'
        created_at: '2026-10-18T16:20:03Z'
        updated_at: '2026-10-20T10:05:12Z'
        accepted_at: '2026-10-18T16:35:00Z'
        received_at: '2026-10-20T10:05:00Z'
        cancelled_at: null
        lines:
          - line_id: 7a6b5c4d-3e2f-4a1b-9c0d-8e7f6a5b4c3d
            order_line_id: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
            sku: HOOD-GRY-L
            barcode: '2000000000022'
            item_key: '2000000000022'
            title: Fleece Hoodie · Grey · L
            quantity_expected: 1
            quantity_received: 1
            condition: damaged
            reason_code: size
            note: Stain on the left sleeve.
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code:
              type: string
              description: Stable, machine-readable code. Branch on it, not on `message`.
              enum:
                - validation_failed
                - invalid_json
                - invalid_cursor
                - unauthorized
                - connection_paused
                - connection_not_active
                - not_found
                - method_not_allowed
                - order_not_found
                - return_not_found
                - order_not_ready
                - order_not_accepted
                - order_already_accepted
                - order_cancelled
                - order_cancel_requested
                - order_already_shipped
                - invalid_state
                - documents_not_ready
                - labels_not_ready
                - idempotency_key_reused
                - idempotency_key_in_progress
                - payload_too_large
                - unknown_item
                - too_many_items
                - labels_not_enabled
                - rate_limited
                - internal_error
            message:
              type: string
              description: Explanation in English for people. It can change; never parse it.
            request_id:
              type: string
              format: uuid
              description: Same as the `X-Request-Id` header.
            details:
              type: object
              description: 'Validation and item errors per field: `{"lines.0.quantity": ["…"]}`.'
              additionalProperties:
                type: array
                items:
                  type: string
    Money:
      type: object
      required: [amount, currency]
      properties:
        amount:
          type: string
          description: Decimal amount with two decimals.
          example: '29.00'
        currency:
          type: string
          description: ISO 4217 code.
          example: EUR
    Ping:
      type: object
      properties:
        api_version:
          type: string
          const: v1
        company:
          type: object
          properties:
            id:
              type: integer
            name:
              type: [string, 'null']
        connection:
          type: object
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            status:
              type: string
              enum: [active, error]
              description: '`error` while Nucleo considers the warehouse silent; your next call clears it.'
        locations:
          type: array
          description: Nucleo locations your warehouse serves.
          items:
            type: object
            properties:
              code:
                type: string
              name:
                type: string
              stock_location_codes:
                type: array
                description: Codes to use as `location_code` in stock updates.
                items:
                  type: string
        labels_by:
          type: string
          enum: [nucleo, warehouse]
        webhooks:
          type: object
          properties:
            enabled:
              type: boolean
        server_time:
          type: string
          format: date-time
    Order:
      type: object
      required: [id, number, status, lines]
      properties:
        id:
          type: string
          format: uuid
        number:
          type: string
          description: The merchant's order number.
          example: '#1042'
        status:
          type: string
          description: Status of **your warehouse's part** of the order (see the Orders tag).
          enum: [to_fulfill, accepted, processed, shipped, cancel_requested, cancelled, rejected, not_ready, closed]
        placed_at:
          type: [string, 'null']
          format: date-time
        updated_at:
          type: string
          format: date-time
        channel:
          type: string
          description: Sales channel the order came from.
          example: shopify
        customer:
          type: object
          properties:
            name:
              type: [string, 'null']
            email:
              type: [string, 'null']
              format: email
            phone:
              type: [string, 'null']
        shipping_address:
          $ref: '#/components/schemas/Address'
        shipping:
          type: object
          properties:
            method_code:
              type: [string, 'null']
              description: Shipping method chosen at checkout.
            method_name:
              type: [string, 'null']
            carrier_code:
              type: [string, 'null']
              description: Carrier to use, from the merchant's carrier rules.
            service_code:
              type: [string, 'null']
              description: Carrier service (for example `EXPRESS`).
            type:
              type: string
              enum: [standard, express]
            labels_by:
              type: string
              enum: [nucleo, warehouse]
              description: Who makes the carrier labels.
        priority:
          type: integer
          description: 1 is the highest. Express orders come first by default.
        notes:
          type: [string, 'null']
          description: Notes for the warehouse (the customer's note and the merchant's instructions).
        documents:
          type: object
          properties:
            external_required:
              type: boolean
              description: The parcel needs customs documents outside (see Get documents).
            external_ready:
              type: [boolean, 'null']
        currency:
          type: string
        total_weight_g:
          type: [integer, 'null']
          description: Sum of the line weights in grams, when known.
        lines:
          type: array
          description: Only the lines assigned to your warehouse.
          items:
            $ref: '#/components/schemas/OrderLine'
        accepted_at:
          type: [string, 'null']
          format: date-time
        wms_reference:
          type: [string, 'null']
          description: Your reference, as sent when accepting.
        cancellation:
          type: ['object', 'null']
          properties:
            requested_by:
              type: string
              enum: [merchant, channel, warehouse]
            cancelled_at:
              type: [string, 'null']
              format: date-time
    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']
    OrderLine:
      type: object
      properties:
        line_id:
          type: string
          format: uuid
        sku:
          type: [string, 'null']
        barcode:
          type: [string, 'null']
          description: EAN/GTIN of the variant.
        item_key:
          type: [string, 'null']
          description: The item code agreed with your warehouse (the barcode, unless agreed otherwise).
        title:
          type: [string, 'null']
        quantity_ordered:
          type: integer
          description: Quantity assigned to your warehouse (your share of the line).
        quantity_open:
          type: integer
          description: Still to ship from your share — ordered minus shipped, cancelled and unfulfillable.
        quantity_picked:
          type: integer
        quantity_unfulfillable:
          type: integer
          description: Declared missing in your picking report (or given up with `cancel`).
        quantity_shipped:
          type: integer
        quantity_cancelled:
          type: integer
        weight_g:
          type: [integer, 'null']
          description: Unit weight in grams, when known.
        unit_price:
          $ref: '#/components/schemas/Money'
    AcceptRequest:
      type: object
      properties:
        wms_reference:
          type: [string, 'null']
          maxLength: 120
          description: Your own order reference, shown to the merchant.
    RejectRequest:
      type: object
      required: [reason_code]
      properties:
        reason_code:
          type: string
          maxLength: 30
          description: Your reason code, shown to the merchant.
        message:
          type: [string, 'null']
          maxLength: 500
    CancelRequest:
      type: object
      required: [reason_code]
      properties:
        reason_code:
          type: string
          maxLength: 50
        message:
          type: [string, 'null']
          maxLength: 500
        cancelled_at:
          type: [string, 'null']
          format: date-time
    ItemRef:
      type: object
      description: Identify the item by `sku` or `barcode` (at least one).
      properties:
        sku:
          type: [string, 'null']
          maxLength: 255
        barcode:
          type: [string, 'null']
          maxLength: 255
    PickedRequest:
      type: object
      required: [lines]
      properties:
        picked_at:
          type: [string, 'null']
          format: date-time
        lines:
          type: array
          minItems: 1
          maxItems: 500
          items:
            allOf:
              - $ref: '#/components/schemas/ItemRef'
              - type: object
                required: [quantity]
                properties:
                  quantity:
                    type: integer
                    minimum: 0
                    description: Pieces picked (0 if none).
                  lot_number:
                    type: [string, 'null']
                    maxLength: 100
                  lot_expiration:
                    type: [string, 'null']
                    format: date
                  serial_number:
                    type: [string, 'null']
                    maxLength: 100
        parcels:
          type: array
          maxItems: 50
          description: At most 50 parcels, or 10 when Nucleo makes the labels (one label per parcel).
          items:
            $ref: '#/components/schemas/ParcelInput'
    ParcelInput:
      type: object
      properties:
        reference:
          type: [string, 'null']
          maxLength: 120
          description: Your parcel ID.
        tracking_number:
          type: [string, 'null']
          maxLength: 120
        weight_g:
          type: [integer, 'null']
          minimum: 0
        height_mm:
          type: [integer, 'null']
          minimum: 0
        width_mm:
          type: [integer, 'null']
          minimum: 0
        depth_mm:
          type: [integer, 'null']
          minimum: 0
    ShipmentRequest:
      type: object
      description: '`carrier_code`, `parcels` and every `tracking_number` are required unless Nucleo makes the labels.'
      properties:
        carrier_code:
          type: string
          maxLength: 60
          example: EXAMPLE
        carrier_name:
          type: [string, 'null']
          maxLength: 120
        service_code:
          type: [string, 'null']
          maxLength: 60
        tracking_url:
          type: [string, 'null']
          format: uri
          description: '`https` only. Without it Nucleo builds the carrier''s tracking link when it knows the carrier.'
        shipped_at:
          type: [string, 'null']
          format: date-time
        parcels:
          type: array
          minItems: 1
          maxItems: 50
          description: At most 50 parcels, or 10 when Nucleo makes the labels.
          items:
            allOf:
              - $ref: '#/components/schemas/ParcelInput'
              - type: object
                required: [tracking_number]
        lines:
          type: array
          description: Partial shipment only. Omit it to ship everything picked.
          items:
            allOf:
              - $ref: '#/components/schemas/ItemRef'
              - type: object
                required: [quantity]
                properties:
                  quantity:
                    type: integer
                    minimum: 1
        return_carrier_code:
          type: [string, 'null']
          maxLength: 60
          description: Carrier of the prepaid return label in the parcel, if any.
        return_tracking_number:
          type: [string, 'null']
          maxLength: 120
    Shipment:
      type: object
      properties:
        id:
          type: string
          format: uuid
        order_number:
          type: string
        order_status:
          type: string
          description: Status of your part of the order after the shipment.
        carrier_code:
          type: [string, 'null']
        carrier_name:
          type: [string, 'null']
        service_code:
          type: [string, 'null']
        tracking_url:
          type: [string, 'null']
        shipped_at:
          type: [string, 'null']
          format: date-time
        parcels:
          type: array
          items:
            type: object
            properties:
              number:
                type: integer
              reference:
                type: [string, 'null']
              tracking_number:
                type: [string, 'null']
              weight_g:
                type: [integer, 'null']
        lines:
          type: array
          items:
            type: object
            properties:
              line_id:
                type: string
                format: uuid
              sku:
                type: [string, 'null']
              barcode:
                type: [string, 'null']
              quantity:
                type: integer
    LabelList:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Label'
    Label:
      type: object
      properties:
        parcel:
          type: integer
          description: Parcel number, from 1.
        status:
          type: string
          enum: [ready, failed]
        carrier_code:
          type: [string, 'null']
        carrier_name:
          type: [string, 'null']
        tracking_number:
          type: [string, 'null']
        format:
          type: string
          enum: [pdf, zpl]
        content_type:
          type: string
          enum: [application/pdf, application/x-zpl]
        content_base64:
          type: [string, 'null']
          description: The label file, base64-encoded; `null` when `status` is `failed`.
    Document:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        kind:
          type: string
          enum: [order_document, insert]
        position:
          type: [string, 'null']
          enum: [internal, external, null]
        page_format:
          type: string
          example: A4
        content_type:
          type: string
          const: application/pdf
        content_base64:
          type: string
    StockRequest:
      type: object
      required: [items]
      properties:
        items:
          type: array
          minItems: 1
          maxItems: 500
          items:
            allOf:
              - $ref: '#/components/schemas/ItemRef'
              - type: object
                required: [quantity]
                properties:
                  location_code:
                    type: [string, 'null']
                    maxLength: 40
                    description: Required when your connection has more than one stock location code.
                  quantity:
                    type: integer
                    minimum: 0
                    description: Absolute level, not an increment.
                  classification:
                    type: string
                    enum: [sellable, damaged, quarantine, reserved, unsellable]
                    default: sellable
                  counted_at:
                    type: [string, 'null']
                    format: date-time
    StockResult:
      type: object
      properties:
        accepted:
          type: integer
          description: Items applied.
        ignored:
          type: array
          items:
            type: object
            properties:
              index:
                type: integer
                description: Position in `items`, from 0.
              sku:
                type: [string, 'null']
              barcode:
                type: [string, 'null']
              reason:
                type: string
                enum: [unknown_item, stale]
                description: '`unknown_item`: not in the catalog. `stale`: `counted_at` older than the level already applied.'
        warnings:
          type: array
          items:
            type: object
            properties:
              location_code:
                type: string
              reason:
                type: string
                enum: [location_not_mapped]
              message:
                type: string
    Return:
      type: object
      properties:
        id:
          type: string
          format: uuid
        number:
          type: string
        status:
          type: string
          enum: [expected, received, cancelled]
        outcome:
          type: [string, 'null']
          enum: [compliant, not_compliant, null]
        unannounced:
          type: boolean
          description: Created by a warehouse from a parcel nobody announced.
        order_number:
          type: [string, 'null']
        reason:
          type: object
          properties:
            code:
              type: [string, 'null']
            text:
              type: [string, 'null']
        customer_note:
          type: [string, 'null']
        return_tracking:
          type: object
          properties:
            carrier_code:
              type: [string, 'null']
            tracking_number:
              type: [string, 'null']
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        accepted_at:
          type: [string, 'null']
          format: date-time
        received_at:
          type: [string, 'null']
          format: date-time
        cancelled_at:
          type: [string, 'null']
          format: date-time
        lines:
          type: array
          items:
            type: object
            properties:
              line_id:
                type: string
                format: uuid
              order_line_id:
                type: [string, 'null']
                format: uuid
              sku:
                type: [string, 'null']
              barcode:
                type: [string, 'null']
              item_key:
                type: [string, 'null']
              title:
                type: [string, 'null']
              quantity_expected:
                type: integer
              quantity_received:
                type: [integer, 'null']
              condition:
                type: [string, 'null']
                enum: [sellable, damaged, defective, used, wrong_item, mixed, null]
                description: '`mixed` when you reported the same item in more than one condition.'
              reason_code:
                type: [string, 'null']
              note:
                type: [string, 'null']
    ReceiptLine:
      allOf:
        - $ref: '#/components/schemas/ItemRef'
        - type: object
          required: [quantity, condition]
          properties:
            quantity:
              type: integer
              minimum: 0
            condition:
              type: string
              enum: [sellable, damaged, defective, used, wrong_item]
            note:
              type: [string, 'null']
              maxLength: 500
    ReceiptRequest:
      type: object
      required: [lines]
      properties:
        received_at:
          type: [string, 'null']
          format: date-time
        lines:
          type: array
          minItems: 1
          maxItems: 500
          items:
            $ref: '#/components/schemas/ReceiptLine'
    UnannouncedRequest:
      type: object
      required: [order_number, lines]
      properties:
        order_number:
          type: string
          description: The order the goods belong to (the `#` can be omitted).
        reference:
          type: [string, 'null']
          maxLength: 120
          description: Your reference for the parcel.
        received_at:
          type: [string, 'null']
          format: date-time
        lines:
          type: array
          minItems: 1
          maxItems: 500
          items:
            $ref: '#/components/schemas/ReceiptLine'
    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:
          type: string
          enum: [order.ready, order.cancelled, return.expected]
        created_at:
          type: string
          format: date-time
        data:
          type: object
          description: |
            `order.ready`: `order_id`, `order_number`. `order.cancelled`: `order_id`, `order_number`,
            `status` (`cancelled` or `cancel_requested`). `return.expected`: `return_id`, `return_number`,
            `order_number`.
          additionalProperties: true
