How to process Karhoo webhooks

A better option than polling for Karhoo Marketplace events

Rather than requiring you to pull 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 your webhook url

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.

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

📘

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.

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"
    "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.

Webhook Events

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.).

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.

Structure

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

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

An Example: Trip Status Event

The Trip Status webhook works similarly to the polling endpoint for getting trip status.

{
    "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\"}"
}

And here is the Trip Status data object once decoded:

{
    "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.

An Example: Driver Details Event

The Driver Details webhook event is sent if any driver or vehicle details change after the trip has been booked.

{
    "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\"}"
}

And here is the Driver Details object once you decode the request. You will need to decode into the relevant structures based on the event 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"
}

An Example: Final Fare Event

Notification that the final fare for this trip has been set and is ready to be retrieved from the Fares API.

{
    "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"
}