# Headless picking

Headless picking refers to managing the pick job lifecycle entirely through the fulfillmenttools API, without using the fulfillmenttools Operations app. This approach is typically chosen by retailers or logistics operators who already have their own warehouse or store fulfillment applications and want to integrate fulfillmenttools as the backend order management engine while keeping their existing frontend experience for store or warehouse staff.

In this setup, your custom application is responsible for guiding pickers through the fulfillment process: displaying items to pick, capturing the quantities picked, and reporting results via the API. fulfillmenttools handles the underlying inventory logic, stock reservations, and order orchestration.

{% hint style="success" %}

## Prerequisites

Before implementing headless picking, ensure the following are in place:

* **Orders:** Orders must be created and managed through fulfillmenttools. Pick jobs are generated automatically when an order is assigned to a facility, so order creation is a hard dependency.
* **Pick jobs:** Your integration must be able to interact with the pick job resource via the fulfillmenttools API (`/api/pickjobs`). This includes reading pick job data and updating statuses and line items as described in this guide.
  {% endhint %}

When an order is created and assigned to a facility, fulfillmenttools automatically generates a pick job and reduces available stock by creating a reservation.

Once your facility has physically picked the items, follow these steps via API to complete the process:

{% stepper %}
{% step %}
**Set the pick job to IN\_PROGRESS**

Using the pick job action endpoint, use the name `START` to set the pick job to `IN_PROGRESS`.

Setting the status to `IN_PROGRESS` triggers the system to fetch the latest stock data and populate the `stockRef` values inside `partialStockLocations` for each pick line item.

{% tabs %}
{% tab title="Endpoint" %}

```http
POST /api/pickjobs/{pickJobId}/actions
```

{% endtab %}

{% tab title="Request body" %}

```json
{
  name: "START"
  version: 1
}
```

{% hint style="info" %}
The `IN_PROGRESS` transition isn't just a status change, it's what triggers the stock refresh.
{% endhint %}
{% endtab %}

{% tab title="Response body" %}

<pre class="language-json"><code class="lang-json">{
  "id": "019cc21a-3b14-7538-b293-26ad30c24317",
  "version": 2,
  "created": "2026-03-06T08:00:00.000Z",
  "lastModified": "2026-03-06T08:05:00.000Z",
  "shortId": "AB42",
  "status": "IN_PROGRESS",
  "facilityRef": "Esb20gpHBL94X5NdMp3C",
  "orderRef": "LGMl2DuvPnfPoSHhYFOm",
  "tenantOrderId": "R456728546",
  "orderDate": "2026-03-06T07:50:00.000Z",
  "deliveryinformation": {
    "channel": "SHIPPING",
    "targetTime": "2026-03-07T12:00:00.000Z"
  },
  "pickLineItems": [
    {
      "id": "019cc21a-3b14-7538-b293-26ad30c24317",
      "status": "OPEN",
      "quantity": 1,
      "picked": 0,
      "article": {
        "tenantArticleId": "ART-001",
        "title": "Blue Running Shoe (Size 42)"
      },
<strong>      "partialStockLocations": [
</strong>        {
<strong>          "stockRef": "stock-7f3a1b29-e4dc-4e8a-bc12-0f5a93d1e847",
</strong>          "tenantPartialStockId": "STOCK-001",
          "quantity": 1,
          "available": 5
        }
      ]
    }
  ],
  "documentsRef": []
}
</code></pre>

{% hint style="info" %}
The response already contains the updated `stockRef` values inside `partialStockLocations` for each pick line item. No separate GET request is needed.
{% endhint %}

**Understanding `partialStockLocations`:**

* A pick line item can have multiple `partialStockLocations`, each representing a stock from which the item could potentially be picked
* The stock with the active reservation will have `quantity` set to the reserved amount
* All other stocks will have `quantity: 0`
* Use the `stockRef` of the entry with a non-zero `quantity` when submitting picked quantities in the next step
  {% endtab %}
  {% endtabs %}

The sum of all picked pick line items must be `0` to trigger fetching the latest stock data. This is the case for newly created pick jobs via an order. You can also achieve this by restarting a pick job. If the sum isn't `0`, we assume the pick job has already started. It's the user's responsibility to ensure the stock information remains correct.
{% endstep %}

{% step %}
**Submit the picked quantities**

Using the same action endpoint, update the picked quantities.

{% tabs %}
{% tab title="Endpoint" %}

```http
POST /api/pickjobs/{pickJobId}/actions
```

{% endtab %}

{% tab title="Request body" %}

```json
{
  "lineItems": [
    {
      "id": "019cc21a-3b14-7538-b293-26ad30c24317",
      "partialStockLocations": [
        {
          "picked": 1,
          "stockRef": "stock-7f3a1b29-e4dc-4e8a-bc12-0f5a93d1e847",
          "tenantPartialStockId": "STOCK-001"
        }
      ],
      "picked": 1,
      "pickedAt": "{{$isoTimestamp}}" //optional
    }
  ],
  "name": "PICK",
  "version": 2
}
```

{% hint style="warning" %}
`picked` must be set on both the root level of the line item update and inside each `partialStockLocations` entry.

* **Root level:** total quantity picked for the line item
* **Inside `partialStockLocations`:** quantity taken from that specific stock

If the `picked` value is missing or zero at the `partialStockLocations` level, no stock will be deducted from inventory.
{% endhint %}
{% endtab %}

{% tab title="Response body" %}

```json
{
  "id": "019cc21a-3b14-7538-b293-26ad30c24317",
  "version": 3,
  "created": "2026-03-06T08:00:00.000Z",
  "lastModified": "2026-03-06T08:05:00.000Z",
  "shortId": "AB42",
  "status": "PICKED",
  "facilityRef": "Esb20gpHBL94X5NdMp3C",
  "orderRef": "LGMl2DuvPnfPoSHhYFOm",
  "tenantOrderId": "R456728546",
  "orderDate": "2026-03-06T07:50:00.000Z",
  "deliveryinformation": {
    "channel": "SHIPPING",
    "targetTime": "2026-03-07T12:00:00.000Z"
  },
  "pickLineItems": [
    {
      "id": "019cc21a-3b14-7538-b293-26ad30c24317",
      "status": "CLOSED",
      "quantity": 1,
      "picked": 1,
      "pickedAt": "{{$isoTimestamp}}",
      "article": {
        "tenantArticleId": "ART-001",
        "title": "Blue Running Shoe (Size 42)"
      },
      "partialStockLocations": [
        {
          "stockRef": "stock-7f3a1b29-e4dc-4e8a-bc12-0f5a93d1e847",
          "tenantPartialStockId": "STOCK-001",
          "quantity": 1,
          "available": 5
        }
      ]
    }
  ],
  "documentsRef": []
}
```

{% endtab %}
{% endtabs %}

**What happens after submission:**

* The pick job status is automatically set to `PICKED`.
* If all picked quantities match the ordered quantities, no rerouting is needed, and the pick job transitions to `CLOSED` automatically (asynchronously).
* Once `CLOSED`, picked quantities are deducted from stock, and reservations are removed. No manual adjustment is needed.
* If `picked` is `0` for all line items, the pick job will be set to `ABORTED`.
* If the `picked` quantities are less than the ordered quantity (short-pick), the pick job will either be `REROUTED` or `CLOSED`, depending on your configuration.
  {% endstep %}
  {% endstepper %}

An example flow of this process is below, where the numbers represent the steps above.

<figure><img src="https://4170739437-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLrrr5jgTsDuR38gNJIrm%2Fuploads%2FKck307rq0KDFhoRJk4se%2FHeadless%20picking%20flow.png?alt=media&#x26;token=992442a0-f3a1-47ff-9e71-94feb11434ee" alt=""><figcaption></figcaption></figure>
