Headless Order Fulfillment

As part of the MACH-Alliance, being headless is one of the key parts of the fulfillmenttools platform. This article shows you how your order might be fulfilled without the Android or Web client.

This page is outdated. Please go to our new documentation under https://docs.fulfillmenttools.com/documentation.

As you have seen in the Order Flow chart, after an order was created and the DOMS routed it, Pick Jobsare created. Most customers pick them using our mobile or web client, but let's imagine there are reasons not to use it. For example you use our app in your stores and a chaotic storage system in your warehouse. In the stores you'd like to pick using our picking app, in the warehouse you want to keep the old WMS picking solution. This is supported by our API, let's have a look how you can do that.

Picking

As you might know, orders are never picked, what is picked is called PickJob. After an order was routed, a routing plan is created which creates PickJobs in 1-n facilities. For each created PickJob, a PICK_JOB_CREATED event occurs, which you could subscribe to. The pickjob contains information on the orders to pick and the locations where to pick them from:

{
  "routingPlanRef": "f328b21d-8e59-4f2d-acdc-7207c89edecc",
  "facilityRef": "bef04548-dacf-402e-a692-48aecd008d08",
  "orderRef": "3ee8e01c-9af0-4af1-82fc-b106c2fe09a1",
  "orderDate": "2024-06-13T13:28:48.000Z",
  "tenantOrderId": "SOME-ORDER-ID",
  "status": "OPEN",
  "pickLineItems": [
    {
      "quantity": 1,
      "status": "OPEN",
      "scannableCodes": [],
      "article": {
        "tenantArticleId": "SHIRT-W-1234",
        "title": "White T-Shirt",
        "titleLocalized": null,
        "attributes": [
          {
            "category": "descriptive",
            "priority": 100,
            "key": "size",
            "value": "L"
          }
        ],
        "prices": [
          {
            "pricePerUnit": 39.9,
            "currency": "EUR"
          }
        ]
      },
      "secondaryPicked": 0,
      "picked": 0,
      "id": "82ff449c-56b5-4053-a93f-bc7559a13655",
      "partialStockLocations": [
        {
          "quantity": 1,
          "tenantPartialStockId": "a9bdece5-a684-486e-893f-24a68af9ffd8",
          "location": null,
          "available": 1,
          "ratingScore": 0,
          "sequenceScore": null,
          "stockProperties": {}
        },
        {
          "quantity": 0,
          "tenantPartialStockId": "a274a87e-58c4-45d1-bb7e-4c0b70bf9b8b",
          "location": null,
          "available": 0,
          "ratingScore": 0,
          "sequenceScore": null,
          "stockProperties": {}
        }
      ],
      "tags": [],
      "scanningRules": {
        "scanningType": [
          {
            "priority": 0,
            "scanningRuleType": "ARTICLE"
          }
        ],
        "scanningMode": "SCAN_NOT_REQUIRED"
      },
      "scanningRule": {
        "values": [
          {
            "priority": 0,
            "scanningRuleType": "ARTICLE"
          }
        ]
      }
    }
  ],
  "deliveryinformation": "left out for privacy",
  "processId": "d9200d3f-e322-4674-8747-ace7f82a164e",
  "shortId": "CX69",
  "paymentInformation": {
    "currency": "IDR"
  },
  "preferredPickingMethods": [
    "MULTI_ORDER"
  ],
  "customAttributes": null,
  "id": "7b71ac26-401b-478c-a297-3d7eda45b804",
  "tags": [
    {
      "id": "sales_channel",
      "value": "MP"
    }
  ],
  "documentHandling": {
    "sendLabel": {
      "enabled": true
    }
  },
  "documentsRef": "5459b5d5-8935-45c1-9da7-9c0ee9fa1e8c",
  "stickers": [],
  "operativeProcessRef": "17a5ea92-7c03-4f6e-b550-3533abc3130b",
  "created": "2024-06-13T13:28:52.532Z",
  "lastModified": "2024-06-13T13:28:52.532Z",
  "version": 1
}

After receiving the event containing that PickJob you can pick the goods. When sending the result of that picking it is crucial to tell the platform which items were collected, this information is stored in the partialStockLocations. If you are unsure which location to take from the list, we recommend using the first item in the list where the quantity is higher than 0, in the example above it is the first item in the list. After you picked with your solution and want report the result to the platform, you can use the PATCH PickJob endpoint:

curl -sSL -X PATCH 'https://your.api.fulfillmenttools.com/api/pickjobs/<PickJobId>' \
--header 'Authorization: Bearer <TOKEN>' \
--header 'Content-Type: application/json' \
--data-raw '{
  "version": 1,
  "actions": [
    {
      "action": "ModifyPickJob",
      "status": "IN_PROGRESS"
    },
    {
      "action": "ModifyPickLineItem",
      "id": "82ff449c-56b5-4053-a93f-bc7559a13655",
      "picked": 1,
      "scannedCodes": [
        {
          "code": "2021-0018",
          "quantity": 1
        }
      ],
      "status": "CLOSED",
      "partialStockLocations": [
        {
          "tenantPartialStockId": "a9bdece5-a684-486e-893f-24a68af9ffd8",
          "picked": 1
        }
      ]
    }
  ]
}
'

After reporting the picking result, the PickJob can be closed using the same endpoint:

curl -sSL -X PATCH 'https://your.api.fulfillmenttools.com/api/pickjobs/<PickJobId>' \
--header 'Authorization: Bearer <TOKEN>' \
--header 'Content-Type: application/json' \
--data-raw '{
    "version": 2,
    "actions": [
        {
            "action": "ModifyPickJob",
            "status": "CLOSED"
        }
    ]
}'

Assuming you have either picked everything or ordersplit after short pick is enabled, a PackJob will be created and a PICK_JOB_PICKING_FINISHED event is thrown as well as a PACK_JOB_CREATED event. If an order is rerouted after a shortpick, you will receive a PICK_JOB_REROUTED event.

Packing

When the PackJob was created, the PACK_JOB_CREATED event will occur. Subscribing to that, packing with a solution which is not provided by fulfillmenttools, won't be an issue. Using the PATCH PackJob Endpoint, you can report the packed items to the platform:

curl -sSL -X PATCH 'https://your.api.fulfillmenttools.com/api/packjobs/<PackJobId>' \
--header 'Authorization: Bearer <TOKEN>' \
--header 'Content-Type: application/json' \
--data-raw '{
    "version": 2,
    "actions" :[
        {
            "action": "ModifyPackLineItem",
            "id": "5a07f221-c7ca-4748-a59b-8d705c8989db",
            "packed": 6
        },
        {
            "action": "ModifyPackLineItem",
            "id": "020c9dce-58a6-40eb-8950-98de0933f5d0",
            "packed": 2
        }
    ]
}'

After that the PackJob should also be closed:

curl -sSL -X PATCH 'https://your.api.fulfillmenttools.com/api/packjobs/<PackJobId>' \
--header 'Authorization: Bearer <TOKEN>' \
--header 'Content-Type: application/json' \
--data-raw '{
    "version": 3,
    "actions": [
        {
            "action": "ModifyPackJob",
            "status": "CLOSED"
        }
    ]
}'

Shipping

Assuming you have configured the carrier DHL and use the same parcel classification every time, you don't need to specify it when trying to get a label. In our platform, a label is something that belongs to a parcel and a parcel belongs to a shipment. Each shipment can have multiple parcels, but each parcel has its own and unique label.

That said, you need to add a parcel to a shipment. In each response of the API calls mentioned above, you get an object containing a processId:

{
    "routingPlanRef": "710fa76d-5699-4653-8cc7-954e61baf2f1",
    "facilityRef": "3a091284-e434-467b-aa3b-da72c28f50f7",
    "orderRef": "710fa76d-5699-4653-8cc7-954e61baf2f1",
    "orderDate": "2023-08-02T14:53:36.918Z",
    "tenantOrderId": null,
    "status": "CLOSED",
    "pickLineItems": ["shortened for documentation!"],
    "deliveryinformation": {
      "targetTime": "2024-06-15T05:00:00.000Z",
      "targetTimeBaseDate": null,
      "channel": "SHIPPING"
    },
    "processId": "e43ff765-88d2-452f-85d6-57adab58a832",
    "shortId": "SG2",
    "paymentInformation": null,
    "preferredPickingMethods": [
        "SINGLE_ORDER"
    ],
    "id": "3411701b-5c05-4220-97d1-c3eb5846a0ae",
    "customAttributes": null,
    "documentHandling": {
        "sendLabel": {
            "enabled": true
        }
    },
    "documentsRef": "0c014957-e925-49de-812e-0a7f333256df",
    "stickers": [],
    "created": "2023-08-07T08:37:44.070Z",
    "lastModified": "2023-11-07T13:47:57.909Z",
    "version": 5,
    "editor": {
        "userId": "anonymized",
        "username": "anonymized"
    }
}

Using the GET process by ID endpoint we can get the process:

curl -sSL 'https://your.api.fulfillmenttools.com/api/processes/<ProcessId>' \
  --header 'Authorization: Bearer <TOKEN>'

200 OK
{
    "gdprCleanupDate": "2023-09-06T08:37:40.541Z",
    "deletionDate": "2023-10-06T08:37:40.541Z",
    "isAnonymized": false,
    "id": "e43ff765-88d2-452f-85d6-57adab58a832",
    "version": 20,
    "orderRef": "710fa76d-5699-4653-8cc7-954e61baf2f1",
    "flatRefs": [
        "710fa76d-5699-4653-8cc7-954e61baf2f1",
        "3411701b-5c05-4220-97d1-c3eb5846a0ae",
        "3a091284-e434-467b-aa3b-da72c28f50f7",
        "24cc693f-29e2-4493-9221-8518f5b9c8fe"
    ],
    "pickJobRefs": [
        "3411701b-5c05-4220-97d1-c3eb5846a0ae"
    ],
    "shipmentRefs": [
        "24cc693f-29e2-4493-9221-8518f5b9c8fe"
    ],
    "handoverJobRefs": [],
    "returnRefs": [],
    "packJobRefs": [
        "27c83af2-f789-4375-987a-a92193acf937"
    ],
    "facilityRefs": [
        "3a091284-e434-467b-aa3b-da72c28f50f7"
    ],
    "created": "2023-08-07T08:37:40.542Z",
    "lastModified": "2023-11-07T13:47:59.765Z",
    "domainStatusHistory": ["shortened for documentation"],
    "domainStatuses": {
        "ORDER": "FINISHED",
        "ROUTING_PLAN": "FINISHED",
        "SHIPMENT": "CREATED",
        "PICKJOB": "FINISHED",
        "PACKJOB": "CREATED"
    },
    "status": "IN_PROGRESS",
    "routingPlanRefs": [
        "710fa76d-5699-4653-8cc7-954e61baf2f1"
    ],
    "schemaVersion": 3,
    "operativeStatus": "IN_PROGRESS",
    "domsStatus": "FINISHED",
    "lastDomainEntityStatuses": ["shortened for documentation"]
}

Here you can find the shipmentRefs, which you need for adding parcels to that shipment. An alternative approach would be to use the GET Shipments endpoint with pickJobRef as a query parameter.

Assuming you have configured the carrier as mentioned above, you can just call the create parcel endpoint without any payload. However, this might depend on the used carrier and the product you want.

curl -sSL -X POST 'https://your.api.fulfillmenttools.com/api/shipments/<ShipmentId>/parcels' \
  --header 'Authorization: Bearer <TOKEN>'

201 Created
{
    "loadUnitRefs": [],
    "version": 1,
    "id": "713b9cd8-6d47-45ff-a12a-ef3acb270180",
    "status": "OPEN",
    "shipmentRef": "3e62db41-af9e-4ce1-8c44-1ef340457057",
    "processRef": "3af2e1a7-d104-4ef9-a43b-13890ae318cf",
    "carrierRef": "488bd1a6-6c76-4407-8dab-ece716dac14d",
    "recipient": {
        "firstName": "Luke",
        "lastName": "Skywalker",
        "street": "Planetenstraße",
        "houseNumber": "13",
        "postalCode": "40223",
        "city": "Düsseldorf",
        "country": "DE"
    },
    "sender": {
        "street": "Carlsplatz",
        "houseNumber": "3",
        "postalCode": "40213",
        "city": "Düsseldorf",
        "country": "DE",
        "companyName": "ptdus",
        "resolvedCoordinates": {
            "lon": 6.77285093516085,
            "lat": 51.2263665950437
        },
        "emailAddresses": null,
        "additionalAddressInfo": null,
        "phoneNumbers": null,
        "resolvedTimeZone": {
            "offsetInSeconds": 3600,
            "timeZoneId": "Europe/Berlin",
            "timeZoneName": "W. Europe Standard Time"
        }
    },
    "dimensions": {
        "weight": 2000
    },
    "created": "2023-04-14T15:03:07.982Z",
    "lastModified": "2023-04-14T15:03:07.982Z"
}

The response contains the Id of the newly created parcel which you can later update, e.g. with tracking information you got from the carrier using the PATCH parcel by ID endpoint:

curl -sSL -X PATCH 'https://your.api.fulfillmenttools.com/api/parcels/<ParcelId>' \
--header 'Authorization: Bearer <TOKEN>' \
--header 'Content-Type: application/json' \
--data-raw '{
    "version": 1,
    "actions": [
        {
            "action": "ModifyParcel",
            "status": "DONE",
            "result": {
              "carrierTrackingNumber": "123456789",
              "returnLabelId": "987654321",
              "summary": "",
              "trackingStatus": "picked_up",
              "trackingUrl": "https://nolp.dhl.de/nextt-online-public/set_identcodes.do?lang=de&idc=123456789"
          }
        }
    ]
}'

More information about that can also be found on the Overview page.

HandoverJob

After you successfully followed the steps above, it might be useful in some cases (for example when using a custom carrier) to mark the HandoverJob as handed over manually. To do this, you will use the PATCH HandoverJob endpoint and a request looking like this:

curl -sSL -X PATCH 'https://your.api.fulfillmenttools.com/api/handoverjobs/<HandoverJobId>' \
--header 'Authorization: Bearer <TOKEN>' \
--header 'Content-Type: application/json' \
--data-raw '{
  "actions": [
    {
      "action": "ModifyHandoverjob",
      "status": "HANDED_OVER"
    }
  ],
  "version": 1
}'

Finished

Congratulations: You have successfully fulfilled an order headlessly.

Last updated