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