Custom carrier
Custom Carriers represent carriers that are not natively supported by fulfillmenttools, but are connected by an external service or operative process.
While it is not possible to have more than one provided carrier in fulfillmenttools, it is possible to configure multiple custom carriers. Since the carrier key is used for identification and thus must be unique, a custom carrier must have a key starting with CUSTOM_ followed by an appendix (e.g., CUSTOM_BOSSONBIKE) to ease identification.
Use case: Headless carrier connection
A custom carrier enables a proprietary carrier integration with the fulfillmenttools platform. This tutorial showcases a possible solution for integrating an unsupported carrier, detailing the required processes and clients.
The integration model allows for a headless connection by using fulfillmenttools eventing and API interactions. This enables an external service, implemented and operated by a partner or customer, to manage the complexities of requesting a shipping label from an arbitrary carrier.
Concept
The following diagram and explanation describe the overall approach.
Subscribe to a
fulfillmenttoolsevent. The process is triggered by an event subscription. The specific event chosen depends on the customer's process, but a logical trigger would be an event that occurs after packing is complete, such asPACK_JOB_CREATED.fulfillmenttoolssends this event to the external service that handles the label request from the Carrier API.The external service gathers data and makes decisions. The external service receives the event and uses its payload to make decisions, such as selecting the exact carrier or carrier product, determining the necessary insurance value, etc. This data can be sourced from other services, a database, or fetched from
fulfillmenttoolsvia its API.Request a label from the Carrier API. Similar to the native
fulfillmenttoolsmechanism, the external service uses the gathered data to request a label and negotiate a pickup with the carrier. The service receives the label and other data (e.g., pickup dates, transit times) from the carrier.Store the carrier label and data in
fulfillmenttools. Once the label, track-and-trace information, and any additional data are received, the external service uploads this data tofulfillmenttoolsfor seamless process integration. For example, the label is uploaded to adocumentset, and the track-and-trace data is stored as a result on theparcelentity.Set handover states based on track-and-trace data.
fulfillmenttoolscan set the handover status of shipped parcels to "handed over." The external service can perform this action by connecting to the carrier's track-and-trace API and deciding when to set thehandoverJobstatus infulfillmenttoolstoHANDED_OVER, according to the desired process.
Proof of concept: Connecting Uber Direct
Overview
As a proof of concept, this section demonstrates how to connect the carrier Uber Direct to fulfillmenttools. An external service, the Uber Connector Service, will be created to manage the integration.
This example uses Uber Direct due to its straightforward technical connection. It can be substituted with any other carrier. For this proof of concept, specific parameters are chosen, such as the trigger event, required data, and handover timing. This represents one of many possible implementation strategies; the approach can be adapted with different event triggers or more complex logic as needed.
In this example, the Parcel (with its label PDF) is automatically ordered when a PackJob is created for a given Process. The following diagram illustrates the components and events involved.
Necessary steps
The integration connects to Uber Direct as the carrier, which offers a simple API for requesting pickups and deliveries.
The external service is hosted within a Google Cloud Project as a Cloud Run service, which scales to zero and responds to HTTP requests from a fulfillmenttools subscription.
Prepare the service infrastructure
This proof of concept is written in Kotlin. To create a running service, refer to the official Google Cloud documentation for deploying a Kotlin service to Cloud Run.
After completing this step, a live Cloud Run service is deployed and reachable via the internet without authentication on its endpoints.
Production Security
This proof of concept omits security measures for simplicity. A production-ready service must implement robust authentication and authorization mechanisms to protect its endpoints.
Configuration of fulfillmenttools
Following the guide on carrier management, create the custom carrier "Uber Direct" as shown below:
curl --location 'https://{YOUR-TENANT-NAME}.api.fulfillmenttools.com/api/carriers' \
--header 'Content-Type: application/json' \
--header 'Authorization: ••••••' \
--data '{
"key": "CUSTOM_UBER",
"name": "Uber Direct",
"status": "ACTIVE"
}'
< 201 Created
{
"id": "26de36d7-883d-456a-91ef-ed9526cd5835",
"key": "CUSTOM_UBER",
"name": "Uber Direct",
"status": "ACTIVE",
...
}Next, connect this carrier to the facility where the service will be provided:
# 469adf78-5aeb-425c-b2dc-91027e69336a being the ID of the facility in this case
curl --location 'https://{YOUR-TENANT-NAME}.api.fulfillmenttools.com/api/facilities/469adf78-5aeb-425c-b2dc-91027e69336a/carriers/26de36d7-883d-456a-91ef-ed9526cd5835' \
--header 'Content-Type: application/json' \
--header 'Authorization: ••••••' \
--data '{
"status": "ACTIVE",
"name": "CUSTOM_UBER",
"configuration": {
"key": "CUSTOM_UBER"
}
}'
< 201 Created
{
"id": "469adf78-5aeb-425c-b2dc-91027e69336a_26de36d7-883d-456a-91ef-ed9526cd5835",
"version": 1,
"name": "CUSTOM_UBER",
"status": "ACTIVE",
"carrierRef": "26de36d7-883d-456a-91ef-ed9526cd5835",
"key": "CUSTOM_UBER",
"deliveryType": "DELIVERY",
"facilityRef": "469adf78-5aeb-425c-b2dc-91027e69336a",
"configuration": {
"key": "CUSTOM_UBER"
},
...
}Implement the Uber Carrier Connector Service
The service is designed as a standalone application that does not require a database or other external components besides its container engine. This requires making practical decisions about which event to use and how much processing to perform during the event handling.
The created service is now extended with the following functionality. Only subsets of the implementation are described here.

Implementing functionality for the Uber Direct API
First, an Uber account with access to the Uber Direct API is required. The following credentials must be obtained:
A Customer ID (
UBER_CUSTOMER_ID)A valid Client ID (
UBER_CLIENT_ID)A matching client secret (
UBER_CLIENT_SECRET)
For simplicity, a new Bearer Token for the Uber API (UBER_TOKEN, valid for 30 days) is generated via curl and provided as a configuration variable to the service. This call obtains the token:
> curl --location 'https://login.uber.com/oauth/v2/token' \
--form 'client_id="UBER_CLIENT_ID"' \
--form 'client_secret="UBER_CLIENT_SECRET"' \
--form 'grant_type="client_credentials"' \
--form 'scope="eats.deliveries direct.organizations"'
< 200 OK
{
"access_token": "UBER_TOKEN",
"token_type": "Bearer",
"expires_in": 2592000,
"scope": "eats.deliveries direct.organizations"
}Next, the service must implement a call to Uber for the creation of a delivery (pickup at location A, drop-off at location B). To accomplish this, a new Uber API object and the implementation for the delivery creation endpoint are created:
Apis.kt
class Api {
companion object Factory {
fun createUberApi():UberApi = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("https://api.uber.com/v1/customers/UBER_CUSTOMER_ID/")
.build().create(UberApi::class.java)
}
}
UberApi.kt
interface UberApi {
@Headers(
"Authorization: Bearer UBER_TOKEN",
)
@POST("deliveries")
fun createDelivery(@Body createDelivery: CreateDelivery): Call<CreatedDelivery?>
}In addition, model classes for the request and response bodies, a repository, and a service are implemented for better code structure and readability.
Implement connections for the fulfillmenttools REST API
Functionality is also required to interact with fulfillmenttools to create parcels and set results. The following endpoints are the most important ones:
FFTApi.kt
interface FFTApi {
@POST("shipments/{shipmentId}/parcels")
fun createParcel(@Path("shipmentId") shipmentId: String, @Body parcelForCreation: ParcelForCreation): Call<Parcel>
@GET("shipments")
fun getShipments(@Query("pickJobRef") pickJobId: String): Call<Shipments>
@PATCH("parcels/{parcelId}")
fun patchParcel(@Path("parcelId") parcelId: String, @Body parcelPatch: ParcelPatch): Call<Parcel>
@GET("handoverjobs")
fun getHandoverJobs(@Query("shipmentRef") shipmentRef: String): Call<HandoverJobs>
@POST("documentsets/{documentSetId}/documents")
fun createDocument(@Path("documentSetId") documentSetId: String, @Body documentForCreation: DocumentForCreation): Call<Document>
...
}In addition to creating a parcel in fulfillmenttools containing the result of the Uber delivery, the service also needs the ability to set the HandoverJob created in the process to HANDED_OVER:
interface FFTApi {
...
@PATCH("handoverjobs/{handoverJobId}")
fun patchHandoverJob(@Path("handoverJobId") handoverJobId: String, @Body handoverJobPatch: HandoverJobPatch): Call<HandoverJob>
}Provide endpoints for fulfillmenttools and Uber events
With the functionality in place, the web endpoints can be defined to receive events from fulfillmenttools and Uber. An event from fulfillmenttools will trigger the creation of a delivery, while track-and-trace events from Uber will trigger further processing later in the flow.
@SpringBootApplication
@RestController
class Application {
private val fftService: FulfillmenttoolsService = FulfillmenttoolsService() //the service to connect to fulfillmenttools
private val uberService: UberService = UberService() //the service to connect to Uber direct
private val pdfService: PdfService = PdfService() //a service that is able to create a PDF as base64
/**
* Callback for fulfillmenttools when a PackJob was created.
*/
@PostMapping("/requestPickupCallback")
suspend fun requestPickupCallback(@RequestBody packJobCreatedEvent: demo.fft.model.PackJobCreatedEvent) = run {
//Authenticate @ fulfillmenttools
if (!fftService.authenticate()) {
return@run
}
val shipments: List<Shipment>? = fftService.getShipmentsForPickjob(pickJobId = packJobCreatedEvent.payload.pickJobRef)
if (shipments.isNullOrEmpty()) {
logger.info { "No shipment found for Pickjob ${packJobCreatedEvent.payload.pickJobRef} - Aborting." }
return@run
}
val shipment: Shipment = shipments.first()
if (packJobCreatedEvent.event == "PACK_JOB_CREATED" &&
shipment.sourceAddress != null &&
shipment.targetAddress != null &&
shipment.sourceAddress.phoneNumbers.isNotEmpty() &&
shipment.targetAddress.phoneNumbers.isNotEmpty()
) {
logger.info { "Processing PACK_JOB_CREATED Event from fulfillmenttools..." }
val parcel: Parcel = fftService.createProcessingParcel(shipment) ?: return@run
val sourceAddress = shipment.sourceAddress
val targetAddress = shipment.targetAddress
//the UberService implementation puts shipment & parcelId to the created delivery at Uber for later use
val createdDelivery : CreatedDelivery? = uberService.createDelivery(
...
)
if (createdDelivery == null) {
logger.error { "Failed to create delivery." }
return@run
}
val labelBase64 = pdfService.createLabel(
...
)
if (parcel.documentsRef == null) {
logger.error { "No documentsRef found on Parcel." }
return@run
}
val document = fftService.uploadLabel(packJobCreatedEvent.payload.documentsRef,labelBase64)
if (document == null) {
logger.error { "No label could be created." }
return@run
}
fftService.setParcelToDone(parcel, createdDelivery, document.id)
} else {
logger.error { "Missing parameters in callback. Aborting." }
return@run
}
}
/**
* Called from Uber for delivery tracking updates
*/
@PostMapping("/deliveryStatusCallback")
suspend fun deliveryStatusCallback(@RequestBody deliveryStatusEvent: DeliveryStatusEvent) = run {
if (deliveryStatusEvent.status != "pickup_complete") {
logger.info { "Discarding Uber Event: ${deliveryStatusEvent.status}. Processing stopped." }
return@run
}
logger.info { "Trigger Event ${deliveryStatusEvent.status} received from Uber Direct: ${Gson().toJson(deliveryStatusEvent)}" }
val ids: List<String> = deliveryStatusEvent.data.external_id?.split("####") ?: emptyList()
if (ids.size != 2) {
logger.error { "Wrong count (${ids.size}) of params in Uber external_id." }
return@run
}
val shipmentId: String = ids[0]
val parcelId: String = ids[1]
val handoverJob = fftService.getHandoverForParcelInShipment(parcelId,shipmentId)
if(handoverJob == null){
throw Exception("No HandoverJob found for Parcel $parcelId after 5 retries!")
}
fftService.setHandoverToHandedOver(handoverJob)
logger.info { "Process completed SUCCESSFULLY for Delivery ${deliveryStatusEvent.id}. Done." }
}Deploy the service to Google Cloud Run
Issuing the command gcloud run deploy headlesscarrierservice in the console triggers the deployment of the service from its source code. After the build completes, the container is available at its assigned URL, for instance: https://headlesscarrierservice-558061097749.europe-west1.run.app.
This host URL is required for the remaining configuration steps in fulfillmenttools and Uber.
Create a fulfillmenttools subscription
To connect the service to a fulfillmenttools instance, create an HTTP subscription for the desired event. This is done by issuing the following API call, as detailed in the eventing documentation:
curl --location 'https://{YOUR-TENANT-NAME}.api.fulfillmenttools.com/api/subscriptions/' \
--header 'Content-Type: application/json' \
--header 'Authorization: ••••••' \
--data '{
"callbackUrl": "https://headlesscarrierservice-558061097748.europe-west1.run.app/requestPickupCallback",
"event": "PACK_JOB_CREATED",
"headers": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"name": "Uber Carrier Connector Service - Request Pickup after Picking Completed"
}'
< 201 Created
{
"callbackUrl": "https://headlesscarrierservice-558061097748.europe-west1.run.app/requestPickupCallback",
"event": "PACK_JOB_CREATED",
"headers": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"name": "Uber Carrier Connector Service - Request Pickup after Picking Completed",
"lastModified": "2024-09-24T09:27:53.833Z",
"id": "b0eee1d0-ae92-4723-8a67-e73ef60d7ac7",
"created": "2024-09-24T09:27:53.833Z"
}From this point on, every PACK_JOB_CREATED event within fulfillmenttools will be received and processed by the created service.
Configuration of Uber
Uber offers a webhook mechanism similar to the subscription pattern in fulfillmenttools. A connection from Uber to the created service can be established by creating a webhook for event.delivery_status events in the Uber developer dashboard (example UI shown in German):

Ensure that the correct service URL and endpoint are used. Once saved, Uber will send tracking events to the specified endpoint, where they will be handled by the service.
When an order is processed in the configured facility, this mechanism will trigger the creation of the delivery, upload a self-generated PDF to fulfillmenttools, and process the track-and-trace events.
Last updated