Documentation

Twilio integration

Connect a Twilio account, store credentials, and let AppEngine manage webhook URLs end to end.

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.

POST/upstream/save-integrationJWT
{
  "type": "TwilioProvider",
  "name": "Acme Twilio",
  "config": {
    "accountSid":   "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "authToken":    "<auth token>",
    "appSid":       "APxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "apiKey":       "SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "apiKeySecret": "<secret>"
  }
}

Field roles:

FieldWhat it's for
accountSid / authTokenREST API access — buy numbers, configure webhooks, send SMS
appSidTwiML Application SID. Outbound dial requests use it as the source. Set the App's voice URL to /connect/webhook/twilio/voice
apiKey / apiKeySecretUsed to mint short-lived voice access tokens for browser softphones (so the master authToken never reaches a frontend)

Test the connection:

POST/upstream/test/{integration}/{operation}JWT
curl -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:

DirectionMethodURL
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 callbackPOST/connect/webhook/twilio/status?orgid={orgId}

The dispatch goes through connect.controller.ts:

POST/connect/webhook/{vendor}/{serviceId?}No auth

This 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:

PathUsed 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-continueResume an automation flow after <Redirect>
/connect/webhook/twilio/ivr-inputDTMF / speech result for IVRCollectInputAction
/connect/webhook/twilio/ivr-menu-inputMenu digit selection for IVRMenuAction
/connect/webhook/twilio/ivr-recordingRecording finished for IVRRecordAction
/connect/webhook/twilio/ivr-payment-statusTwilio Pay completion for IVRTwilioPayAction
/connect/webhook/twilio/ivr-queue-musicHold music for queued calls
/connect/webhook/twilio/ivr-ai-outboundOutbound AI call TwiML (used by make_call tool)
/connect/webhook/twilio/ivr-ai-transferTwiML for AI-initiated mid-call transfer
/connect/webhook/twilio/ai-voiceAI 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:

POST/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:

GET/phone/verifyJWT

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

Don't store credentials elsewhere

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.