Signals Docs
Guides

Webhooks

Signals tells your server about calls by requesting URLs you configure: the voice URL and fallback URL on a voice application, the Url you pass when you create a call, the action URL on a verb such as <Gather>, and status callback URLs. This guide covers the request format they share, the limits Signals applies, and how to see exactly what Signals sent and what your server answered.

Standard parameters

Every voice URL, Url and action request includes these parameters, in addition to any that are specific to the request:

ParameterDescription
CallSidUnique identifier for the call.
AccountSidYour Voice Account SID.
FromThe number that placed the call.
ToThe number the call was placed to.
CallerSame value as From, kept for compatibility with existing client libraries.
CalledSame value as To, kept for compatibility with existing client libraries.
CallStatusAlways in-progress on voice URL and action requests, because the call is already answered when Signals asks for instructions. Status callbacks use a wider range of values. See Status callbacks.
Directioninbound for a call received on one of your numbers, outbound-api for a call created through the Calls API.
ApiVersionAlways 2010-04-01.

Check that AccountSid is your own before you act on a request.

Transport

By default, Signals sends parameters as an application/x-www-form-urlencoded body over POST. Set the relevant method field to GET to receive them as query string parameters instead.

Requests carry a User-Agent header of TwilioProxy/1.1, kept for compatibility with tooling that filters on that value.

Signals applies these limits to every request:

  • It waits at most 10 seconds for your server to respond.
  • It follows at most 5 HTTP redirects, and never a redirect from https:// to http://.
  • It only connects to public addresses. A URL that resolves to a private, loopback or link-local address is refused.

Fallback URL

A voice application can name a fallback URL and method. Signals requests the fallback once, only for the first document of an inbound call, when the voice URL:

  • Cannot be reached or times out.
  • Answers with a document Signals cannot parse or run.

The fallback request carries the standard parameters plus ErrorCode and ErrorUrl. ErrorCode is 11200 when the voice URL could not be retrieved and 12100 when its document could not be run. ErrorUrl is the voice URL that failed.

A verb that fails later in the call, such as a failing <Redirect>, <Gather> or <Record> action, does not reach the fallback. If the fallback also fails, or the application has none, the call is rejected.

Host the fallback on different infrastructure from the voice URL, so one outage does not take down both. A fallback that returns a short apology and hangs up is enough:

XMLCode
<?xml version="1.0" encoding="UTF-8"?> <Response> <Say>We are unable to take your call right now. Please try again shortly.</Say> <Hangup/> </Response>

Request signatures

Every request Signals makes to your URLs carries an X-Twilio-Signature header. Signals computes it the way Twilio does, keyed with your Voice Account auth token. Validate it to confirm a request came from Signals and was not changed.

The signing key is your auth token. There is no separate signing secret. To view the token, open Developers, then Voice Account in the console and choose Show. Admins and engineers can view it at any time.

Validate with a Twilio helper library

Twilio's helper libraries validate Signals requests unchanged. Pass your auth token, the header value, the full URL Signals requested and the request parameters.

JavascriptCode
import express from "express"; import twilio from "twilio"; const app = express(); app.use(express.urlencoded({ extended: false })); app.post("/voice/incoming", (req, res) => { const valid = twilio.validateRequest( process.env.SIGNALS_AUTH_TOKEN, req.get("X-Twilio-Signature") ?? "", "https://voice.example.com" + req.originalUrl, req.body, ); if (!valid) return res.status(403).end(); res.type("text/xml").send("<Response><Say>Thank you for calling.</Say></Response>"); });
Code
import os from flask import Flask, abort, request from twilio.request_validator import RequestValidator app = Flask(__name__) validator = RequestValidator(os.environ["SIGNALS_AUTH_TOKEN"]) @app.post("/voice/incoming") def incoming(): signature = request.headers.get("X-Twilio-Signature", "") if not validator.validate(request.url, request.form, signature): abort(403) return "<Response><Say>Thank you for calling.</Say></Response>", 200, {"Content-Type": "text/xml"}

How the signature is computed

  1. Start with the full URL Signals requested, exactly as sent: scheme, host, port when it is not the default, path and query string. Leave out any fragment.
  2. For a POST, sort the body parameters by name. Append each name followed directly by its value, with no separator. When a name repeats, sort its distinct values and append the name and each value in that order. For a GET, the parameters are already in the query string, so append nothing.
  3. Compute HMAC-SHA1 over the UTF-8 bytes of that string, keyed with the UTF-8 bytes of your auth token.
  4. Base64-encode the result and compare it to X-Twilio-Signature in constant time.
JavascriptCode
import crypto from "node:crypto"; function isValidSignature(authToken, signature, url, params) { let data = url; for (const name of Object.keys(params).sort()) { const values = [...new Set([].concat(params[name]))].sort(); for (const value of values) data += name + value; } const expected = Buffer.from( crypto.createHmac("sha1", authToken).update(data, "utf8").digest("base64"), ); const given = Buffer.from(signature); return expected.length === given.length && crypto.timingSafeEqual(expected, given); }

For a GET request, pass {} as params.

If your server sits behind a proxy or load balancer, rebuild the public URL Signals called, with its scheme, host and port, not the internal URL your application sees. A different URL gives a different signature.

When you rotate the auth token

Rotation replaces the token in one step. The old token stops working when the change applies, usually within seconds. After that:

  • New calls and status callbacks are signed with the new token.
  • Calls already in progress keep signing their document and action requests with the old token until they hang up.

While a rotation is in progress, accept either token for a short time: check the new token first, then the old one.

Inspect requests with call events

Signals records each request it makes to your URLs during a call, together with your server's response: status code, content type, body and how long it took. Read them with the call's events resource, using the same credentials as the rest of the Calls API:

TerminalCode
curl -u "ACCOUNT_SID:AUTH_TOKEN" \ "https://api.signals.pressone.africa/2010-04-01/Accounts/ACCOUNT_SID/Calls/CA5388f7a5e04d0ed71cac5cdf1ea17a53/Events.json?PageSize=20&Page=0"
JSONCode
{ "events": [ { "request": { "url": "https://voice.example.com/voice/incoming", "method": "POST", "parameters": { "call_sid": "CA5388f7a5e04d0ed71cac5cdf1ea17a53", "account_sid": "AC9ce1cd445308e3ec5080c6126288aa09", "from": "+2348031234567", "to": "+2342013300100", "call_status": "in-progress", "direction": "inbound", "api_version": "2010-04-01" } }, "response": { "date_created": "Thu, 02 Jul 2026 12:00:01 +0000", "request_duration": 34, "response_code": 200, "content_type": "text/xml", "response_body": "<Response><Say>Thank you for calling.</Say></Response>" } } ], "start": 0, "end": 0, "page": 0, "page_size": 20, "uri": "/2010-04-01/Accounts/AC9ce1cd445308e3ec5080c6126288aa09/Calls/CA5388f7a5e04d0ed71cac5cdf1ea17a53/Events.json?PageSize=20&Page=0", "first_page_uri": "/2010-04-01/Accounts/AC9ce1cd445308e3ec5080c6126288aa09/Calls/CA5388f7a5e04d0ed71cac5cdf1ea17a53/Events.json?PageSize=20&Page=0", "next_page_uri": null, "previous_page_uri": null }

Things to know about call events:

  • Events are listed in the order they happened. PageSize accepts 1 to 1,000 (default 50) and Page starts at 0. Only the PascalCase parameter names are recognized.
  • Parameter names are converted to snake_case, so CallSid appears as call_sid.
  • Query string values in recorded URLs are replaced with [REDACTED].
  • request_duration is in milliseconds. response is absent when your server never answered.
  • Request and response content is stored only while call event history is enabled on your Voice Account, which is the default, and is kept for 30 days. Change the setting in the console; it applies to calls that start after the change.

The console shows the same history on each call under Programmable Voice, Calls.

Action parameters by verb

Beyond the standard parameters, each verb's action request carries what the verb collected. Each verb's page documents these in full:

VerbParameterDescription
GatherDigitsDTMF digits collected.
GatherSpeechResultTranscribed speech text.
GatherConfidenceConfidence score for SpeechResult.
RecordRecordingSidUnique identifier for the recording.
RecordRecordingUrlURL of the recording audio on the Calls API. Requires your AccountSid and auth token.
RecordRecordingDurationDuration of the recording, in seconds.
DialDialCallStatusOutcome of the dialed call.
DialDialCallDurationDuration of the bridged call, in seconds. Present only when the call bridged.
DialDialCallSidUnique identifier for the dialed call leg. Present only when the call bridged.

Next steps

Last modified on