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.

{: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.1"}]
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.

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. 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.