Securing Webhooks

Ensuring your integration is only receiving the expected Deliveroo requests.

Deliveroo notifies you about order and rider events through webhooks. A hash-based message authentication code (HMAC) signature, included alongside the webhook payload, can be used to verify the event. You may read more about HMAC here.

Once you have configured your webhook endpoints, we will provide a webhook secret. This secret is known only by you and Deliveroo. The verification signature is generated using the webhook secret.

Verifying Signature

This guide describes how to verify the old webhook events, i.e new_order and cancel_order event types.

Step 1: Extract the signature and GUID from request headers

Retrieve the GUID and signature from the request headers X-Deliveroo-Sequence-Guid and
X-Deliveroo-Hmac-Sha256, respectively.

Step 2: Prepare the signed payload

Create the payload by concatenating the GUID and the request body,

  • separated by \n (a newline character with a space before and after it) for legacy new_order and cancel_order webhooks in POS integration.
  • separated by (a space) for all the other webhooks.

📘

Important note

It's crucial that the raw bytes received in the HTTP request are used directly to validate the HMAC signature. This means no converting to strings, serialising as JSON objects, or any other manipulation should be done prior to confirming the data's origin and integrity. Once the HMAC signature is successfully validated, you can proceed with any necessary transformations or serialisation processes.

Step 3: Determine the expected signature

Compute an HMAC with the SHA256 hash function. Use the webhook secret as the key, and use the payload prepared in step 2 as the message.

Step 4: Verify the signature

Compare the signature you determined with the signature you retrieved from the request header. You may consider the event valid only if the two signatures match.

Examples

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"fmt"
	"io"
	"log"
	"net/http"
)

var (
	sharedSecret     = "abc123"
	legacyPOSWebhook = false
)

func deliveroo(w http.ResponseWriter, req *http.Request) {
	sequence := req.Header.Get("x-deliveroo-sequence-guid")
	expected := req.Header.Get("x-deliveroo-hmac-sha256")

	hash := hmac.New(sha256.New, []byte(sharedSecret))
	hash.Write([]byte(sequence))

	if legacyPOSWebhook {
		// Legacy new_order and cancel_order webhooks in POS integration
		// require a line break between spaces.
		hash.Write([]byte(" \n "))
	} else {
		// All other webhooks require a single blank space.
		hash.Write([]byte(" "))
	}

	// Use the raw bytes that arrived in the request.
	// Do not transform them into string when calculating the HMAC.
	bodyBytes, _ := io.ReadAll(req.Body)

	hash.Write(bodyBytes)
	calculated := fmt.Sprintf("%x", hash.Sum(nil))

	fmt.Printf("calculated [%s]\n", calculated)
	fmt.Printf("deliveroo's [%s]\n", expected)

	if expected != calculated {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	w.WriteHeader(http.StatusOK)
}

func main() {
	http.HandleFunc("/deliveroo", deliveroo)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

from http.server import BaseHTTPRequestHandler, HTTPServer
import hmac
import hashlib

legacy_pos_webhook = False
shared_secret = b'abc123'

class DeliverooHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        sequence = self.headers.get('x-deliveroo-sequence-guid')
        expected = self.headers.get('x-deliveroo-hmac-sha256')

        hmac_calculator = hmac.new(shared_secret, digestmod=hashlib.sha256)
        hmac_calculator.update(sequence.encode())

        if legacy_pos_webhook:
            hmac_calculator.update(b' \n ')
        else:
            hmac_calculator.update(b' ')

        content_length = int(self.headers.get('content-length', 0))
        body = self.rfile.read(content_length)
        hmac_calculator.update(body)

        calculated = hmac_calculator.hexdigest()

        print(f'calculated [{calculated}]')
        print(f"deliveroo's [{expected}]")

        if expected != calculated:
            self.send_response(400)
        else:
            self.send_response(200)

        self.end_headers()

def run(server_class=HTTPServer, handler_class=DeliverooHandler, port=8080):
    server_address = ('', port)
    httpd = server_class(server_address, handler_class)
    print(f'Starting httpd on port {port}...')
    httpd.serve_forever()

if __name__ == '__main__':
    run()

require 'webrick'
require 'openssl'

$legacy_pos_webhook = false
$shared_secret = 'abc123'

class DeliverooHandler < WEBrick::HTTPServlet::AbstractServlet
  def do_POST(request, response)
    sequence = request.header['x-deliveroo-sequence-guid'].first
    expected = request.header['x-deliveroo-hmac-sha256'].first

    hmac = OpenSSL::HMAC.new($shared_secret, OpenSSL::Digest.new('sha256'))
    hmac.update(sequence)

    if $legacy_pos_webhook
      hmac.update(" \n ")
    else
      hmac.update(" ")
    end

    body = request.body
    hmac.update(body)

    calculated = hmac.hexdigest

    puts "calculated [#{calculated}]"
    puts "deliveroo's [#{expected}]"

    if expected != calculated
      response.status = 400
    else
      response.status = 200
    end
  end
end

server = WEBrick::HTTPServer.new(:Port => 8080)
server.mount '/deliveroo', DeliverooHandler

trap 'INT' do
  server.shutdown
end

server.start

The examples above can be tested using the following cURL request

curl --location 'http://localhost:8080/deliveroo' \
--header 'x-deliveroo-sequence-guid: 1174efedab186000' \
--header 'x-deliveroo-hmac-sha256: 3ecb144a17c06b81b6cd95c29349927f05f3d6c9b6c1821d226ed0100a4fefa6' \
--header 'Content-Type: application/json' \
--data '{
    "hello": "world"
}'

What’s Next