validate_webhook_signature

View as MarkdownOpen in Claude

Verify that an incoming webhook was signed by SignalWire with your Signing Key. This is the primary standalone validator for custom (non-AgentBase) servers. Pass it the raw, unparsed request body so the signature matches what the platform computed.

The signature value comes from the X-SignalWire-Signature request header (or X-Twilio-Signature for Compatibility callbacks). See the security overview for the recognized header names.

Parameters

signing_key
strRequired

Your Signing Key from the Dashboard, as a UTF-8 string. A None or empty value raises ValueError — this is a programming error, not a validation failure.

signature
strRequired

The X-SignalWire-Signature header value (or X-Twilio-Signature for Compatibility callbacks). A missing or empty value returns False without raising.

url
strRequired

The full URL SignalWire POSTed to — scheme, host, optional port, path, and query. It must match what the platform saw.

raw_body
strRequired

The raw request body as a UTF-8 string, before any JSON or form parsing. Must be a str; passing a parsed dict raises TypeError.

Returns

boolTrue if the signature matches, False otherwise.

Example

1from fastapi import FastAPI, Request, Response, status
2from signalwire.core.security import validate_webhook_signature
3
4app = FastAPI()
5SIGNING_KEY = "PSK_your_signing_key"
6
7@app.post("/webhook")
8async def webhook(request: Request):
9 raw_body = (await request.body()).decode("utf-8")
10 signature = request.headers.get("x-signalwire-signature", "")
11
12 if not validate_webhook_signature(
13 signing_key=SIGNING_KEY,
14 signature=signature,
15 url=str(request.url),
16 raw_body=raw_body,
17 ):
18 return Response(status_code=status.HTTP_403_FORBIDDEN)
19
20 # Signature is valid — safe to process the request.
21 return {"ok": True}

If you already have pre-parsed form parameters instead of a raw body, use validate_request, which accepts either form.