> For the complete documentation index, see [llms.txt](https://docs.fulfillmenttools.com/documentation/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.fulfillmenttools.com/documentation/by-pillar/store-operations/handover.md).

# Handover

{% hint style="info" %}
This article focuses on the handover process for developers. For information on the handovers in the store operations process, see the [Handover article](/documentation/apps/operations-app/handover.md) in the [Operations app section](/documentation/apps/operations-app.md).
{% endhint %}

A handover job represents the handover of an order to the consumer or shipping provider.

In addition to general information such as the order date, tenant order ID, or a reference to the corresponding facility, a handover job also carries handover-relevant information. This covers:

* **Handover channel:**
  * **Delivery**: Will be handed over to a shipping service provider
  * **Pick up**: Will be handed over to the end customer
* **Handover job cancel reason**: Can be defined if the parcel(s) weren't handed over

fulfillmentools automatically creates a handover job in the following scenarios:

* The status of the pick job changes to `CLOSED`, and:
  * Packing isn't active.
  * All linked service jobs associated with the pick job are `FINISHED`.
* The status of the linked service jobs changes to `FINISHED`, and:
  * Packing isn't active.
  * The related pick job is in a `CLOSED` status.
* The status of the pack job changes to `CLOSED`, and:
  * Packing is active.
  * All linked service jobs associated with the pick job are `FINISHED`.
* The status of the parcel changes to `DONE`, and:
* The related pick job is in a `CLOSED` status.
* `deliveryChannel` is `SHIPPING`.

For shipments processed through a custom carrier, handover job creation depends on the carrier configuration. If `manualParcelHandlingActive` is active, fulfillmenttools doesn't create a handover job when a pick job, linked service job, or pack job closes. Instead, creation is deferred until the parcel reaches `DONE` status. If `manualParcelHandlingActive` is deactivated, the standard triggers above apply.

## Automatic versus manual handover job completion

A handover job is completed (set to status `HANDED_OVER`**) automatically** when a track-and-trace event is received from a carrier with track-and-trace enabled (if a [carrier integration](/documentation/by-pillar/store-operations/carrier-management.md) is in place).

It's possible to **manually** set a handover job to status `HANDED_OVER`, which triggers the handover event even before the carrier has physically scanned or picked up the parcel.\
In this case, the information from the **handover job**, such as items and quantities, is used to mark all associated line items as handed over. This ensures that line item quantities are passed correctly.

{% hint style="warning" %}
Currently, it's not possible to restrict the manual action based on user roles or permissions. As a result, users can manually trigger a handover event even before the carrier picks up the parcel.
{% endhint %}

## Handover configuration

The handover configuration allows users to define rules for the handover process.

The handover configuration is available at `/api/configurations/handover` (see the [handover configuration endpoint section](#handover-configuration-endpoints) for more information) and affects the whole system regardless of the user role or the facility.

In the handover configuration, you can define:

* [The reasons a handover was refused](#handover-refusal-reasons)

### Handover refusal reasons

The refused reason configuration defines the options a user can choose when a consumer refuses a handover job, and the product is marked with the reason.

You need to add the `availableRefusedReasons` array. For the option to be available, `active` must be set to `true`. You can then input a `refusedReasonLocalized` object and set up different inputs for different languages.

#### Localization for reasons

The response has a `refusedReason` field that contains one of the locales provided by the `refusedReasonLocalized` object. Which exact translation was chosen depends on the locale set in the authorization token when sending the request to the GET endpoint.

If no locale was provided by the client or the locale is not available in the `refusedReasonLocalized` object, the answer will default to the tenant locale. If the tenant locale is also not available in the `refusedReasonLocalized` object, then the first key-value pair will be selected.

### Handover configuration endpoints

To access the current handover configuration, use the endpoint below:

```http
GET https://{YOUR_TENANT_NAME}.api.fulfillmenttools.com/api/configurations/handover
```

To update the current handover configuration, use the endpoint below:

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

```http
PUT https://{YOUR_TENANT_NAME}.api.fulfillmenttools.com/api/configurations/handover
```

{% endtab %}

{% tab title="Request body" %}

```json
{
    "version": 2,
    "availableRefusedReasons": [
        {
            "active": true,
            "refusedReasonLocalized": {
                "de_DE": "Falsche farbe",
                "fr_FR": "Mauvaise couleur",
                "en_US": "Wrong color"
            }
        },
        {
            "active": false,
            "refusedReasonLocalized": {
                "de_DE": "Falsche größe",
                "fr_FR": "Mauvaise taille",
                "en_US": "Wrong size"
            }
        }
    ]
}
```

{% endtab %}

{% tab title="Response body" %}

```json
{
    "id": "handover",
    "version": 3,
    "availableRefusedReasons": [
        {
            "active": true,
            "refusedReasonLocalized": {
                "de_DE": "Falsche farbe",
                "fr_FR": "Mauvaise couleur",
                "en_US": "Wrong color"
            },
            "refusedReason": "Wrong color"
        },
        {
            "active": false,
            "refusedReasonLocalized": {
                "de_DE": "Falsche größe",
                "fr_FR": "Mauvaise taille",
                "en_US": "Wrong size"
            },
            "refusedReason": "Wrong size"
        }
    ],
    "created": "2025-12-04T13:54:45.549Z",
    "lastModified": "2026-03-05T09:29:16.408Z"
}
```

{% endtab %}
{% endtabs %}

## Update handover configuration

> This part of the API is in Beta status. For details please check the \<a href="<https://docs.fulfillmenttools.com/documentation/developer-docs/api/core-concepts/api-release-life-cycle#beta>" target="\_blank">api-release-life-cycle documentation\</a>.\<br />\<br />Updates the handover configuration for the tenant. If the configuration does not exist, it will be created.

```json
{"openapi":"3.0.1","info":{"title":"fulfillmenttools","version":"VERSIONLESS"},"tags":[{"description":"Endpoints to create, update, and read handover configuration.","name":"Handovers Configuration (Operations)"}],"servers":[{"url":"https://{tenant}.api.fulfillmenttools.com","variables":{"tenant":{"default":"your-tenant-name"}}}],"security":[{"BearerToken":[]}],"components":{"securitySchemes":{"BearerToken":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"HandoverConfigurationForCreate":{"additionalProperties":false,"properties":{"availableRefusedReasons":{"items":{"$ref":"#/components/schemas/AvailableRefuseReasonForCreation"},"type":"array"},"createStandaloneHandoverJobs":{"deprecated":true,"description":"This part of the API is deprecated. For details please check the <a href=\"https://docs.fulfillmenttools.com/documentation/developer-docs/api/core-concepts/api-release-life-cycle#deprecated\" target=\"_blank\">api-release-life-cycle documentation</a>.<br /><br />If true, a handover job will be created out of a valid routing plan. It is only possible, if no pickjob nor packjobs are used for this tenant. This is a alpha feature and might be completly replaced in the future. Please only use in communication with the FFT team.","type":"boolean"},"version":{"type":"number"}},"required":["version"],"type":"object","title":"HandoverConfigurationForCreate","description":"HandoverConfigurationForCreate"},"AvailableRefuseReasonForCreation":{"additionalProperties":false,"properties":{"refusedReasonLocalized":{"$ref":"#/components/schemas/LocaleString","description":"Localized reason"},"active":{"type":"boolean"}},"required":["active","refusedReasonLocalized"],"title":"AvailableRefuseReasonForCreation","description":"AvailableRefuseReasonForCreation"},"LocaleString":{"additionalProperties":{"type":"string"},"description":"Provides Localized values. The key is the locale, the value is the translation. https://docs.fulfillmenttools.com/documentation/developer-docs/api/core-concepts/localization","title":"LocaleString","type":"object"},"HandoverConfiguration":{"allOf":[{"$ref":"#/components/schemas/VersionedResource"}],"properties":{"availableRefusedReasons":{"items":{"$ref":"#/components/schemas/AvailableRefusedReason"},"type":"array"},"createStandaloneHandoverJobs":{"deprecated":true,"description":"This part of the API is deprecated. For details please check the <a href=\"https://docs.fulfillmenttools.com/documentation/developer-docs/api/core-concepts/api-release-life-cycle#deprecated\" target=\"_blank\">api-release-life-cycle documentation</a>.<br /><br />If true, a handover job will be created out of a valid routing plan. It is only possible, if no pickjob nor pack jobs are used for this tenant. This is a alpha feature and might be completely replaced in the future. Please only use in communication with the fulfillmenttools team.","type":"boolean"}},"type":"object","title":"HandoverConfiguration","description":"HandoverConfiguration"},"VersionedResource":{"properties":{"created":{"description":"The date this entity was created at the platform. This value is generated by the service.","format":"date-time","type":"string"},"lastModified":{"description":"The date this entity was modified last. This value is generated by the service.","format":"date-time","type":"string"},"version":{"description":"The version of the document to be used in optimistic locking mechanisms.","format":"int64","type":"integer"}},"required":["version"],"type":"object","title":"VersionedResource","description":"VersionedResource"},"AvailableRefusedReason":{"additionalProperties":false,"properties":{"refusedReasonLocalized":{"$ref":"#/components/schemas/LocaleString"},"active":{"type":"boolean"},"refusedReason":{"description":"translated refusedReason selected from refusedReasonLocalized","type":"string"}},"required":["refusedReasonLocalized","active"],"title":"AvailableRefusedReason","description":"AvailableRefusedReason"},"ApiError":{"items":{"$ref":"#/components/schemas/ErrorInner"},"type":"array","xml":{"name":"ApiError"},"title":"ApiError","description":"ApiError"},"ErrorInner":{"properties":{"description":{"type":"string"},"requestVersion":{"description":"The version provided within an invalid request.","format":"int64","type":"integer"},"summary":{"type":"string"},"version":{"format":"int64","type":"integer"}},"required":["summary"],"type":"object","title":"ErrorInner","description":"ErrorInner"}}},"paths":{"/api/configurations/handover":{"put":{"description":"This part of the API is in Beta status. For details please check the <a href=\"https://docs.fulfillmenttools.com/documentation/developer-docs/api/core-concepts/api-release-life-cycle#beta\" target=\"_blank\">api-release-life-cycle documentation</a>.<br /><br />Updates the handover configuration for the tenant. If the configuration does not exist, it will be created.","operationId":"upsertHandoverConfiguration","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HandoverConfigurationForCreate"}}},"description":"Desired HandoverConfiguration","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HandoverConfiguration"}}},"description":"The handover configuration was successfully updated."},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"description":"Invalid input. See response for details"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"description":"Your user is not allowed to operate against this API instance"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"description":"Your user, although recognized, is not authorized to use this endpoint"}},"summary":"Update handover configuration","tags":["Handovers Configuration (Operations)"]}}}}
```

## Moving handover job line items

A handover job organizes its line items across three distinct arrays, each representing a different state in the handover process:

| Array                          | Type                           | Meaning                                                  |
| ------------------------------ | ------------------------------ | -------------------------------------------------------- |
| `handoverJobLineItems`         | `HandoverLineItem[]`           | Items that are physically present and ready to hand over |
| `expectedHandoverJobLineItems` | `ExpectedHandoverLineItem[]`   | Items that are expected but not yet confirmed present    |
| `missingHandoverJobLineItems`  | `MissingHandoverJobLineItem[]` | Items that have been confirmed as missing                |

The `MOVE_HANDOVER_JOB_LINE_ITEMS` action allows redistribution of line items across these arrays. Partial moves are supported. A move instruction can target fewer units than the full quantity of a source item. In that case, the source entry has its quantity reduced, and the target receives a new entry with a new identifier.

### Allowed move directions

All combinations of different source and target arrays are permitted:

| Source (`from`) | Target (`to`) | Notes                                                              |
| --------------- | ------------- | ------------------------------------------------------------------ |
| `EXPECTED`      | `HANDOVER`    |                                                                    |
| `EXPECTED`      | `MISSING`     |                                                                    |
| `MISSING`       | `HANDOVER`    |                                                                    |
| `MISSING`       | `EXPECTED`    | `transferId` and `globalLineItemId` aren't carried over            |
| `HANDOVER`      | `MISSING`     | Quantity limit applies (see [validation rules](#validation-rules)) |
| `HANDOVER`      | `EXPECTED`    | `transferId` and `globalLineItemId` aren't carried over            |

Moving a line item within the same array (for example, `EXPECTED` to `EXPECTED`) isn't permitted.

### Effect on handover job status

The handover job status is recalculated after every move operation based on the state of `expectedHandoverJobLineItems`:

* If one or more expected items remain, the handover job status is set to `WAITING_FOR_INPUT`.
* If no expected items remain, the handover job status is set to `OPEN`.

### Move handover endpoints

Use the endpoint below to move handover job line items:

```http
POST /api/handoverjobs/{id}/actions
```

**Required permission:** `HANDOVERJOB_WRITE`

#### Request body

```json
{
  "name": "MOVE_HANDOVER_JOB_LINE_ITEMS",
  "version": 3,
  "items": [
    {
      "lineItemId": "<id-of-source-line-item>",
      "from": "EXPECTED",
      "to": "HANDOVER",
      "targetQuantity": 2
    }
  ]
}
```

| Field     | Type                             | Description                                    |
| --------- | -------------------------------- | ---------------------------------------------- |
| `name`    | `string`                         | Must be `"MOVE_HANDOVER_JOB_LINE_ITEMS"`       |
| `version` | `number`                         | Optimistic-locking version of the handover job |
| `items`   | `MoveHandoverJobLineItemsInfo[]` | One or more move instructions                  |

**`MoveHandoverJobLineItemsInfo` fields**

| Field            | Type                                        | Description                                                                     |
| ---------------- | ------------------------------------------- | ------------------------------------------------------------------------------- |
| `lineItemId`     | `string`                                    | Identifier of the line item to move. Must exist in the `from` array.            |
| `from`           | `"EXPECTED"` \| `"HANDOVER"` \| `"MISSING"` | Source array.                                                                   |
| `to`             | `"EXPECTED"` \| `"HANDOVER"` \| `"MISSING"` | Target array. Must differ from `from`.                                          |
| `targetQuantity` | `number`                                    | Number of units to move. Must be at least 1 and at most the available quantity. |

#### Response

Returns the updated `Handoverjob` object (HTTP 200). Article titles in the response are localized based on the locale set in the request's authorization token.

### Validation rules

All validation is evaluated before any mutations are applied. If multiple errors are present, they are collected and returned together.

**Request-level checks**

| Rule                                                                         | Error                  |
| ---------------------------------------------------------------------------- | ---------------------- |
| `items` array is empty                                                       | `ValidationError`      |
| `version` in the request doesn't match the stored entity version             | `VersionConflictError` |
| Handover job status isn't `OPEN` or `WAITING_FOR_INPUT`                      | `ValidationError`      |
| Two or more entries in `items` share the same `lineItemId` and the same `to` | `ValidationError`      |

**Per-item checks**

| Rule                                                                                    | Error condition                                     |
| --------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `targetQuantity` is less than 1                                                         | Must be at least 1                                  |
| `from` and `to` are identical                                                           | Source and target must differ                       |
| `lineItemId` does not exist in the `from` array                                         | Line item not found in source                       |
| `targetQuantity` exceeds `lineItem.quantity`                                            | Can't move more than the available quantity         |
| Source is `HANDOVER` and `targetQuantity` exceeds `quantity` minus `handedOverQuantity` | Can't move units that have already been handed over |

### New line item properties

Each move instruction creates a new entry in the target array. The new entry receives a new `id`. The following describes which fields are carried over from the source and which are set to fixed values:

**When moving to `HANDOVER`**

| Field                                                               | Value              |
| ------------------------------------------------------------------- | ------------------ |
| `id`                                                                | New identifier     |
| `globalLineItemId`                                                  | New identifier     |
| `quantity`                                                          | `targetQuantity`   |
| `handedOverQuantity`                                                | `0`                |
| `status`                                                            | `OPEN`             |
| `article`, `tags`, `stickers`, `scannableCodes`, `customAttributes` | Copied from source |

**When moving to `MISSING`**

| Field                                                               | Value              |
| ------------------------------------------------------------------- | ------------------ |
| `id`                                                                | New identifier     |
| `quantity`                                                          | `targetQuantity`   |
| `article`, `tags`, `stickers`, `scannableCodes`, `customAttributes` | Copied from source |

The fields `transferId`, `handedOverQuantity`, `globalLineItemId`, and `status` aren't present on `MissingHandoverJobLineItem` and are not set.

**When moving to `EXPECTED`**

| Field                                                               | Value                 |
| ------------------------------------------------------------------- | --------------------- |
| `id`                                                                | New identifier        |
| `quantity`                                                          | `targetQuantity`      |
| `transferId`                                                        | Not set (`undefined`) |
| `article`, `tags`, `stickers`, `scannableCodes`, `customAttributes` | Copied from source    |

The fields `handedOverQuantity`, `globalLineItemId`, and `status` are not present on `ExpectedHandoverJobLineItem` and are not set.

{% hint style="info" %}
When a line item is moved to `EXPECTED`, the `transferId` from the source item is never carried over. The new entry always has `transferId` set to `undefined`, regardless of the source array.
{% endhint %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.fulfillmenttools.com/documentation/by-pillar/store-operations/handover.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
