Skip to main content

tai42-channel-twilio

License: Apache 2.0

A Twilio SMS/WhatsApp channel plugin for the TAI ecosystem. It delivers an ask_user question to a human's phone through the Twilio Messages API and bridges the human's reply back into the interactions store — so an agent can reach a person out-of-band instead of only showing the question in the Studio inbox. It implements the tai42_contract.channels.Channel protocol and registers under the name "twilio".

The TAI ecosystem

TAI is an open-source runtime for MCP tools, agents, and workflows. A Channel is "how a question reaches a human" — a pluggable deliverer the runtime resolves by name when ask_user is called with channel=.... This package is one such deliverer (Twilio SMS/WhatsApp); siblings back the same contract with Telegram or Slack. The ecosystem is open-ended: any package can back the same contract, so this repo is this plugin's own full doc home, and the documentation site covers the platform-level story:

Its only tai-* dependencies are tai42-contract (the Channel protocol, ChannelDelivery, ChannelDeliveryError, and the tai42_app handle) and tai42-kit[redis] (HttpxClient, RedisClient, TaiBaseSettings, and the settings cache). Beyond those it depends on httpx, starlette, and pydantic / pydantic-settings. There is no Twilio SDK: the send is one Basic-auth form POST over httpx, and webhook signature validation is ~30 lines of stdlib hmac/hashlib/base64.

Install

Requires Python 3.13+. Install from PyPI into the environment that runs the server:

uv add tai42-channel-twilio

Or from source — clone this repo and add it as an editable dependency; the tai42-* dependencies resolve in-tree from the workspace.

git clone https://github.com/tai42ai/tai42   # next to your app checkout
cd /path/to/your/app
uv add --editable ../tai42/plugins/channel-twilio

Discovery

The runtime discovers this plugin through the manifest's channel_modules key:

channel_modules: ["tai42_channel_twilio"]

At app load the runtime imports every module under the package, and register.py fires the registrations as its import side-effect: the "twilio" channel on tai42_app.channels, and — via the inbound import — the unauthenticated webhook route on tai42_app.http. A bare import tai42_channel_twilio registers nothing — the package is library-safe; only the register module carries the side-effect.

Configuration

Settings are read from the CHANNEL_TWILIO_ environment group (see TwilioSettings / TwilioRedisSettings):

Env var Required Meaning
CHANNEL_TWILIO_ACCOUNT_SID yes Twilio Account SID (Basic-auth username, URL path segment)
CHANNEL_TWILIO_AUTH_TOKEN yes Auth token (SecretStr) — Basic-auth password AND webhook signature HMAC key
CHANNEL_TWILIO_FROM yes The deployment's Twilio number (E.164, or whatsapp:+...)
CHANNEL_TWILIO_DEFAULT_RECIPIENT for recipient-less asks Destination number used when the caller requests no recipient (E.164, or whatsapp:+...)
CHANNEL_TWILIO_ALLOWED_RECIPIENTS for caller-requested recipients Whitelist a caller-requested recipient must be on — comma-separated or a JSON list; an unlisted request is refused loudly
CHANNEL_TWILIO_REDIS_URL yes Correlation store (plugin-owned Redis)
CHANNEL_TWILIO_REDIS_MAX_CONNECTIONS … no The rest of the kit RedisConnectionSettings fields, same names under this prefix
CHANNEL_TWILIO_HTTP_TIMEOUT_SECONDS no (30.0) Outbound send + answer-forward timeout, seconds
CHANNEL_TWILIO_DEDUPE_TTL no (172800) Seen-MessageSid replay-guard window, seconds

Recipient policy is operator-owned: a caller may request a recipient per ask, but it is sent to only if it is on CHANNEL_TWILIO_ALLOWED_RECIPIENTS (fail closed — an unlisted recipient raises, nothing is sent); with no requested recipient the ask goes to CHANNEL_TWILIO_DEFAULT_RECIPIENT, which is trusted without an allowlist check. Secrets live only in the environment.

Two steps happen out-of-band (the plugin never mutates Twilio account configuration at startup):

  1. Point the Twilio number's "A message comes in" webhook at {public base URL}/api/channels/twilio/inbound, HTTP POST (Twilio console or REST API).
  2. Deploy behind a TLS-terminating proxy you control that sets X-Forwarded-Proto / X-Forwarded-Host — the webhook signature is validated against the public URL reconstructed from those headers, so it always tracks what Twilio actually called.

How a human answers

A text or select question arrives as a normal SMS (a select ask lists its options numbered). The human just replies — no code to quote, no prefix. Correlation is fully out-of-band: any reply from the configured number resolves the pair's pending question, so one question can be pending per number pair at a time; a second concurrent one is rejected loudly.

A confirm or external question arrives as a tappable link and is answered in the browser via the callback door — no SMS reply is expected or matched, and it never consumes the number pair.

Security

  • Inbound requests authenticate via X-Twilio-Signature: base64(HMAC-SHA1(auth_token, public_url + sorted form pairs)), validated fail-closed with a constant-time compare before one byte of the body is trusted. A missing/empty auth token is an operator error that raises loudly — never a soft 401 that reads like a bad signature.
  • Twilio's signature scheme carries no timestamp, so a captured request validates forever; the MessageSid dedupe window (48h default) plus HTTPS are the replay guards.
  • The auth token is a SecretStr — it never surfaces in a repr, log line, or traceback; the plaintext is read only at the Basic-auth and HMAC seams.
  • The unauthenticated route bounds its body read (1 MiB → 413) before any signature work.

v1 limits

Limit Consequence Future
Recipients fixed by operator env (_ALLOWED_RECIPIENTS/_DEFAULT_RECIPIENT) A question can only reach a whitelisted or default number — no dynamic/unlisted destinations Directory-backed recipient resolution
One pending question per number pair A second concurrent ask_user over this channel fails loudly with PendingQuestionExistsError while the first is unanswered/unexpired Number pool via Messaging Service sticky sender
SMS-first; WhatsApp freeform only With whatsapp: numbers, a question outside the human's 24h session window is rejected by Twilio (error 63016) as a loud delivery failure Approved-template (ContentSid) sends
Single send attempt A transient Twilio outage fails the ask instead of retrying (no idempotency key exists → a blind retry risks double-texting) App-side dedupe + retry
No StatusCallback DLR handling Delivery confirmation is Twilio's synchronous accept only; an undelivered SMS surfaces as the ask's timeout StatusCallback route for sent/delivered/failed
No timestamp in Twilio's signature scheme Replay of a captured request validates forever for that URL+body; MessageSid dedupe (48h default window) + HTTPS are the guards — (Twilio protocol property)

Development

uv venv --python 3.13
uv pip install --no-sources --group dev --editable .
uv run --no-sync pytest --cov --cov-report=term-missing
uv run --no-sync ruff check .
uv run --no-sync ruff format --check .
uv run --no-sync pyright

The live integration suite (pytest -m integration) sends real messages via the Twilio API and runs only when the CHANNEL_TWILIO_* credentials are present in the environment; it skips cleanly otherwise.

License

Apache-2.0. See LICENSE and NOTICE.

Metadata

Release files for tai42-channel-twilio 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tai42-channel-twilio 1.1.0
File Size Uploaded
tai42_channel_twilio-1.1.0.tar.gz 40.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tai42-channel-twilio 1.1.0
File Interpreter ABI Platform
tai42_channel_twilio-1.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 66.5 kB

Release files / tai42_channel_twilio-1.1.0.tar.gz

Download URL tai42_channel_twilio-1.1.0.tar.gz
Size 40.6 kB
Tags Source
SHA-256 checksum
How to use checksums
7330a5ea03ed2fc949ad8f94a5ac18c4ea9e0127cb689a0732521f54de472fe1
BLAKE2b-256 checksum
How to use checksums
42298f7ff15aae5938b45b9894ad6b1988452052cb2fbdcdc1717cb49d33de7b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / tai42_channel_twilio-1.1.0-py3-none-any.whl

Download URL tai42_channel_twilio-1.1.0-py3-none-any.whl
Size 25.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
11b479414eb29cf30eb43e0bb5e760ba02d3a4181fa254d48fe138cacd8a0406
BLAKE2b-256 checksum
How to use checksums
5be15adf6fb5df79cf8c4867dcf52bf66bb4416c4df18bcd5688dffc54944b47
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

7.0.6

2 release files

7.0.5

2 release files

7.0.4

2 release files

7.0.3

2 release files

7.0.2

2 release files

7.0.1

2 release files

7.0.0

2 release files

6.0.1

2 release files

6.0.0

2 release files

5.3.4

2 release files

5.3.3

2 release files

5.3.2

2 release files

5.3.1

2 release files

5.3.0

2 release files

5.2.3

2 release files

5.2.2

2 release files

5.2.1

2 release files

5.2.0

2 release files

5.1.2

2 release files

5.1.1

2 release files

5.1.0

2 release files

5.0.3

2 release files

5.0.2

2 release files

5.0.1

2 release files

5.0.0

2 release files

4.0.9

2 release files

4.0.8

2 release files

4.0.7

2 release files

4.0.6

2 release files

4.0.5

2 release files

4.0.4

2 release files

4.0.3

2 release files

4.0.2

2 release files

4.0.1

2 release files

4.0.0

2 release files

3.2.1

2 release files

3.2.0

2 release files

3.1.1

2 release files

3.1.0

2 release files

3.0.1

2 release files

3.0.0

2 release files

2.2.1

2 release files

2.2.0

2 release files

2.1.0

2 release files

2.0.0

2 release files

1.1.4

2 release files

1.1.3

2 release files

1.1.2

2 release files

1.1.1

2 release files

This release

1.1.0 This release

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page