Signature Verification
Every webhook request is signed. Always verify the X-Notifo-Signature header before processing the payload.
How the signature is computed
secretHash = SHA-256( rawSecret )
signature = "sha256=" + HMAC-SHA256( key=secretHash, data=requestBody )
rawSecret is the value shown once when you create the webhook.
Verification examples
- Node.js (Express)
- Python (Flask)
- PHP
- Go
const crypto = require('crypto');
// Use express.raw() so req.body is a Buffer (raw bytes — required for correct HMAC)
app.post('/webhooks/notifo', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-notifo-signature'];
const rawBody = req.body; // Buffer
const secretHash = crypto.createHash('sha256').update(YOUR_SIGNING_SECRET).digest('hex');
const expected = 'sha256=' + crypto.createHmac('sha256', secretHash).update(rawBody).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(rawBody);
console.log(event.event, event.messageId, event.status);
res.sendStatus(200);
});
import hashlib, hmac, json
from flask import Flask, request, abort
app = Flask(__name__)
YOUR_SIGNING_SECRET = 'your_secret_here'
@app.route('/webhooks/notifo', methods=['POST'])
def handle():
sig = request.headers.get('X-Notifo-Signature', '')
raw_body = request.get_data() # raw bytes
secret_hash = hashlib.sha256(YOUR_SIGNING_SECRET.encode()).hexdigest()
expected = 'sha256=' + hmac.new(secret_hash.encode(), raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
event = json.loads(raw_body)
print(event['event'], event['messageId'], event['status'])
return '', 200
<?php
$sig = $_SERVER['HTTP_X_NOTIFO_SIGNATURE'] ?? '';
$rawBody = file_get_contents('php://input');
$secret = 'your_secret_here';
$secretHash = hash('sha256', $secret);
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secretHash);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($rawBody, true);
error_log($event['event'] . ' ' . $event['messageId']);
http_response_code(200);
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
)
const signingSecret = "your_secret_here"
func webhookHandler(w http.ResponseWriter, r *http.Request) {
sig := r.Header.Get("X-Notifo-Signature")
body, _ := io.ReadAll(r.Body)
// Step 1: hash the secret
h := sha256.Sum256([]byte(signingSecret))
secretHash := hex.EncodeToString(h[:])
// Step 2: HMAC the body
mac := hmac.New(sha256.New, []byte(secretHash))
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(sig), []byte(expected)) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
fmt.Println("Verified webhook:", string(body))
w.WriteHeader(http.StatusOK)
}
Important
Always use the raw request body bytes for HMAC computation. Parsing JSON first and re-serializing will change byte order and invalidate the signature.