Webhook Signature Verification
Every webhook HTTP request sent by HappyColis includes an HMAC-SHA256 signature so you can confirm the request is genuine and has not been tampered with.
How the Signature Is Computed
The signature is generated in QueueProcessor.send() (see apps/apps/src/app/bus/queue.processor.ts, lines 180–184):
const webhook = {
header: message.header,
body: message.body,
};
const messageContent = JSON.stringify(webhook);
const hmac = crypto
.createHmac('SHA256', app.clientSecret)
.update(messageContent)
.digest('base64');Steps:
- Build the
webhookobject from{ header, body }— the exact same object that is sent as the request body. - Serialize it to a JSON string with
JSON.stringify(). - Compute HMAC-SHA256 over that string using the application's
clientSecretas the key. - Base64-encode the raw HMAC digest.
The result is placed in the x-happycolis-webhook-signature request header.
Signature Header
| Header | Format | Example |
|---|---|---|
x-happycolis-webhook-signature | Base64-encoded HMAC-SHA256 | K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols= |
What to Sign
The signature covers the full JSON-serialized request body — the same object your endpoint receives:
{
"header": {
"organizationId": "550e8400-e29b-41d4-a716-446655440000",
"messageId": "7b3f1c82-4d9a-4e2b-9c01-2a3b4c5d6e7f",
"webhookId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "order/created",
"date": "2026-03-16T12:00:00.000Z"
},
"body": { ... }
}Important: You must read the raw bytes of the request body before any JSON parsing. Parsing and re-serializing may produce a different string and cause signature verification to fail.
Verification Steps
- Read the raw request body as a string (do not parse first).
- Compute
HMAC-SHA256(rawBody, clientSecret). - Base64-encode the digest.
- Compare the result to the
x-happycolis-webhook-signatureheader using a constant-time comparison. - Reject the request if the signatures do not match.
Your clientSecret is the client_secret stored in the application table for your app. It is provided to you when your integration is set up.
Code Samples
Node.js
const crypto = require('crypto');
/**
* Verifies the HappyColis webhook signature.
*
* @param {string} rawBody - raw request body string (not parsed)
* @param {string} signature - value of x-happycolis-webhook-signature header
* @param {string} clientSecret - your application client secret
* @returns {boolean}
*/
function verifyWebhookSignature(rawBody, signature, clientSecret) {
const computed = crypto
.createHmac('sha256', clientSecret)
.update(rawBody)
.digest('base64');
// Use timingSafeEqual to prevent timing attacks
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(computed),
);
} catch {
return false;
}
}
// Express example — use express.raw() to get the unmodified body
const express = require('express');
const app = express();
app.post(
'/webhooks/happycolis',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-happycolis-webhook-signature'];
const rawBody = req.body.toString('utf8');
if (!verifyWebhookSignature(rawBody, signature, process.env.CLIENT_SECRET)) {
return res.status(401).send('Invalid signature');
}
const webhook = JSON.parse(rawBody);
switch (webhook.header.type) {
case 'order/created':
handleOrderCreated(webhook.body);
break;
case 'shipment/shipping_event':
handleShipmentEvent(webhook.body);
break;
// handle other event types ...
}
res.status(200).send('OK');
},
);Python
import hmac
import hashlib
import base64
def verify_webhook_signature(raw_body: str, signature: str, client_secret: str) -> bool:
"""
Verifies the HappyColis webhook signature.
raw_body -- raw request body string (not parsed)
signature -- value of x-happycolis-webhook-signature header
client_secret -- your application client secret
"""
computed = base64.b64encode(
hmac.new(
client_secret.encode('utf-8'),
raw_body.encode('utf-8'),
hashlib.sha256,
).digest()
).decode('utf-8')
return hmac.compare_digest(signature, computed)
# Flask example
from flask import Flask, request, abort
import json
app = Flask(__name__)
CLIENT_SECRET = 'your-client-secret'
@app.route('/webhooks/happycolis', methods=['POST'])
def handle_webhook():
signature = request.headers.get('x-happycolis-webhook-signature', '')
raw_body = request.get_data(as_text=True)
if not verify_webhook_signature(raw_body, signature, CLIENT_SECRET):
abort(401, description='Invalid signature')
webhook = json.loads(raw_body)
event_type = webhook['header']['type']
if event_type == 'order/created':
handle_order_created(webhook['body'])
elif event_type == 'shipment/shipping_event':
handle_shipment_event(webhook['body'])
# handle other event types ...
return 'OK', 200PHP
<?php
/**
* Verifies the HappyColis webhook signature.
*
* @param string $rawBody Raw request body (not decoded)
* @param string $signature Value of x-happycolis-webhook-signature header
* @param string $clientSecret Your application client secret
* @return bool
*/
function verifyWebhookSignature(string $rawBody, string $signature, string $clientSecret): bool
{
$computed = base64_encode(hash_hmac('sha256', $rawBody, $clientSecret, true));
return hash_equals($computed, $signature);
}
// Laravel route example
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::post('/webhooks/happycolis', function (Request $request) {
$signature = $request->header('x-happycolis-webhook-signature');
$rawBody = $request->getContent();
if (!verifyWebhookSignature($rawBody, $signature, config('services.happycolis.client_secret'))) {
return response('Invalid signature', 401);
}
$webhook = json_decode($rawBody, true);
switch ($webhook['header']['type']) {
case 'order/created':
handleOrderCreated($webhook['body']);
break;
case 'shipment/shipping_event':
handleShipmentEvent($webhook['body']);
break;
// handle other event types ...
}
return response('OK', 200);
});Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"io"
"net/http"
"os"
)
// verifyWebhookSignature verifies the HappyColis webhook signature.
// rawBody is the unmodified request body bytes.
// signature is the value of the x-happycolis-webhook-signature header.
// clientSecret is your application client secret.
func verifyWebhookSignature(rawBody []byte, signature, clientSecret string) bool {
mac := hmac.New(sha256.New, []byte(clientSecret))
mac.Write(rawBody)
computed := base64.StdEncoding.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(computed), []byte(signature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
rawBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "cannot read body", http.StatusBadRequest)
return
}
signature := r.Header.Get("x-happycolis-webhook-signature")
clientSecret := os.Getenv("CLIENT_SECRET")
if !verifyWebhookSignature(rawBody, signature, clientSecret) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
var payload map[string]interface{}
if err := json.Unmarshal(rawBody, &payload); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
header := payload["header"].(map[string]interface{})
switch header["type"] {
case "order/created":
handleOrderCreated(payload["body"])
case "shipment/shipping_event":
handleShipmentEvent(payload["body"])
// handle other event types ...
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func main() {
http.HandleFunc("/webhooks/happycolis", webhookHandler)
http.ListenAndServe(":8080", nil)
}Ruby
require 'openssl'
require 'base64'
require 'json'
# Verifies the HappyColis webhook signature.
#
# raw_body - raw request body string (not parsed)
# signature - value of x-happycolis-webhook-signature header
# client_secret - your application client secret
#
# Returns true if the signature matches, false otherwise.
def verify_webhook_signature(raw_body, signature, client_secret)
computed = Base64.strict_encode64(
OpenSSL::HMAC.digest('SHA256', client_secret, raw_body)
)
# Constant-time comparison
ActiveSupport::SecurityUtils.secure_compare(computed, signature)
rescue StandardError
false
end
# Sinatra example
require 'sinatra'
CLIENT_SECRET = ENV.fetch('CLIENT_SECRET')
post '/webhooks/happycolis' do
raw_body = request.body.read
signature = request.env['HTTP_X_HAPPYCOLIS_WEBHOOK_SIGNATURE'] || ''
unless verify_webhook_signature(raw_body, signature, CLIENT_SECRET)
halt 401, 'Invalid signature'
end
webhook = JSON.parse(raw_body)
case webhook.dig('header', 'type')
when 'order/created'
handle_order_created(webhook['body'])
when 'shipment/shipping_event'
handle_shipment_event(webhook['body'])
# handle other event types ...
end
'OK'
endSecurity Best Practices
- Always verify the signature before processing the payload.
- Use constant-time comparison (
timingSafeEqual,hmac.Equal,hash_equals,secure_compare) to prevent timing attacks. - Read the raw body before parsing JSON — do not re-serialize.
- Use HTTPS endpoints. HappyColis will not deliver to plain HTTP.
- Return 200 quickly and process the event asynchronously to avoid timeouts.
- Use
messageIdfor deduplication — deliveries may be retried on network errors.