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"
}
Updated 8 months ago