Signing & verification
Verify that webhook requests genuinely come from elNudge using HMAC-SHA256.
Every webhook POST that elNudge sends includes an X-ElNudge-Signature header. Verifying this signature before processing the payload confirms the request came from elNudge and was not tampered with in transit.
Always verify the signature. Reject any request where it does not match.
How it works
- elNudge computes an HMAC-SHA256 digest of the raw request body bytes using your signing secret as the key.
- The result is hex-encoded and sent as
X-ElNudge-Signature: sha256=<hex>. - Your server recomputes the same HMAC using the raw body and your signing secret, then compares the two values.
Your signing secret is available at Dashboard → Settings → Webhooks → Signing Secret.
Critical: Always use the raw request body bytes — not a parsed and re-serialised JSON object. Any whitespace or key-ordering difference will produce a different digest and cause verification to fail.
Secret rotation
You can rotate your signing secret at any time in Dashboard → Settings → Webhooks → Rotate Secret.
When you rotate the secret, there is a 5-minute grace period during which both the old and the new secret are valid. This lets you deploy your updated handler before the old secret expires. After 5 minutes, only the new secret is accepted.
Code examples
Node.js
const crypto = require('crypto');
function verifySignature(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody) // rawBody must be a Buffer or string — not a parsed object
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader);
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
// Express example
app.post('/webhooks/elnudge', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-elnudge-signature'];
if (!verifySignature(req.body, sig, process.env.ELNUDGE_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// process event...
res.sendStatus(200);
});
Python
import hashlib
import hmac
import os
def verify_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhooks/elnudge', methods=['POST'])
def handle_webhook():
sig = request.headers.get('X-ElNudge-Signature', '')
raw_body = request.get_data() # raw bytes, before any parsing
if not verify_signature(raw_body, sig, os.environ['ELNUDGE_WEBHOOK_SECRET']):
abort(401)
event = request.get_json()
# process event...
return '', 200
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"strings"
)
func verifySignature(rawBody []byte, signatureHeader, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signatureHeader))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
rawBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
sig := r.Header.Get("X-ElNudge-Signature")
if !verifySignature(rawBody, sig, os.Getenv("ELNUDGE_WEBHOOK_SECRET")) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
// process rawBody as JSON...
w.WriteHeader(http.StatusOK)
}
PHP
<?php
function verifySignature(string $rawBody, string $signatureHeader, string $secret): bool {
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signatureHeader);
}
$rawBody = file_get_contents('php://input'); // must read raw input
$sig = $_SERVER['HTTP_X_ELNUDGE_SIGNATURE'] ?? '';
$secret = getenv('ELNUDGE_WEBHOOK_SECRET');
if (!verifySignature($rawBody, $sig, $secret)) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($rawBody, true);
// process event...
http_response_code(200);
Security notes
- Use a constant-time comparison (e.g.
crypto.timingSafeEqual,hmac.compare_digest,hash_equals) to prevent timing attacks. - Never log the raw signing secret — treat it like a password.
- Store the secret in an environment variable, not in source code.
- Reject requests with a missing or malformed header before doing any other processing.