Last updated

Webhooks

Using Bettermile Webhooks for real-time updates

Listen for events on your Bettermile Driver App tours, so your integration can automatically trigger reactions in your system.

The Bettermile Webhook API provides a way for Bettermile customers to interact programmatically with Bettermile by subscribing to important events of your tours. Bettermile will notify your application whenever an important event happens.

For tour set up and tour interaction processes via your backend, we recommend an integration of the Tour Commander API or the use of the Bettermile Driver App.

How does it work?

When your application endpoint is enabled on Bettermile to receive events, Bettermile will push real-time notifications to it. These notifications contain the type of event, such as updates to sequences on tours. On your side, you can then use that to integrate Bettermile with existing or new software.

Getting started with the Bettermile Webhook API

Preparing to receive a Webhook Notifications

As prerequisite of Bettermile enabling to send you Webhook notifications, you must make sure you are prepared to receive them:

  • You will need to implement an HTTPS endpoint to receive POST requests as a JSON payload that include the event objects. (please share the endpoint URL with the Bettermile team)
  • For each request received from Bettermile, your endpoint needs to answer with a 2xx status code.
  • Bettermile will not transfer any data on unsafe connections. So please, ensure your created endpoint is both public, and can handle HTTPS connections. We accept endpoints with self-signed keys. (To be provided by you.)

Event types

You should only get from the events you receive the important fields for your logic to work. You also should ignore any events not important for your use case. Parsing or checking the data on fields and events you don't need may put undue stress on your side.

Delivery attempts

If your endpoint is down or not responding with a 2xx code, Bettermile will keep retrying the delivery of the messages by around an hour. The retries have an exponential backoff independent for each message.

After the 1-hour period is over, Bettermile will stop trying to deliver the message and discard it. Due to the fast nature of delivery events we consider 1 hour to be enough time for the message to still be relevant and be retried.

Handling the Events

Due to the distributed nature of our internal systems, there may be some edge cases happening.

Out Of Order Delivery

The delivery of the messages to your endpoint may come out of order from the actual order the events themselves occurred. If ordering is important for your internal systems, you should set up a system to handle this case. We strongly recommend you don't do that though. It may put undue strain on your systems to order the messages you receive. We recommend you project a system as stateless as possible to prevent unforeseen edge-cases.

Duplicate Delivery

Some messages may be delivered more than once, though rare, this can happen. Thus, we recommend your systems are able to handle the received messages in an idempotent manner. One way to do this is storing the events you received and if you receive one that is already stored, you may discard it and answer with a 2xx code.

Secure connections

Bettermile will outright refuse any non-encrypted connection. So be sure that HTTPS is enabled on your endpoint, even if with a self-signed key.

Redirects and other HTTP statuses

Bettermile will consider any status different from 2xx statutes an error. This also means Bettermile will not follow any redirects, considering a redirect status an error, and will keep retrying until the status is 2xx or the retry window expires.

Webhook Events

This document describes the common message structure all the Bettermile's webhook events share.

The message body

Any messages sent by Bettermile will have the following structure:

Ƭ EventSchema: Object

Example

{
    "version": "0",
    "id": "2a4650b3-e231-dc1b-2046-c7beb1ada720",
    "time": "2023-10-05T09:47:07Z",
    "source": "better_route",
    "detail-type": "SEQUENCE_UPDATED",
    "detail": {
        "data based on the event type"
    }
}

Type declaration

NameDescription
versionThe version for this event. Currently 0 (zero) for all events.
idSpecific event identifier
timeThe time the event occurred.
sourceThis identifies the service that generated the event. Events can be generated in any Bettermile product out of the Bettermile Suite - the source giving you an orientation which Bettermile product the event originates from.
detail-typeThe event type is describing the type of event being sent. For example when a sequence is updated detail-type will be SEQUENCE_UPDATED emitted.
detailA JSON object that contains information about the event.

Event types

The EventType, which defines what kind of data each event will have as well as what resource type the event happened upon.

Ƭ EventType: SEQUENCE_UPDATED

Description SEQUENCE_UPDATED: When there is an update to the Sequence of a tour. This is triggered by changes to the order of waypoints. (a quite common example is here a timeframe update by the driver on one of the waypoints and with this the waypoint moving within the sequence or also a pickup being added during the day)

SEQUENCE_UPDATED Schema

Ƭ SequenceChanged: Object The SEQUENCE_UPDATED data model: All SEQUENCE_UPDATED events in the API will have this format.

Description A SEQUENCE_UPDATED is an update to the order of waypoints on a Bettermile Tour.

Example

{
    "version": "0",
    "id": "2a4650b3-e231-dc1b-2046-c7beb1ada720",
    "time": "2023-10-05T09:47:07Z",
    "source": "better_route",
    "detail-type": "SEQUENCE_UPDATED",
    "detail": {
        "tourId": "ac15b30a-23b4-45e4-ad2f-2250557b1d09",
        "depot": "65",
        "date": "2023-10-05",
        "generatedAt": "2023-10-05T09:47:07Z",
        "assignment": "4567",
        "waypoints": [
            {
                "waypointId": "5ccf646c-658b-47f9-ae05-b44c5aa2466a",
                "status": "UNPROCESSED",
                "sequenceError": null,
                "coordinates": {
                    "lat": 52.500185,
                    "lon": 13.420512
                },
                "address": {
                    "street": "Oranienstrasse",
                    "streetNumber": "183",
                    "locality": "Berlin",
                    "postalCode": "10999",
                    "country": "DE"
                },
                "type": "GENERATED",
                "jobs": [
                    {
                        "jobId": "1420db1a-a315-483b-bb2f-0bb03b1e3867",
                        "state": "UNPROCESSED",
                        "type": "DELIVERY",
                        "externalId": "112G1K336L15"
                    },
                    {
                        "jobId": "11ed0624-26cf-4b45-831c-2e28ece75133",
                        "state": "UNPROCESSED",
                        "type": "DELIVERY",
                        "externalId": "2H74B49A394"
                    },
                    {
                        "jobId": "7fa75f76-1357-4eec-9203-19aeb15a39de",
                        "state": "UNPROCESSED",
                        "type": "PICKUP",
                        "externalId": "7M21O67P173"
                    },
                    {
                        "jobId": "5a8cb64a-6c3d-438f-bf8c-f6025ac01de1",
                        "state": "UNPROCESSED",
                        "type": "PICKUP",
                        "externalId": "33H43R2W897"
                    }
                ],
                "waypointId": "5ccf646c-890b-47f9-ae05-b33c5aa2477a",
                "status": "UNPROCESSED",
                "sequenceError": null,
                "coordinates": {
                    "lat": 52.518706,
                    "lon": 13.408118
                },
                "address": {
                    "street": "Rathausstrasse",
                    "streetNumber": "15",
                    "locality": "Berlin",
                    "postalCode": "10178",
                    "country": "DE"
                },
                "type": "GENERATED",
                "jobs": [
                    {
                        "jobId": "1420db1a-a315-483b-bb2f-0bb03b1e3867",
                        "state": "UNPROCESSED",
                        "type": "DELIVERY",
                        "externalId": "6Q40P9K2L98"
                    }
                ]
            }
        ],
        "unassignedJobs": [
            {
                "jobId": "2a660a2a-bfb6-451e-b4f4-bca4976023bd",
                "state": "PROCESSED",
                "type": "DELIVERY",
                "externalId": "9P5I44A887"
            }
        ]
    }
}

Type declaration

NameDescription
tourIdUnique tour identifier generated by Bettermile
depotDepot identifier
dateDate of an assignment being out for delivery
generatedAtA time of the sequence update occurred
assignmentVehicle / route / driver identifier
waypointsThe array of waypoints, that contain respectively the jobs – the order of waypoints will reflect the order of waypoints within the sequence
waypoints waypointIdA unique identifier of this waypoint - This is a stop on the tour where multiple jobs/services can be grouped in to. It is based on locality/address
waypoints statusState of the waypoint - Enum: "UNPROCESSED", "PROCESSED", "CONFIRMED"
waypoints sequenceErrorIt describes a reason that prevents this waypoint from being placed into the sequence. - Enum: "UNREACHABLE_TIMEFRAME", "OUTSIDE_ROAD_NETWORK", "EXCEEDS_MAX_DISTANCE_TO_DEPOT", "UNMAPPED_REASON" - Default = null - UNREACHABLE_TIMEFRAME: The waypoint cannot be visited within the given timeframe OUTSIDE_ROAD_NETWORK: The waypoint cannot be accessed over road network EXCEEDS_MAX_DISTANCE_TO_DEPOT: The waypoint is too far away from tour's depot UNMAPPED_REASON: Some other undocumented reason.
waypoints coordinatesArray of the location of the waypoint as coordinates
waypoints addressArray of the address of the waypoint: street, streetNumber, locality, postalCode and country
waypoints typeType of waypoint - Enum: "GENERATED", "CUSTOM"
waypoints jobsArray of jobs within a waypoint
jobs jobIdA unique identifier used for jobs within Bettermile
jobs externalIdThe job id provided to Bettermile Data Gateway. (matches in most cases the customers parcel Id)
jobs stateStatus of the job - Enum: "PROCESSED", "UNPROCESSED", "UNKNOWN"
jobs typeType of job - Enum: "DELIVERY", "PICKUP", "COLLECTION", "CUSTOM", "UNKNOWN" - note: “custom” jobs do NOT have an external job id - hence can also not be mapped back to customers jobs
unassignedJobsArray of jobs not being part of a waypoint - this can be caused by e.g. missing coordinates on the job or missing address components.

Some more context on Better Route

Important to know

SEQUENCE_UPDATED events can only be generated when a tour was previously set up (e.g. by logging into the Bettermile Driver App to a tour) and updates to sequences are “requested”, this is only the case if e.g. the driver is being online with the app (in foreground).

SEQUENCE_UPDATED events are generated with every change in the sequence - so when there is a change in the waypoint order (common examples are: a timeframe update by the driver on one of the waypoints or also a Pickup being added during the day)

How to sort the given jobs in a SEQUENCE_UPDATED event best?

The event SEQUENCE_UPDATED indeed gives quite some freedom for interpretation for sorting, but it will allow you to show the same order in jobs as in the Bettermile Driver App also on your driver’s hand scanners.

Please note all sequences are a calculated status, not what the driver may actually have done while delivering.

Please consider these 4 groups of waypoints and jobs to sort or take into account for sorting:

  • processed waypoints (and their jobs) - in order
  • unprocessed waypoints (and their jobs) - in order
  • unprocessed waypoints (and their jobs) with a sequenceError - not ordered
  • unassigned jobs (jobs not being part of a waypoint) - not ordered

It is up to you to either separate these out (as we do also in the Driver App) or to separate only sequence error and unassigned jobs out.

A recommendation is to sort all 4 as follows:

  1. all processed waypoints (and their jobs; regardless of these having a sequence error or not)
  2. all unassigned jobs
  3. all unprocessed waypoints (and their jobs; only with sequence errors)
  4. all unprocessed waypoints (and their jobs; only if without sequence errors)

We have a slight different order within the Bettermile Driver App - it makes sense though on the hand scanner to reflect problematic jobs quite high up (or during the tour in view), as with this the driver can react to these better (e.g. by adding or updating the address)

In need for another EventType?

In case you have further ideas or needs for Bettermile webhook events, please contact your Bettermile key account team or drop us an email to route-api@bettermile.com - we are just 1 hook away.