Skip to content

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

typescript
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:

  1. Build the webhook object from { header, body } — the exact same object that is sent as the request body.
  2. Serialize it to a JSON string with JSON.stringify().
  3. Compute HMAC-SHA256 over that string using the application's clientSecret as the key.
  4. Base64-encode the raw HMAC digest.

The result is placed in the x-happycolis-webhook-signature request header.

Signature Header

HeaderFormatExample
x-happycolis-webhook-signatureBase64-encoded HMAC-SHA256K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=

What to Sign

The signature covers the full JSON-serialized request body — the same object your endpoint receives:

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

  1. Read the raw request body as a string (do not parse first).
  2. Compute HMAC-SHA256(rawBody, clientSecret).
  3. Base64-encode the digest.
  4. Compare the result to the x-happycolis-webhook-signature header using a constant-time comparison.
  5. 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

javascript
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

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', 200

PHP

php
<?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

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

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'
end

Security 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 messageId for deduplication — deliveries may be retried on network errors.

HappyColis API Documentation