Skip to content

SMS drivers

Environment.sms.driver decides who actually sends SMS messages. authn.sh ships three drivers in v0.4: Twilio, Vonage, and a null driver for development. Drivers are configured at the environment level via PATCH /v1/instance (or Dashboard → Configure → Messaging → SMS); credentials are write-only — they’re accepted on PATCH, encrypted at rest, and never returned by any GET (BAPI or FAPI).

driverWhen to use itRequired credentialsOptional credentials
nullDev environments where you don’t want real SMS sends. Sends are dropped with a no-op audit-log entry.
twilioProduction. Default choice for US + EU coverage.twilio.account_sid, twilio.auth_tokentwilio.messaging_service_sid
vonageProduction. Often better rates outside US / EU; supports more numbering ranges.vonage.api_key, vonage.api_secret

from_number is required for twilio (unless messaging_service_sid is set) and vonage — it’s the E.164 number / short code messages send from. Per-template overrides via SmsTemplate.from_number_override (see SMS templates) take precedence at send time.

The dashboard wizard writes to InstanceSettings.sms.* directly, so most operators never touch env vars. For self-host deployments that bootstrap state from infrastructure-as-code, the matrix is:

SettingEnv var bootstrap keyRequired for
sms.driverAUTHN_SMS_DRIVERalways (defaults to null)
sms.from_numberAUTHN_SMS_FROM_NUMBERtwilio (when no MSID), vonage
sms.twilio.account_sidAUTHN_SMS_TWILIO_ACCOUNT_SIDtwilio
sms.twilio.auth_tokenAUTHN_SMS_TWILIO_AUTH_TOKENtwilio
sms.twilio.messaging_service_sidAUTHN_SMS_TWILIO_MESSAGING_SERVICE_SIDoptional
sms.vonage.api_keyAUTHN_SMS_VONAGE_API_KEYvonage
sms.vonage.api_secretAUTHN_SMS_VONAGE_API_SECRETvonage

The bootstrap pipeline reads these on first deploy and seeds InstanceSettings.sms.*. After bootstrap, all edits go through the BAPI / Dashboard — env-var changes are not re-read.

Set up a Twilio account, provision a number (or a Messaging Service for higher throughput), and copy the credentials into the dashboard.

PATCH /v1/instance
Authorization: Bearer sk_live_…
Content-Type: application/json
{
"sms": {
"driver": "twilio",
"from_number": "+15555550100",
"twilio": {
"account_sid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"auth_token": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
}
}
}

auth_token is write-only — GET /v1/instance returns the twilio block with auth_token redacted. Send null to clear:

PATCH /v1/instance
Content-Type: application/json
{ "sms": { "twilio": { "auth_token": null } } }

Twilio Messaging Services pool multiple numbers / short codes to handle higher throughput, automatic failover, and country-specific routing. Set twilio.messaging_service_sid (MGxxxx…); when present, the driver passes it on send and Twilio picks the right number. from_number is ignored in this mode (per-template from_number_override is also ignored).

PATCH /v1/instance
Content-Type: application/json
{
"sms": {
"driver": "twilio",
"twilio": {
"messaging_service_sid": "MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
}

The driver registers a webhook with Twilio for delivery / failure callbacks. authn.sh emits sms.delivered / sms.failed events when those land — see Webhooks for the envelope.

PATCH /v1/instance
Content-Type: application/json
{
"sms": {
"driver": "vonage",
"from_number": "+447700900100",
"vonage": {
"api_key": "abcd1234",
"api_secret": "efgh5678"
}
}
}

Same credential semantics as Twilio: api_secret is write-only; send null to clear.

Vonage’s API rejects sender IDs containing spaces or non-ASCII characters in some regions; if a send fails with a Vonage-side rejection, surface the error from the sms.failed webhook.

driver: null (or omitted) drops every send with a no-op audit-log entry. Useful for:

  • Local dev. No carrier credentials needed. The verification_code SMS body is logged at info level and audit-log; combine with test_mode: enabled so the 424242 magic code accepts every Challenge against the reserved test-number range.
  • CI. Tests assert on the audit-log entry without consuming SMS credit.
  • Staging environments where SMS is intentionally off.

Switching to null:

PATCH /v1/instance
Content-Type: application/json
{ "sms": { "driver": null } }

The null driver still respects delivered_by_us: false on a template — the sms.queued webhook fires regardless, so operator-handled delivery still works in dev.

The reserved range +1 (555) 555-0100 through +1 (555) 555-0199 is always treated as test numbers, regardless of which driver is configured:

  • Sends to those numbers are no-op’d at the engine — no real SMS hits the driver, no carrier fees.
  • test_mode: enabled — the magic code 424242 is always accepted as the verification answer on Challenges issued for those numbers.
  • test_mode: disabled — sends still no-op, but the magic code isn’t accepted (Challenges run normally; users won’t receive real codes, so verification fails).
  • test_mode: rejected — sign-ups using a number from this range are refused outright (production hardening).

The sms.queued audit-log entry still fires for the test range so end-to-end tests can assert on the engine path.

The FAPI bootstrap (GET /v1/environment) exposes only driver + from_number from the sms block — every credential field is stripped. The SDK uses these to render UI affordances (e.g. show the phone-MFA toggle only when a driver is configured); credentials never leak to a browser.

The SMS template revert endpoint and delivered_by_us: false flow give operators ways to recover from driver issues without flipping the env-wide driver:

  • Driver outage (Twilio / Vonage degraded): flip multi_factor.phone_code.enabled: false in InstanceSettings.multi_factor to stop new MFA enrollments while the carrier is down. Existing reservations survive.
  • Carrier rejected the body (regulatory / regional): PATCH the verification_code template body to a carrier-compliant variant.
  • Per-region rate-limiting: switch to delivered_by_us: false for the affected slug and route through your own aggregator via the sms.queued webhook.
MethodPathDescription
GET/v1/instanceRead InstanceSettings.sms (credentials redacted).
PATCH/v1/instanceUpdate sms.driver, sms.from_number, sms.twilio.*, sms.vonage.*. Credentials are write-only.
GET/v1/environment (FAPI)Read the public bootstrap — only driver + from_number, no credentials.