How to process Karhoo webhooks

A better option than polling for Karhoo Marketplace events

Rather than requiring you to constantly poll for information via our API, webhooks will push information to your endpoint. When one of those events is triggered (for example a trip is updated by the fleet's driver), Karhoo will send this notification as an HTTP POST request, with a JSON body, to the endpoint you specify.

If you're new to webhooks, read this guide to learn more.

Registering a webhook subscription

You can enable directly using the webhooks register endpoint . Karhoo will need the url to send events to and a shared_secret to generate a HMAC authorization header. You can also a supply a set of topics for which the subscription will listen to. The available topics are:

  • TripStatus - Updates about the trips state.
  • DriverDetails - Driver and vehicle details.
  • FinalFare - Notification when final fare for the trip has been completed.
  • DriverPositionChanged - GPS coordinates of the driver.

See the webhook events section below for more details. If no topics are supplied your subscription will be created with a default set containing TripStatus, DriverDetails and FinalFare.

{
  "url": "https://your-company.com/karhoo/events",
  "shared_secret": "dontT3llAnyoneThisSecret!",
  "topics": ["TripStatus"]
}

📘

Need to test a webhook?

A great way to test webhooks without building services is to use webhook.site which will allow you to sample the payloads we send from our sandbox environment. Do not use this for production trips.

Managing subscriptions

After subscription creation as well as when retrieving active subscriptions via the GET endpoint the subscription ID is returned. You can use this to individually disable subscriptions via the delete endpoint. Note that disabled subscriptions can not be re-enabled, you will need to create a new subscription. If you wish to change the URL or topics of a subscription you should delete the previous one and create a new subscription with the desired configuration.

Securing your webhook

Karhoo webhooks are secured using HMAC authentication using a shared secret.

The HMAC signature digest is computed on the payload content using the supplied secret key. This SHA512 hash is sent as lowercase hexits in the X-Karhoo-Request-Signature header.

🚧

Always check each request

It is important to verify each request body to avoid man-in-the-middle attacks or fake requests.

The following is copied from a golang playground example of how we would sign our X-Karhoo-Request-Signature header with the shared secret you set when subscribing to a webhook.

package main

import (
	"crypto/hmac"
	"crypto/sha512" // not to be confused with sha256
	"encoding/hex"
	"encoding/json"
	"fmt"
)

// Please do not set your key to this example in production
const (
	signature         string = "8816883ca05dda771ddf522c26a958b262ebe52753ed5fcc87828b24aff49b3369aa005a2f664a87f1a1958e0f44121f1643aebcba35a32ff2d921eaad5e4ad7"
	exampleSecretKey  string = "EAlOTQ1IHwansbPn0cUOPyQYrONmuOAu"
	sampleRequestBody string = `{"attempt_number":0,"checksum":"c7fe6dfefc4765337e8e0a31bbe1e49c2addd676c2f1b764100a0b27fb6f296c440ecb9c8f4aec552c86babe24ae1d7040cde399cc5153600799257a8a16f845","data":"{\"status\":\"ARRIVED\",\"trip_id\":\"c2374749-e983-4d8b-9312-1ca06a5ffe37\"}","event_type":"TripStatus","id":"5948ec35-a071-4f71-9416-c607d0120ca8","sent_at":"2020-12-02T00:04:58.711Z"}`
)

type Event struct {
	ChecksumHEX string `json:"checksum"`
	Data        string `json:"data"`
}

// HashRequestBody represents a function similiar to how we sign our signature
func HashRequestBody(body []byte, secretKey []byte) (string, error) {
	mac := hmac.New(sha512.New, secretKey)
	_, err := mac.Write(body)
	if err != nil {
		return "", err
	}
  
	return hex.EncodeToString(mac.Sum(nil)), nil
}

func main() {
	// we need to generate our own hash to compare against the signature
	hash, err := HashRequestBody([]byte(sampleRequestBody), []byte(exampleSecretKey))

	if err != nil {
		panic(err)
	}

	// will print true if the signature matches the hash we computed and our data is secure.
	fmt.Println(signature == hash)

	// we can go a step further and check the contents of the event type
	var evt Event
	if err := json.Unmarshal([]byte(sampleRequestBody), &evt); err != nil {
		panic(err)
	}
  
	sha := sha512.Sum512([]byte(evt.Data))
	checksum := hex.EncodeToString(sha[:])

	fmt.Println(checksum == evt.ChecksumHEX)
}

❗️

You must hash the entire response body

Failing to account for linefeed (LF) or new line (NL) characters when comparing hash signatures will result in a failed cryptographic check.

Successful delivery

A webhook is considered to be successfully delivered is a response is received with a 200 range status code (e.g. 200, 202, 204, etc.). Failure to respond with such will result in retries.

Retries

In the event of a failure defined as a request timeout, 400, 402-409 or 411-599 HTTP response codes, we will make 3 retry attempts.

  • Immediately
  • After 10 seconds
  • After 30 seconds

Duplicate events

We strive to keep these to a minimum, however it is still possible that you may receive duplicate events in a very short space of time. We recommend keeping a short-lived cache of seen checksums that you have handled to avoid processing the same event multiple times. A checksum is provided in each event to assist with this.

Resending events

Karhoo is unable to resend events outside of the automatic retries, nor are we able to retrieve payloads of missed events.

Webhook Events

Event Structure

All Karhoo webhook events have a common structure with the exception of the data field which changes depending on the event_type. The common structure is as follows:

{
    "id": "94282c25-f975-4aac-b86c-20929c8a066a",
    "event_type": "TripStatus",
    "sent_at": "2020-10-27T17:55:49.403Z",
    "checksum": "c8968066ff2919fd80ad178da76794e6925b121c1d",
    "attempt_number": 0,
    "data": "{...}"
}

Webhook consumers should first parse the event_type to identify which payload to expect within the data field.

Data structures and types

There are currently 4 webhook types to consume:

  • TripStatus
  • DriverDetails
  • FinalFareReleased
  • DriverPositionChanged

TripStatus

This event is triggered whenever the state of a trip is changed. This is a simple payload containing string fields: trip_id, status.

The status to expect are listed here: Trip Status. Example:

{
    "trip_id": "b6a5f9dc-9066-4252-9013-be85dfa563bc",
    "status": "CONFIRMED"
}

And here is the full event Trip Status once decoded:

{
    "id": "94282c25-f975-4aac-b86c-20929c8a066a",
    "event_type": "TripStatus",
    "sent_at": "2020-10-27T17:55:49.403Z",
    "checksum": "c8968066ff2919fd80ad178da76794e6925b121c1d",
    "attempt_number": 0,
    "data": "{\"trip_id\":\"b6a5f9dc-9066-4252-9013-be85dfa563bc\",\"status\":\"CONFIRMED\"}"
}

📘

Handling 'Failed' trip status event

In the event that you receive a trip status event with a 'FAILED' status, we strongly recommend polling the main Bookings API with the Trip ID for more details on the state of this trip.

DriverDetails

The Driver Details data will contain all driver & vehicle information available, here is a full example of all fields to expect (note that the capacity fields are of the number type:

{
    "trip_id": "b6a5f9dc-9066-4252-9013-be85dfa563bc",
    "description": "Renault Scenic (Black)",
    "driver": {
        "first_name": "Michael",
        "last_name": "Higgins",
        "phone_number": "+441111111111",
        "photo_url": "https://karhoo.com/drivers/mydriver.png",
        "license_number": "ZXZ151YTY"
    },
    "luggage_capacity": 2,
    "passenger_capacity": 3,
    "vehicle_class": "MPV",
    "vehicle_license_plate": "123 XYZ"
}

And here is a complete example:

{
    "id": "94282c25-f975-4aac-b86c-20929c8a066a",
    "event_type": "DriverDetails",
    "sent_at": "2020-10-27T17:55:49.403Z",
    "checksum": "c8968066ff2919fd80ad178da76794e6925b121c1d",
    "attempt_number": 0,
    "data": "{\"trip_id\":\"b6a5f9dc-9066-4252-9013-be85dfa563bc\",\"description\":\"Renault Scenic (Black)\",\"driver\":{\"first_name\":\"Michael\",\"last_name\":\"Higgins\",\"phone_number\":\"+441111111111\",\"photo_url\":\"https:\/\/karhoo.com\/drivers\/mydriver.png\",\"license_number\":\"ZXZ151YTY\"},\"luggage_capacity\":2,\"passenger_capacity\":3,\"vehicle_class\":\"MPV\",\"vehicle_license_plate\":\"123 XYZ\"}"
}

Final Fare Event

This event signals that the supplier has confirmed the final fare for this trip, and that the information is ready for retrieval from the Fares API.

The data field here is very light, containing just the trip_id:

{
    "trip_id": "7edd8f27-4643-475e-b8af-84a98b091123"
}
{
    "attempt_number": 0,
    "checksum": "3524be952b4135de7e7fbef2dc07d343f83b3e379e48afa4d72c87ed66758116238fb4c82e6d848bb5014020492cb1ee18d99b509742ce672452cd456444fab5",
    "data": "{\"trip_id\":\"7edd8f27-4643-475e-b8af-84a98b091123\"}",
    "event_type": "FinalFareReleased",
    "id": "5ecfcded-4d8e-4444-a043-19d65d345k6l",
    "sent_at": "2020-10-27T17:55:49.403Z"
}

DriverPositionChanged Event

This event will be triggered whenever we detect the driver has moved more than 5m.

The data field contains the trip_id and the latitude and longitude coordinates of the driver as well as the current heading of the vehicle in compass degrees.

{
  "latitude":51.534412,
  "longitude":-0.12170931,
  "trip_id":"b31dc68d-ceec-43b3-9d12-84034db8a626",
  "heading": 87
}
{
  "attempt_number": 0,
  "checksum": "9c4ac081f164a0ded56f8845937217469e92537ebf5dfb898bc0e92a9b25e60ad7da57a0843e908e977cf0ff012b015f3d8aa544289ea8331807794507334a65",
  "data": "{\"latitude\":51.534412,\"longitude\":-0.12170931,\"trip_id\":\"b31dc68d-ceec-43b3-9d12-84034db8a626\"}",
  "event_type": "DriverPositionChanged",
  "id": "e4ba7068-c511-4a90-9d9c-839a6148998b",
  "sent_at": "2023-09-19T14:35:29.662Z"
}