# 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:

| Parameter | Description |
|-----------|-------------|
| `CallSid` | Unique identifier for the call. |
| `AccountSid` | Your Voice Account SID. |
| `From` | The number that placed the call. |
| `To` | The number the call was placed to. |
| `Caller` | Same value as `From`, kept for compatibility with existing client libraries. |
| `Called` | Same value as `To`, kept for compatibility with existing client libraries. |
| `CallStatus` | Always `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](/programmable-voice/status-callbacks). |
| `Direction` | `inbound` for a call received on one of your numbers, `outbound-api` for a call created through the Calls API. |
| `ApiVersion` | Always `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:

```xml
<?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.

```js title="Node.js (Express)"
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>");
});
```

```python title="Python (Flask)"
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.

```js title="Node.js, without a helper library"
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:

```bash
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"
```

```json
{
  "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:

| Verb | Parameter | Description |
|------|-----------|-------------|
| [Gather](/programmable-voice/oneml/gather) | `Digits` | DTMF digits collected. |
| [Gather](/programmable-voice/oneml/gather) | `SpeechResult` | Transcribed speech text. |
| [Gather](/programmable-voice/oneml/gather) | `Confidence` | Confidence score for `SpeechResult`. |
| [Record](/programmable-voice/oneml/record) | `RecordingSid` | Unique identifier for the recording. |
| [Record](/programmable-voice/oneml/record) | `RecordingUrl` | URL of the recording audio on the Calls API. Requires your `AccountSid` and auth token. |
| [Record](/programmable-voice/oneml/record) | `RecordingDuration` | Duration of the recording, in seconds. |
| [Dial](/programmable-voice/oneml/dial) | `DialCallStatus` | Outcome of the dialed call. |
| [Dial](/programmable-voice/oneml/dial) | `DialCallDuration` | Duration of the bridged call, in seconds. Present only when the call bridged. |
| [Dial](/programmable-voice/oneml/dial) | `DialCallSid` | Unique identifier for the dialed call leg. Present only when the call bridged. |

## Next steps

- [Receiving calls](/programmable-voice/receiving-calls): create a voice application and answer inbound calls.
- [Status callbacks](/programmable-voice/status-callbacks): get notified as a call moves through its lifecycle.
- [OneML overview](/programmable-voice/oneml/overview): every verb available for scripting a call.
