Twilio
Twilio's REST API for Elixir: send messages, make calls, buy phone numbers,
read recordings and usage. Every call of Twilio's core API (2010-04-01,
197 operations) has a function with docs and typespecs, and every response
decodes into a struct. Plus what isn't in the API: checking that webhooks
come from Twilio, and access tokens for Twilio's client SDKs.
{:ok, message} =
Twilio.Messages.create(to: "+15558675310", from: "+15017122661", body: "Hello from Elixir")
message.sid #=> "SM..."
message.status #=> "queued"
Generated from Twilio's OpenAPI spec by Infusion, so it follows the API as Twilio changes it.
Installation
def deps do
[{:twilio, "~> 0.2"}]
end
Configuration
The SDK reads TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN from the
environment. Or set them in your config:
# config/runtime.exs
config :twilio,
api_key: {System.fetch_env!("TWILIO_ACCOUNT_SID"), System.fetch_env!("TWILIO_AUTH_TOKEN")},
account_sid: System.fetch_env!("TWILIO_ACCOUNT_SID")
With an API key instead of the
auth token, api_key is {key_sid, key_secret} and account_sid stays your
account's SID.
Usage
Every function returns {:ok, result} or {:error, %Infusion.Runtime.Error{}},
and has a ! variant that returns the result or raises.
# Calls
{:ok, call} =
Twilio.Calls.create(
to: "+15558675310",
from: "+15017122661",
url: "http://demo.twilio.com/docs/voice.xml"
)
Twilio.Calls.update(call.sid, status: "completed")
# Phone numbers
{:ok, page} = Twilio.AvailablePhoneNumbers.Local.list("US", area_code: 415, page_size: 5)
Twilio.IncomingPhoneNumbers.create(phone_number: hd(page.available_phone_numbers).phone_number)
# Your account
{:ok, balance} = Twilio.Balance.retrieve()
Lists come one page at a time, or as a lazy stream of every item with
the _all functions, which fetch the next page only when the stream gets
there:
Twilio.Messages.list_all!(date_sent: "2026-11-15", page_size: 100)
|> Stream.filter(&(&1.status == "failed"))
|> Enum.take(10)
Subaccounts: the account comes from your config, and any call can use another one:
Twilio.Messages.list(%{page_size: 20}, account_sid: "AC...")
Errors carry Twilio's error code and message:
case Twilio.Messages.create(to: "+1555", from: "+15017122661", body: "Hi") do
{:ok, message} ->
message
{:error, %Infusion.Runtime.Error{type: :bad_request, body: %{"code" => 21211}}} ->
# https://www.twilio.com/docs/errors/21211: not a valid phone number
:invalid_number
{:error, %Infusion.Runtime.Error{type: :rate_limit}} ->
:try_later
end
Retries: connection errors, 429 and 5xx responses are retried twice with
backoff, as Twilio's official SDKs do. Set max_retries and timeout in
your config, or on a single call:
Twilio.Messages.retrieve("SM...", timeout: 5_000, max_retries: 0)
Anything else: Twilio.request/4 calls an endpoint this SDK has no
function for, and raw: true returns the whole %Req.Response{}, for
headers.
Webhooks
Twilio signs the requests it sends your app (incoming messages and calls, status callbacks) with your auth token. Check the signature before trusting one:
def incoming_sms(conn, params) do
signature = conn |> get_req_header("x-twilio-signature") |> List.first()
if Twilio.Webhook.valid?(current_url(conn), params, signature) do
# params["From"], params["Body"], ...
else
send_resp(conn, 403, "")
end
end
The URL must be the public one Twilio called, query string included. The
auth token comes from config :twilio, auth_token: ... or
TWILIO_AUTH_TOKEN. JSON bodies (bodySHA256) and SharedKey (HMAC-SHA256)
signatures work too. Twilio.Webhook.signature/3 signs requests for your
tests.
Access tokens
For Twilio's Voice, Video, Conversations and Sync client SDKs, your server hands each user a short-lived token, signed with an API key:
Twilio.AccessToken.generate(
identity: "alice",
grants: [voice: [outgoing_application_sid: "AP...", incoming_allow: true]],
ttl: 3600
)
The key comes from config :twilio, api_key_sid: ..., api_key_secret: ...
or TWILIO_API_KEY and TWILIO_API_SECRET. Grants: voice, video,
chat, sync, task_router, playback.
Testing your code
Requests go through Req, so you can stub Twilio
with Req.Test:
# config/test.exs
config :twilio, req_options: [plug: {Req.Test, Twilio}], api_key: {"AC1", "token"}, account_sid: "AC1"
# in a test
Req.Test.stub(Twilio, fn conn ->
Req.Test.json(conn, %{"sid" => "SM1", "status" => "queued"})
end)
assert {:ok, %Twilio.Schemas.Message{sid: "SM1"}} = MyApp.Notify.send_sms("+15558675310")
What's covered
Twilio's core API: Messages, Calls (with recordings, streams, payments, transcriptions), Conferences, Queues, phone numbers (available, incoming, caller IDs), Addresses, Applications, Recordings, Transcriptions, SIP, Usage, Keys and Accounts; webhook signatures and access tokens. Twilio's newer product APIs (Verify, Conversations, Messaging Services, ...) are separate specs and not in this package yet.
License
Apache 2.0. Not affiliated with or endorsed by Twilio.