4ce4204b6c
CI / Lint (push) Failing after 2s
CI / Build (push) Has been skipped
SDK CI / PHP SDK (push) Failing after 1s
Split PHP SDK / PHP SDK tests (push) Failing after 1s
CI / Test (push) Failing after 1s
CI / Dashboard (push) Failing after 0s
SDK CI / JavaScript SDK (push) Failing after 0s
SDK CI / Python SDK (push) Failing after 2s
SDK CI / Java SDK (push) Failing after 1s
Split PHP SDK / Mirror sdk/php -> rmyndharis/openwa-php (push) Has been skipped
CI / Test (PostgreSQL migrations) (push) Failing after 7m47s
CI / Docker Build (push) Has been skipped
3.3 KiB
3.3 KiB
Webhook Signature Verification
OpenWA signs webhook deliveries when a webhook is configured with a secret. Receivers should verify the signature before processing the event.
Headers
OpenWA sends these system headers with webhook deliveries:
| Header | Description |
|---|---|
X-OpenWA-Signature |
HMAC-SHA256 signature, present only when the webhook has a secret |
X-OpenWA-Event |
Event name, for example message.received |
X-OpenWA-Idempotency-Key |
Stable key for duplicate detection |
X-OpenWA-Delivery-Id |
Unique identifier for this delivery (stable across retry attempts) |
X-OpenWA-Retry-Count |
Retry count for the current delivery |
The signature format is:
sha256=<hex digest>
The digest is computed over the exact raw request body bytes using the webhook secret.
Node.js / Express
Use express.raw() for the webhook route so the signature is checked against the raw body. Parse JSON only after verification succeeds.
const crypto = require('crypto');
const express = require('express');
const app = express();
const WEBHOOK_SECRET = process.env.OPENWA_WEBHOOK_SECRET;
function verifyOpenWASignature(rawBody, signature, secret) {
if (!signature || !secret) return false;
const expected =
'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const signatureBuffer = Buffer.from(signature);
const expectedBuffer = Buffer.from(expected);
if (signatureBuffer.length !== expectedBuffer.length) return false;
return crypto.timingSafeEqual(signatureBuffer, expectedBuffer);
}
app.post('/openwa/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.header('X-OpenWA-Signature');
if (!verifyOpenWASignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
// Process event here.
// Return a 2xx response only after the event is safely accepted.
return res.status(200).send('OK');
});
Python / FastAPI
Read the raw request body before parsing JSON.
import hmac
import hashlib
import os
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
WEBHOOK_SECRET = os.environ["OPENWA_WEBHOOK_SECRET"]
def verify_openwa_signature(raw_body: bytes, signature: str | None, secret: str) -> bool:
if not signature:
return False
expected = "sha256=" + hmac.new(
secret.encode("utf-8"), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.post("/openwa/webhook")
async def openwa_webhook(request: Request):
raw_body = await request.body()
signature = request.headers.get("x-openwa-signature")
if not verify_openwa_signature(raw_body, signature, WEBHOOK_SECRET):
raise HTTPException(status_code=401, detail="Invalid signature")
event = await request.json()
# Process event here.
return {"status": "ok"}
Processing Checklist
- Verify
X-OpenWA-Signaturebefore trusting or parsing the event. - Use the exact raw request body received by your HTTP server.
- Use a constant-time comparison function.
- Return
401for invalid signatures. - Use
X-OpenWA-Idempotency-Keyto avoid duplicate processing on retries. - Return a
2xxresponse only after the event is accepted for processing.