Documentation menu

Providers

Twilio

Uses the Ahena CLI (beta). Install it with npm install -g @ahena/cli, or prefix commands with npx @ahena/cli. See Installation. The dashboard covers projects, connections, Doctor, the Stack Graph, plans and approvals without it.

Overview

Ahena checks that your Twilio account can actually text your users (not a trial, not suspended, not test credentials), creates and configures the messaging service your app sends through, adds numbers you already own to its sender pool, points its inbound and delivery-status webhooks at your route, and reports A2P 10DLC registration for US long codes. It generates a small provider-neutral ahena.sms adapter on the official twilio SDK. Your app sends straight to Twilio with its own API key. Ahena is never in the sending path, never sends a message, and never reads message bodies or message logs.

Package: @ahena/provider-twilio · Category: sms Capabilities: account, messaging-services, sender-pool, phone-numbers, webhooks, a2p-10dlc

Maturity: verified against a fake of the API; not yet verified against the real service.

Connection

Field Secret Used for
TWILIO_API_KEY_SID yes The API key SID (SK…), HTTP Basic user name.
TWILIO_API_KEY_SECRET yes The API key secret (shown once at creation), HTTP Basic password.
TWILIO_ACCOUNT_SID no The Account SID (AC…) the key belongs to; needed in every api.twilio.com path.
TWILIO_API_KEY_SID=… TWILIO_API_KEY_SECRET=… ahena connect twilio -e production --set TWILIO_ACCOUNT_SID=AC…

Ahena calls api.twilio.com (2010-04-01 API: the account and its phone numbers), messaging.twilio.com (v1: messaging services, sender pools, A2P campaigns and brands) and iam.twilio.com (v1: the key's own policy). Nothing else is reachable with the key.

An Account SID with its Auth Token in the two key fields also works (it's the account's master credential, so Doctor warns). Test credentials (test Account SID + test Auth Token) are refused: Twilio answers error 20008 to every read, so Ahena could check nothing, and they never deliver.

Permissions

Use a Standard or Restricted API key made for Ahena (Twilio Console → Account → API keys & tokens). Ahena reports permissionCheck: checked, and here is exactly how:

  • Reads are probed with one-item GETs: the services list, the account's phone numbers, the Account resource, and (when a service exists) one service's phone numbers and A2P campaigns. Answered = granted, refused = missing.
  • Writes are read from the key's own policy (GET iam.twilio.com/v1/Keys/{sid}). A Main key's policy is null (full access); a Restricted key's policy is its allow-list. Standard keys can't read Keys (or the Account resource), so for them the write permissions are reported …:unknown. Ahena never attempts a write to find out.
Permission (Twilio's name) Kind Why
twilio/messaging/services/list and twilio/messaging/services/read read Find the messaging service. Required.
twilio/messaging/services.phonenumbers/list read Sender pool.
twilio/messaging/services.usa2p-campaign/list read A2P 10DLC campaign status.
twilio/phone-numbers/active-numbers/list read Numbers the account owns (for adding to the pool).
/twilio/iam/accounts/read read, optional Trial vs upgraded, active vs suspended. Without it Doctor says "Not verified".
/twilio/iam/api-keys/read read, optional Lets Ahena read the key's own policy, so write permissions are checked instead of unknown.
twilio/messaging/services/create write Create the messaging service.
twilio/messaging/services/update write Set its webhooks.
twilio/messaging/services.phonenumbers/create write Add an owned number to the pool.

Optional reads Ahena uses when allowed: twilio/messaging/services.shortcodes/list, twilio/messaging/services.alphasenders/list. Never grant twilio/messaging/messages/*, twilio/phone-numbers/active-numbers/create (buying) or …/delete: Ahena doesn't use them.

Ahena drops the account's auth_token from Account responses, hides user names, passwords and query values in webhook URLs it shows (https://***@host/path?key=…), and drops A2P message samples and descriptions.

Capabilities

Capability Access Verification Changes Refuses
account read-only Upgrading a trial or changing billing; subaccounts
messaging-services writable read-back CONFIRMATION_REQUIRED Deleting or renaming services
sender-pool writable read-back CONFIRMATION_REQUIRED Removing senders; moving a number from another service; adding short codes or sender IDs
phone-numbers manual Buying (billable) or releasing numbers; changing a number's own webhooks
webhooks writable read-back CONFIRMATION_REQUIRED URLs that aren't public https or embed credentials; clearing a webhook
a2p-10dlc manual Registering brands or campaigns (billable; needs business details)

ahena lock records the account's status and type, the service's webhook settings (redacted) and its sender pool, so ahena drift sees a webhook or sender changed in the Console.

Configure

sms: {
  provider: "twilio",
  messagingService: "Leo notifications",        // exact friendly name (created if absent) or "MG…"
  from: { production: "+15551234567" },         // number(s) the account already owns
  webhook: { production: { endpoint: "https://leo.example.com/api/webhooks/twilio" } },
  usecase: "notifications",                     // optional; used only when creating the service
},

webhook.endpoint becomes both the service's inbound request URL (POST) and its status callback; add statusCallback: "https://…" to send delivery statuses elsewhere. from may be a string or a list, per environment.

Change Classification Notes
Create the messaging service (POST /v1/Services) CONFIRMATION_REQUIRED Free. Created with the declared webhooks, InboundMethod=POST and UseInboundWebhookOnNumber=false. Matched by exact name, so never duplicated; two services with the same name are refused as ambiguous.
Set the inbound request URL and status callback (POST /v1/Services/{sid}) CONFIRMATION_REQUIRED Also in production. Only differing fields are sent. If the service defers to each number's own webhook, Ahena turns that off (said in the plan).
Add a number to the sender pool (POST /v1/Services/{sid}/PhoneNumbers) CONFIRMATION_REQUIRED Only a number the account already owns, that can send SMS, and that isn't in another service. No purchase.
Buy a phone number MANUAL (refused) Billable every month. Ahena tells you to buy it in the Console, then adds it.
A2P 10DLC brand and campaign MANUAL Billable and needs business details; Doctor gives the steps.

Refused outright: releasing numbers, deleting services, removing senders, moving a number out of another service (error 21712 territory), sending messages, webhook URLs that aren't public https or that carry credentials.

Generate

ahena generate twilio [--framework nextjs]:

  • src/ahena/sms/index.ts: the provider-neutral interface every SMS provider generates, implemented with client.messages.create:

    /** One text message. */
    export interface SendSms {
      /** Recipient in E.164 format, e.g. "+15551234567". */
      to: string;
      /** The message text. Long texts are split into segments by the provider (and billed per segment). */
      body: string;
      /** Sender: an E.164 number or an alphanumeric sender ID. Defaults to the configured sender. */
      from?: string;
    }
    
    /** What sms.send resolves to: the provider's id for the accepted message. */
    export interface SentSms {
      id: string;
    }
    
    export interface SmsClient<Raw> {
      /** Sends one text message. Throws with the provider's message if it's rejected. */
      send(message: SendSms): Promise<SentSms>;
      /** The provider's own SDK client, for anything not wrapped here. */
      readonly raw: Raw;
    }
    
    export const sms: SmsClient<TwilioClient>;

    send uses TWILIO_MESSAGING_SERVICE_SID (recommended) or SMS_FROM, and resolves to the message SID.

  • src/ahena/providers/twilio.ts: the SDK client from TWILIO_ACCOUNT_SID, TWILIO_API_KEY_SID, TWILIO_API_KEY_SECRET (the app's own key).

  • .env.example.twilio.

  • src/app/api/webhooks/twilio/route.ts (Next.js): verifies X-Twilio-Signature with the SDK's twilio.validateRequest(authToken, signature, url, params). Answers 403 when the signature is missing or wrong, 500 when TWILIO_AUTH_TOKEN isn't set, 204 for status callbacks and empty TwiML (200) for inbound messages. Set TWILIO_WEBHOOK_BASE_URL when a proxy changes the URL your app sees, because Twilio signs the public URL.

The webhook secret is the Auth Token. Twilio signs webhooks with the account's Auth Token (HMAC-SHA1 of the URL and POST parameters), not with an API key secret. Ahena doesn't hold the Auth Token and never reads it. If you declare a webhook, Doctor checks that TWILIO_AUTH_TOKEN is stored in the environment (by name only); store it yourself with ahena env set <environment> TWILIO_AUTH_TOKEN (copy it from Console → Account → API keys & tokens).

Limitations

  • Ahena doesn't buy, release or configure phone numbers themselves, register A2P 10DLC brands or campaigns, verify toll-free numbers, or manage short codes and alphanumeric sender IDs (it lists them).
  • Toll-free verification status isn't read; Doctor reports US toll-free senders as "Not verified".
  • With a Standard key, the account's type and status and the key's write permissions can't be read (Twilio doesn't allow Standard keys on Accounts or Keys); Doctor says "Not verified".
  • A2P brand status (/v1/a2p/BrandRegistrations) isn't in Twilio's Restricted-key permission tables; with a Restricted key it may be unreadable, and Doctor falls back to the campaign status.
  • Only Twilio's default (US1) API hosts; regional edges aren't supported.
  • A sender number's own SMS webhook (sms_url) is shown but never changed. The service's inbound URL wins while the service doesn't defer to numbers.
  • Not yet run against the real Twilio API. Taken from the docs, not observed: the HTTP status that accompanies error 70051 for a Restricted key missing a permission (the fake uses 401), whether a suspended account can still read its Account resource, and the exact status for errors 21710/21712 (the fake uses 409).

Manual steps

  1. Production on a trial account: upgrade it (Console → Upgrade; Admin → Account billing).
  2. A number Ahena should add but the account doesn't own: buy or port it in the Console (Phone Numbers → Manage → Buy a number), then ahena configure twilio.
  3. US long codes: register an A2P 10DLC brand and campaign for the messaging service (Console → Messaging → Regulatory Compliance → Onboarding). Both are billed.
  4. US toll-free numbers: submit toll-free verification in the Console.
  5. Webhook route: store the Auth Token with ahena env set <environment> TWILIO_AUTH_TOKEN and in your app's environment.

Doctor checks

Id Severity Meaning
twilio.credentials PASS / FAIL The credentials work, and which kind (Main, Restricted, API key, Auth Token).
twilio.credentials.test FAIL / WARNING Test credentials (error 20008): FAIL in production, WARNING elsewhere. Nothing else is checked.
twilio.credentials.kind WARNING / INFO Connected with the Auth Token instead of an API key.
twilio.account PASS / INFO / FAIL Active; suspended or closed (FAIL, manual); unreadable with this key (INFO, "Not verified").
twilio.account.type PASS / INFO / FAIL Trial in production FAILs with manual upgrade steps; trial elsewhere is INFO.
twilio.key.permissions PASS / INFO / WARNING The key can create and update the service and add numbers; missing (WARNING, which permissions to add) or unknown (INFO).
twilio.service PASS / FAIL The declared messaging service exists (missing: CONFIRMATION_REQUIRED fix); duplicate names (MANUAL).
twilio.service.undeclared INFO No sms.messagingService; lists the services in Twilio.
twilio.service.senders PASS / FAIL The pool isn't empty and has an SMS-capable sender.
twilio.service.sender[.<digits>] PASS / FAIL Each sms.from number is in the pool; owned but missing is plannable, not owned is MANUAL (buying is billable).
twilio.webhook.inbound PASS / WARNING / FAIL The service's inbound request URL is the endpoint (POST); WARNING if it defers to each number's webhook.
twilio.webhook.status PASS / WARNING The service's status callback is the declared URL.
twilio.webhook.endpoint PASS / WARNING / FAIL One unsigned POST to your route: 401/403 is right; 2xx means it doesn't verify X-Twilio-Signature; 404/405 offers a SAFE generate fix.
twilio.a2p PASS / WARNING US long codes in the pool have a VERIFIED A2P 10DLC campaign; otherwise WARNING with MANUAL steps (in review, failed or missing).
twilio.tollfree INFO US toll-free senders: verification isn't checked.

Core also checks that TWILIO_AUTH_TOKEN is stored (by name) when a webhook is declared.

Disconnect behavior

Removes the connection and the key Ahena stored. Ahena doesn't delete API keys: delete Ahena's key in Console → Account → API keys & tokens (if your app uses the same key, it stops sending, which is why Ahena should have its own). Messaging services, sender pools, numbers, webhooks and A2P registrations stay in Twilio.

Set up step by step

  1. Create the key. Twilio Console → Account → API keys & tokens → Create API key. Name it "Ahena", choose Standard or Restricted, and copy the SID (SK…) and the secret (shown once). Copy the Account SID (AC…) from the Console home page.

  2. Minimum permissions. For a Restricted key, allow the reads and writes in the Permissions table above (add /twilio/iam/api-keys/read so Ahena can check write permissions, and /twilio/iam/accounts/read so it can tell a trial account). A Standard key works too; Doctor then reports the account type and write access as "Not verified". Give your app a different key.

  3. Connect.

    TWILIO_API_KEY_SID=… TWILIO_API_KEY_SECRET=… ahena connect twilio -e development --set TWILIO_ACCOUNT_SID=AC…
    TWILIO_API_KEY_SID=… TWILIO_API_KEY_SECRET=… ahena connect twilio -e production --set TWILIO_ACCOUNT_SID=AC…

    Pipe the values or type them at the hidden prompt; never pass them as arguments.

  4. Verify the connection. ahena inspect twilio -e production lists the account, messaging services with their senders, webhooks and A2P status, and the account's numbers (no key, no Auth Token, no message content).

  5. Run Doctor. ahena doctor -e production.

  6. Plan. Add the sms section above to ahena.config.ts, then ahena plan -e production (or ahena configure twilio -e production to plan and apply one provider).

  7. Apply. ahena apply -e production. Creating the service, setting its webhooks and adding an owned number each need confirmation. Nothing billable is ever planned.

  8. Verify. ahena plan -e production shows no Twilio changes and ahena doctor -e production passes. Store the Auth Token for the route with ahena env set production TWILIO_AUTH_TOKEN, run ahena generate twilio -e production --framework nextjs, set the variables from .env.example.twilio (the service SID is in ahena inspect twilio -e production) and deploy.

  9. Troubleshooting.

    Symptom Cause Fix
    "Twilio rejected the API key" Wrong or deleted key, secret mistyped, or the account is suspended Create a new key and reconnect.
    "The API key belongs to a different account" TWILIO_ACCOUNT_SID is another account or subaccount Reconnect with the key's own account SID.
    "These are Twilio test credentials" Test Account SID / test Auth Token Create a live API key and reconnect.
    "This API key can't list messaging services" Restricted key without twilio/messaging/services/list Add it (and services/read) to the key.
    Apply fails naming twilio/messaging/services/create Restricted key without write permissions Add the write permissions from the table.
    "isn't a phone number this Twilio account owns" sms.from not bought or ported Buy it in the Console (billable), then plan again.
    "is in messaging service …" A number can be in one service only Remove it from the other service's Sender Pool in the Console.
    twilio.account.type FAIL Trial account in production Upgrade the account.
    twilio.a2p WARNING US long codes without a verified A2P 10DLC campaign Register a brand and campaign (billed), wait for review.
    Route returns 403 to real Twilio requests TWILIO_AUTH_TOKEN wrong, or the app sees a different URL than Twilio signed Use the account's Auth Token; set TWILIO_WEBHOOK_BASE_URL to your public origin.
  10. Disconnect and revoke. ahena disconnect twilio -e production, then delete Ahena's API key in Console → Account → API keys & tokens.