Skip to Content
WebhooksSignature Verification

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

  1. You configure a webhook secret when setting up your webhook endpoint in the Proximity dashboard
  2. For each delivery, Proximity computes an HMAC-SHA256 hash of the raw request body using your secret
  3. The signature is included in the X-Webhook-Signature header as a hex-encoded string
  4. Your server recomputes the signature and compares it to the header value

Verification Steps

  1. Extract the X-Webhook-Signature header from the request
  2. Read the raw request body (not parsed JSON — the exact bytes matter)
  3. Compute HMAC-SHA256 of the raw body using your webhook secret as the key
  4. 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", 200

Go

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

IssueSolution
Signature mismatchEnsure you’re using the raw request body, not re-serialized JSON
Encoding errorsThe secret and body should both be treated as UTF-8
Middleware interferenceEnsure no middleware parses the body before your verification code
Last updated on