Signature Verification
Every webhook request includes an HMAC-SHA256 signature in the X-Webhook-Signature header. You should always verify this signature to confirm the request genuinely came from Proximity.
How It Works
- You configure a webhook secret when setting up your webhook endpoint in the Proximity dashboard
- For each delivery, Proximity computes an HMAC-SHA256 hash of the raw request body using your secret
- The signature is included in the
X-Webhook-Signatureheader as a hex-encoded string - Your server recomputes the signature and compares it to the header value
Verification Steps
- Extract the
X-Webhook-Signatureheader from the request - Read the raw request body (not parsed JSON — the exact bytes matter)
- Compute HMAC-SHA256 of the raw body using your webhook secret as the key
- Compare the computed signature with the header value using a constant-time comparison
Important: Always use constant-time string comparison to prevent timing attacks.
Code Examples
Node.js (Express)
const crypto = require('crypto');
const WEBHOOK_SECRET = process.env.PROXIMITY_WEBHOOK_SECRET;
app.post('/webhooks/proximity', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const body = req.body; // raw Buffer
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(body);
console.log(`Verified event: ${event.event}`);
res.status(200).send('OK');
});Python (Flask)
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["PROXIMITY_WEBHOOK_SECRET"]
@app.route("/webhooks/proximity", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Webhook-Signature")
body = request.get_data() # raw bytes
expected = hmac.new(
WEBHOOK_SECRET.encode(),
body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401, "Invalid signature")
event = request.get_json()
print(f"Verified event: {event['event']}")
return "OK", 200Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
)
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("X-Webhook-Signature")
body, _ := io.ReadAll(r.Body)
secret := os.Getenv("PROXIMITY_WEBHOOK_SECRET")
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(signature), []byte(expected)) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}C# (.NET)
using System.Security.Cryptography;
using System.Text;
app.MapPost("/webhooks/proximity", async (HttpContext context) =>
{
var signature = context.Request.Headers["X-Webhook-Signature"].ToString();
using var reader = new StreamReader(context.Request.Body);
var body = await reader.ReadToEndAsync();
var secret = Environment.GetEnvironmentVariable("PROXIMITY_WEBHOOK_SECRET")!;
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(body));
var expected = Convert.ToHexString(hash).ToLowerInvariant();
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(signature),
Encoding.UTF8.GetBytes(expected)))
{
return Results.Unauthorized();
}
return Results.Ok("OK");
});Troubleshooting
| Issue | Solution |
|---|---|
| Signature mismatch | Ensure you’re using the raw request body, not re-serialized JSON |
| Encoding errors | The secret and body should both be treated as UTF-8 |
| Middleware interference | Ensure no middleware parses the body before your verification code |
Last updated on