Verify SWML request signatures
When you serve SWML from your own web server, anyone who learns your endpoint URL can POST to it and read back the SWML document you return. Since a SWML document can contain phone numbers, SIP credentials, and prompts, that endpoint should not answer to just anyone.
To let you check the caller, SignalWire signs every request for a SWML document with an HMAC signature derived from your project’s signing key. Verifying that signature proves the request came from SignalWire and that neither the URL nor the body was altered in transit. The requests an AI agent sends to your server, such as SWAIG function calls and the post-prompt summary, carry the same signature.
There is no setting to enable signing. Every project has a signing key, and SignalWire signs these requests with it automatically. Your server decides whether to check the signature. In the rare case SignalWire can’t compute a signature, it sends the request unsigned, as described in Plan for unsigned requests, redirects, and retries.
For production applications it is extremely important to verify the signature, so that requests from a malicious third party are rejected instead of being served a SWML document.
Serve the document over HTTPS as well. SignalWire accepts an http:// URL, but the document, and
any credentials in it, then travels unencrypted. To require a password as well as a signature, put
basic auth credentials in the URL you configure, as in https://username:password@example.com/swml.
SignalWire removes them from the URL and sends them in an Authorization header when it fetches
the document.
Which requests are signed
Every POST SignalWire makes to fetch a SWML document from a URL you control is signed. That includes:
- The initial fetch, when a Resource or phone number is configured with an External URL rather than a hosted script, and the fetch from your fallback URL when the primary one fails.
- Every subsequent fetch caused by
executeortransferpointing at an external URL. - The same fetches on the messaging side, when a number handles inbound SMS and MMS with Messaging SWML.
- Every request an AI agent makes during a call: the POST to each
SWAIG function’s
web_hook_url, including one set inSWAIG.defaults, the webhooks in a function’sdata_map, the function-definition request to eachSWAIG.includesURL, the summary sent topost_prompt_url, the request topost_prompt_urlthat loads a stored conversation when the agent setssave_conversationand aconversation_id, and the live stream sent todebug_webhook_url. - Function webhooks from
ai_sidecarandamazon_bedrock.
Requests during a call, AI agent requests included, carry two headers. Requests for a messaging document carry the SHA-1 header alone, so verify that one if your endpoint serves both.
Both are computed over the same string: the request URL concatenated directly with the raw request body, with no separator.
The url is the full URL SignalWire requested, including any query string. A call request,
AI agent requests included, signs that URL without any basic auth credentials you embedded in it; a
message request signs it exactly as you configured it, credentials included. The raw_body is the
JSON payload exactly as sent. For a SWML document, that’s the object containing call (or
message for a messaging document), vars, envs, and, when the document was reached through
execute or transfer, params. For an AI agent request, it’s the JSON the agent posts, such as
the SWAIG function call or the post-prompt summary. A data_map webhook is the exception: its body
is the JSON built from the webhook’s params, form-encoded instead when the webhook sets
form_param. When a data_map webhook has no body, as with a GET, the signature covers the URL
alone.
Verify against the URL you configured, in the Dashboard or in your SWML, not the URL your framework reconstructs from the incoming request. Proxies, load balancers, and tunnels such as ngrok routinely rewrite the host or scheme, which changes the string being hashed and makes a valid signature look invalid. For a call request, remove any credentials embedded in the URL first.
When the query string changes from call to call, as it does when the Server SDKs add a per-call token to an agent’s webhook URLs, you can’t hard-code the URL. Rebuild it from your public base URL plus the path and query string of the request you received.
Which requests are not signed
A few requests a SWML document can send are not signed. An endpoint that insists on a signature rejects them, so protect these endpoints with basic auth or a shared secret instead.
The request method is unsigned when a call runs it. Use
basic auth in its URL, or send a shared secret in a custom X- header through the method’s
headers parameter. Its messaging counterpart does carry
the SHA-1 header.
An amazon_bedrock agent signs its function
webhooks, but not the summary it sends to post_prompt_url or the request for its
summarize_conversation function. Embed basic auth credentials in those URLs, in the form
username:password@url.
Plan for unsigned requests, redirects, and retries
If SignalWire can’t compute a signature for a request, it sends the request without the signature headers rather than not sending it. An endpoint that requires a signature rejects that request, and the fetch or function call it carried fails. Treat a missing signature the same as an invalid one.
Signed AI agent requests don’t follow redirects. A signature covers the exact URL it was computed
for, so a 3xx response to a signed AI agent request counts as a failed request instead of being
followed. An unsigned request, such as the amazon_bedrock post-prompt summary or a request sent
without a signature for the reason above, does follow a redirect. Point web_hook_url,
post_prompt_url, and the agent’s other URLs at their final address, including the https://
scheme and any trailing slash your server would otherwise redirect to.
When an AI agent’s POST to a SWAIG function’s web_hook_url, to post_prompt_url, or to another of
its webhook URLs fails to connect, times out, or gets a 5xx response, SignalWire retries it with
the same body and the same signature. data_map webhooks are not retried. A signature identifies
the content of a request, not one delivery, so don’t reject a request only because you’ve seen its
signature before.
Because a timeout triggers a retry, a slow handler can receive the same function call again while it is still processing the first. Make your handlers idempotent, so that running a function twice has the same effect as running it once.
Get your signing key
Your signing key is on the API Credentials page of your Dashboard. Click Show to reveal it. Each project has its own key, so use the key belonging to the project that serves the call.

You can rotate the key with the reset button on the same page. A new key takes about a minute to become active, and the page shows it to you before you confirm the reset so you can copy it into your application first.
Treat the signing key like a password: keep it in an environment variable or a secret manager, not in the source you deploy.
Verify signatures with the Server SDKs
Agents built with the Server SDKs check signatures for you. Give the agent your
signing key, and it verifies every POST to its SWML, SWAIG, and post-prompt endpoints, answering an
unsigned or mis-signed request with 403 Forbidden. Pass the key when you create the agent, or set
it in the SIGNALWIRE_SIGNING_KEY environment variable and the agent reads it from there.
Install the latest Server SDK to get signature checking. Without a signing key, the agent logs a warning at startup and accepts unsigned requests.
The signature check works alongside the agent’s other protections: the basic auth credentials the SDK adds to every webhook URL it generates, and the per-function token on functions marked secure. Server SDK security covers all three.
Behind a reverse proxy or a tunnel such as ngrok, set SWML_PROXY_URL_BASE to your public base
URL, for example https://example.ngrok.io. The agent then checks the signature against the URL
SignalWire requested, not the one the proxy forwarded to it.
Verify the signature in Node
The validateRequest helper in @signalwire/web-api implements the check for you:
validateRequest needs the raw request body, so capture it before your JSON parser consumes
it. Re-serializing the parsed object is not reliable — key order and whitespace change, and the
hash changes with them.
Verify the signature in any language
The scheme is a plain hex HMAC, so you can implement it directly wherever a helper is not available. Compare digests with a constant-time comparison rather than string equality.
To verify the stronger header on a call request, hash the same url + raw_body string with SHA-256
and compare it against X-Signalwire-SHA256-Signature.
Confirm your endpoint rejects forgeries
With the server running and reachable at the URL you configured, POST to it yourself, without a signature:
The response is 403 Forbidden, with Invalid signature as the body. Now place a call to the
number pointed at that URL: this request carries a valid signature, passes the check, and the caller
hears the prompt your document plays.
If the real call is rejected too, the WEBHOOK_URL in your code doesn’t match the URL SignalWire
requested — an ngrok URL that changed when the tunnel restarted is the common cause. The next
section covers the rest.
Troubleshoot a failing signature
A signature that never validates almost always comes down to one of these:
- The URL does not match. Scheme, host, port, path, and query string all feed the hash. Use the exact URL from the Dashboard or your SWML, including its query string. When the query string varies per call, rebuild the URL from your public base URL plus the path and query string of the request you received.
- The body was re-serialized. Hash the bytes you received, not
JSON.stringifyof the parsed object. - The wrong project’s key. Signing keys are per project.
- The key was just rotated. A new key needs about a minute to become active.
- Basic auth in the URL. A call, AI agent requests included, strips embedded credentials before signing, so hash the URL without them. A message signs the URL as configured, so hash it with them.
- A redirect. A signed AI agent request isn’t redirected, so a webhook URL that answers with a
3xxfails. A SWML document fetch does follow a redirect, but its signature still covers the URL you configured, not the one it was redirected to.
Next steps
- Handle incoming calls from code — set up the external SWML endpoint this guide protects.
- Server SDK security — signatures, basic auth, and per-function tokens for agents built with the Server SDKs.
- Webhooks — how webhooks and status callbacks work across the platform.