# Webhooks

PressOne notifies your server about calls by requesting URLs you configure (the voice URL on a number, the `action` URL on a verb such as `<Gather>`, or a status callback URL on an outbound call). This guide covers the request format common to all of them, and how to verify that a request genuinely came from PressOne.

## Standard parameters

Every webhook and action request includes the following parameters, in addition to any parameters specific to that request:

| Parameter | Description |
|-----------|-------------|
| `CallSid` | Unique identifier for the call. |
| `AccountSid` | Your account SID. |
| `From` | The number that placed the call. |
| `To` | The number the call was placed to. |
| `Caller` | Mirrors `From`, retained for compatibility with existing client libraries. |
| `Called` | Mirrors `To`, retained for compatibility with existing client libraries. |
| `CallStatus` | State of the call when the request was sent. Voice URL and action requests always send `in-progress`, since the call is already answered by the time PressOne requests further instructions. Status callbacks use a wider range of values. See [status callbacks](/voice/status-callbacks). |
| `Direction` | `inbound` for a call received on one of your numbers, `outbound-api` for a call created through the REST API. |
| `ApiVersion` | The API version in effect for the call. Currently always `2010-04-01`. |

## Transport

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

Requests identify themselves with a `User-Agent` header of `TwilioProxy/1.1` (the value is kept for compatibility with tooling that filters on that string).

PressOne waits at most 10 seconds for your server to respond, and follows at most 5 HTTP redirects before treating the request as failed.

## Verify webhook signatures

Every request includes an `X-Twilio-Signature` header (the header name is kept for compatibility with existing signature-verification libraries). Validate it before trusting the request body: anyone who knows one of your webhook URLs can otherwise send a request that looks legitimate.

The signature is computed as follows:

1. Start with the request URL as PressOne signed it: for a `GET` request, this is the full URL including its query string; for a `POST` request, this is the URL without a query string.
2. For a `POST` request, sort the form parameter keys alphabetically, then append each key immediately followed by its value to the URL string from step 1. Skip this step for a `GET` request: the query string already carries the parameters, so nothing further is appended.
3. Compute an HMAC-SHA1 of the resulting string, using your auth token as the key.
4. Base64-encode the digest. This is the expected signature.

Compare the expected signature to the `X-Twilio-Signature` header using a constant-time comparison, and reject the request with `403` if they do not match.

<CodeTabs syncKey="lang">

```javascript title="Node.js"
const crypto = require("crypto");
const express = require("express");

const AUTH_TOKEN = "AUTH_TOKEN";

function verifySignature(authToken, url, params, signature) {
  let data = url;

  for (const key of Object.keys(params).sort()) {
    data += key + params[key];
  }

  const expected = crypto
    .createHmac("sha1", authToken)
    .update(data, "utf8")
    .digest("base64");

  const expectedBuffer = Buffer.from(expected);
  const signatureBuffer = Buffer.from(signature || "");

  return (
    expectedBuffer.length === signatureBuffer.length &&
    crypto.timingSafeEqual(expectedBuffer, signatureBuffer)
  );
}

const app = express();
app.use(express.urlencoded({ extended: false }));

app.post("/voice/incoming", (req, res) => {
  const signature = req.header("X-Twilio-Signature");
  const url = "https://example.com/voice/incoming";

  if (!verifySignature(AUTH_TOKEN, url, req.body, signature)) {
    return res.status(403).send("Signature verification failed");
  }

  const oneml = `<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>Request verified.</Say>
  <Hangup/>
</Response>`;

  res.type("text/xml").send(oneml);
});

app.listen(3000);
```

```python title="Python"
import base64
import hashlib
import hmac

from flask import Flask, request, Response

AUTH_TOKEN = "AUTH_TOKEN"

app = Flask(__name__)


def verify_signature(auth_token, url, params, signature):
    data = url
    for key in sorted(params.keys()):
        data += key + params[key]

    digest = hmac.new(
        auth_token.encode("utf-8"), data.encode("utf-8"), hashlib.sha1
    ).digest()
    expected = base64.b64encode(digest).decode("utf-8")

    return hmac.compare_digest(expected, signature or "")


@app.route("/voice/incoming", methods=["POST"])
def voice_incoming():
    signature = request.headers.get("X-Twilio-Signature", "")
    url = "https://example.com/voice/incoming"

    if not verify_signature(AUTH_TOKEN, url, request.form.to_dict(), signature):
        return "Signature verification failed", 403

    oneml = """<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>Request verified.</Say>
  <Hangup/>
</Response>"""

    return Response(oneml, mimetype="text/xml")


if __name__ == "__main__":
    app.run(port=3000)
```

</CodeTabs>

> **Note:** For a `GET` request, sign the full request URL, including its query string, and pass an empty parameter object to `verifySignature` or `verify_signature`: the query string is already part of the signed URL, so no parameters get appended.

## Action parameters by verb

Beyond the standard parameters, each verb's `action` request carries the fields that verb collected. These are documented in full on each verb's page:

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

## Next steps

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