Voice, SMS, IVR, and softphone presence all go through Twilio. The connection is configured once per org through the Upstream module; from then on, every phone, message, and call is brokered through that single account.
Connecting an account
Twilio is one of the vendors registered in UpstreamRegistry. Connect through /upstream/save-integration with the credentials Twilio provides.
/upstream/save-integrationJWT{
"type": "TwilioProvider",
"name": "Acme Twilio",
"config": {
"accountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"authToken": "<auth token>",
"appSid": "APxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"apiKey": "SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"apiKeySecret": "<secret>"
}
}
Field roles:
| Field | What it's for |
|---|---|
accountSid / authToken | REST API access — buy numbers, configure webhooks, send SMS |
appSid | TwiML Application SID. Outbound dial requests use it as the source. Set the App's voice URL to /connect/webhook/twilio/voice |
apiKey / apiKeySecret | Used to mint short-lived voice access tokens for browser softphones (so the master authToken never reaches a frontend) |
Test the connection:
/upstream/test/{integration}/{operation}JWTcurl -X POST "https://appengine.appmint.io/upstream/test/TwilioProvider/listPhoneNumbers" \
-H "orgid: my-org" -H "Authorization: Bearer <jwt>"
If the call returns numbers (or an empty array), the credentials are good.
Credential storage
Credentials live in the upstream_integration collection, encrypted at rest. They never appear in REST responses; the upstream service holds an in-memory provider instance per org and proxies all Twilio API calls through it. The frontend should never touch authToken directly — use /phone/voice/register-device to mint a scoped, short-lived voice token instead.
Webhook URLs AppEngine sets
Whenever AppEngine purchases a number (POST /phone/numbers) or imports one (POST /phone/numbers/add), it configures three webhooks against Twilio's incoming-numbers API:
| Direction | Method | URL |
|---|---|---|
| Voice (incoming call) | POST | /connect/webhook/twilio/voice?orgid={orgId} (mode voice) |
| Voice (incoming call) | POST | /connect/webhook/twilio/ivr-menu?orgid={orgId} (mode ivr) |
| SMS (incoming message) | POST | /connect/webhook/twilio/sms?orgid={orgId} |
| Status callback | POST | /connect/webhook/twilio/status?orgid={orgId} |
The dispatch goes through connect.controller.ts:
/connect/webhook/{vendor}/{serviceId?}No authThis is the single front door for vendor webhooks. The controller routes to TwilioProvider.handleWebhook(serviceId, payload) based on the vendor segment.
Other webhook endpoints AppEngine exposes for Twilio
These are not configured per number — they're invoked by other Twilio verbs and IVR redirects:
| Path | Used by |
|---|---|
/connect/webhook/twilio/recording-status | <Record> callbacks for voicemail, transfer recordings |
/connect/webhook/twilio/transcription-status | <Record transcribe="true"> callbacks |
/connect/webhook/twilio/ivr-continue | Resume an automation flow after <Redirect> |
/connect/webhook/twilio/ivr-input | DTMF / speech result for IVRCollectInputAction |
/connect/webhook/twilio/ivr-menu-input | Menu digit selection for IVRMenuAction |
/connect/webhook/twilio/ivr-recording | Recording finished for IVRRecordAction |
/connect/webhook/twilio/ivr-payment-status | Twilio Pay completion for IVRTwilioPayAction |
/connect/webhook/twilio/ivr-queue-music | Hold music for queued calls |
/connect/webhook/twilio/ivr-ai-outbound | Outbound AI call TwiML (used by make_call tool) |
/connect/webhook/twilio/ivr-ai-transfer | TwiML for AI-initiated mid-call transfer |
/connect/webhook/twilio/ai-voice | AI assistant TwiML — opens the <Stream> to SimpleVoiceGateway |
You generally don't configure these manually. The TwiML each IVR action emits already references them with the right query parameters.
Voice access tokens
Browser softphones use voice access tokens, not the master auth token. AppEngine mints them via:
/phone/tokenJWT{ "identity": "user:[email protected]" }
Or, more typically, through /phone/voice/register-device which returns the same token plus the user's assigned phones.
The token grants a VoiceGrant with outgoingApplicationSid set to your appSid. When the device dials out, Twilio runs the voice URL on that App, which is /connect/webhook/twilio/voice — the same endpoint inbound calls use, so caller ID and routing rules apply consistently.
Multiple Twilio accounts per org
Not supported. TwilioProvider is registered as a single instance per org. If you need to segregate billing across business units, run them as separate orgs (each with its own orgid and Twilio credentials).
Drift handling
If someone edits a number's webhooks directly in the Twilio dashboard, the local phone record will go out of sync. Run:
/phone/verifyJWTIt compares Twilio's view of every number against the local record and returns a delta. Re-run POST /phone/numbers/{phoneId}/routing to push the local config back into Twilio.
The Upstream-encrypted record is the only sanctioned credential store. Don't bake authToken into env vars on app servers or front-end builds — voice tokens and the upstream proxy exist precisely so that doesn't have to happen.