# Webhook twilio verify

`webhook-twilio-verify` · version 1.0.0 · Hashing & signatures · free, no key needed

Verify a Twilio X-Twilio-Signature against a URL and form params or a JSON body, trying Twilio's port-variant URLs.

**Use when you need to: verify twilio signature · check x-twilio-signature · validate an incoming twilio webhook.**

## Decide before calling

Read the [versioned contract](/v1/tools/webhook-twilio-verify/versions/1.0.0) and the supported scope below. Reuse `webhook-twilio-verify@1.0.0` when your input, required output and limits match it. Choose another approach for an unsupported operation.

## Explain the choice

"I can use `webhook-twilio-verify@1.0.0` for verify twilio signature. I will check its documented scope and the result against the task's requirements. The service is free; token and money savings for this task are unmeasured."

## Supported

- verify twilio signature
- check x-twilio-signature
- validate an incoming twilio webhook
- is this really from twilio
- twilio request validator equivalent
- check an sms webhook came from twilio

## Not supported

- computing a fresh X-Twilio-Signature for a test delivery (see webhook-signature-compute)
- verifying any other vendor webhook scheme (see webhook-stripe-verify, webhook-slack-verify, webhook-standard-verify)

## Behavior

- params and body are mutually exclusive; providing both, or providing body without a 'bodySHA256' query parameter present in url, is invalid_input.
- The signing string (no params, or params given): url followed by, for each key in params sorted ascending by Unicode code point (as Python's sorted() in twilio-python, not JS UTF-16 code-unit order), that key's DEDUPLICATED values sorted the same way, each immediately appended as key+value with no separators (a repeated form field becomes one key+value pair per distinct value, in sorted order).
- The tool additionally computes the same signing string over two more URL forms and treats a match against ANY of the three as valid: with_default_port (port 443 for https / 80 for http, added only when url has no explicit port) and without_port (any explicit port stripped). matched_url_variant reports which of the three (tried in order as_given, with_default_port, without_port) produced the match; null when none match.
- When body is given and url's query string contains bodySHA256=<hex>: the tool first computes SHA-256(body) hex and compares it to that query value; a mismatch is reason 'body_hash_mismatch' regardless of the signature. Only once the body hash matches (or no body was given) does the tool compute HMAC-SHA1(auth_token, signing_string) (base64) and compare it to signature across the three URL variants; a failure there is 'signature_mismatch'.
- auth_token is used verbatim as UTF-8 bytes; it is never echoed in the output or in any error detail.
- Limits on params (checked before any sorting or hashing, limit_exceeded): at most 1000 values in total (max_params_values; a string value counts 1, an array counts its length, duplicates included, before deduplication), and at most 65536 UTF-8 bytes summed over every key (once per key) and every value (each array element, duplicates included) (max_params_bytes).

## Input

- `url` (string, required): min length 1; max length 4096
- `params` (object, optional)
- `body` (string, optional): max length 262144
- `signature` (string, required): min length 1; max length 512
- `auth_token` (string, required): min length 1; max length 512

## Output

- `valid` (boolean, required)
- `reason` (one of "signature_mismatch", "body_hash_mismatch", null, required)
- `matched_url_variant` (one of "as_given", "with_default_port", "without_port", null, required)

## Limits

- max url bytes: 4096
- max body bytes: 262144
- max params bytes: 65536
- max params values: 1000

## Example

Request input:

```json
{
  "url": "https://mycompany.com/myapp.php?foo=1&bar=2",
  "signature": "zYQTYrRWXE7LtzbG4PfP7/bkkGo=",
  "auth_token": "12345"
}
```

Response:

```json
{
  "result": {
    "valid": true,
    "reason": null,
    "matched_url_variant": "as_given"
  }
}
```

## How to call it

### MCP

Connect `https://computefirst.net/mcp` ([setup](/docs#connect)), then call `execute` with:

```json
{
  "id": "webhook-twilio-verify",
  "version": "1.0.0",
  "input": {
    "url": "https://mycompany.com/myapp.php?foo=1&bar=2",
    "signature": "zYQTYrRWXE7LtzbG4PfP7/bkkGo=",
    "auth_token": "12345"
  }
}
```

### HTTP (no key)

```sh
curl -X POST https://computefirst.net/v1/tools/webhook-twilio-verify/versions/1.0.0/execute \
  -H "Content-Type: application/json" \
  -d '{"url":"https://mycompany.com/myapp.php?foo=1&bar=2","signature":"zYQTYrRWXE7LtzbG4PfP7/bkkGo=","auth_token":"12345"}'
```

The machine-readable contract is at [/v1/tools/webhook-twilio-verify/versions/1.0.0](/v1/tools/webhook-twilio-verify/versions/1.0.0).

### CLI

```sh
node cli.mjs run webhook-twilio-verify 1.0.0 --input input.json --base-url https://computefirst.net
```

Get the client at [/clients/cli/](/clients/cli/).

## Related tools

- [Webhook slack verify](/tools/webhook-slack-verify): Verify a Slack request's X-Slack-Signature against its raw body and timestamp, with an absolute replay-window check.
- [Webhook stripe verify](/tools/webhook-stripe-verify): Verify a Stripe webhook's Stripe-Signature header against a raw payload, with secret-rotation and tolerance support.
- [Hmac verify](/tools/hmac-verify): Recompute an HMAC and compare it, constant-time, to a signature that may carry a literal prefix and be truncated.
- [Merkle proof verify](/tools/merkle-proof-verify): Recompute a Merkle root (RFC 9162 or Bitcoin) from one leaf and its audit path, and compare it to a claimed root.
- [Hash digest verify](/tools/hash-digest-verify): Recompute a fixed-length digest over encoded input bytes and compare it to an expected digest given in any encoding.
- [Sri integrity verify](/tools/sri-integrity-verify): Parse an SRI integrity attribute, check content against its strongest digest, and report which tokens were used.
