NAVIGATION
ELOWEN DOCUMENTATION

Last updated: 10 October 2026

Browse documentation · twilio implementation
Developer reference

twilio implementation

Control contract

Consumers declare consumesControls and requiresControls containing phoneCalls, plus capabilities.reads: ['controls']. Resolve the control at call time through ctx.control('phoneCalls'). A missing provider is unavailable, never an empty or successful result.

const phone = ctx.control('phoneCalls');
const normalized = await phone.normalize(rawNumber);
// { e164: '+420777123456' } or { invalid: true }; Lookup outage throws.
const result = await phone.call({
  to: normalized.e164,
  accountUserId: serviceAccountId,
  language: 'cs',
  brief: trustedBrief,
});

normalize(raw) uses basic Twilio Lookup v2 with CountryCode=CZ, no paid Fields option and API-key authentication. Only a successful response with valid: false means invalid. Non-success HTTP responses, invalid response schemas and network failures throw, so consumers must retry later rather than tell someone their number is invalid. No phone parsing dependency is included.

call({ to, accountUserId, language, brief }) accepts an E.164 number, a paying human account, BCP 47 language and trusted instructions. It admits through ctx.host.voice().thirdPartyCall before dialing.

A refusal returns exactly { dial: 'refused', refused }, with one of not_granted, disabled, spend_limit, call_active, provider_unavailable or daemon_restart. No dial is sent.

An admitted operation returns:

{
  dial: 'answered' | 'no_answer' | 'busy' | 'voicemail' | 'failed';
  callId: string;
  seconds: number;
  transcript: VoiceTranscriptEntry[];
  endReason: VoiceCallEndReason;
  error?: string;
}

seconds is core's Live billed duration, not Twilio's PSTN duration. The transcript and end reason pass through unchanged, including daemon_restart. Consumers own interpretation; no outcome schema or second model completion is accepted. A proven rejected create, a missing CallSid at engine completion or an unavailable final resource returns dial: 'failed' with a safe error. An ambiguous create response is reconciled through authenticated callbacks or media before classification and accounting. Programming errors, malformed settings and an unavailable host seam reject the operation.

Seams used

SeamMinimal use and effectAbsent behavior and limits
ctx.configRead the host's loaded-generation snapshot and validate SID, E.164 and finite price before each operation.Invalid settings reject before admission/network. Saved changes require the host's next plugin load; no live configuration callbacks.
registerControl('phoneCalls', ...)Exposes normalize and call to declared consumers.Without the plugin, the control is unavailable. No core provider registry.
host.voice().thirdPartyCall(input)Admit, then attach PCM16LE mono 24 kHz media after pickup.Refusal prevents dialing. Daemon only, one call per account, explicit account grant. Core owns voice costs, bounded transcripts and end reasons; consumers own interpretation.
registerWebSocketRoute public modemedia/:callId/:token, mandatory pre-upgrade authorize.A refusal yields HTTP 401 before upgrade. No Elowen identity or ticket. Tokens are consumed on authorization.
registerHttpRouteExact call-status hook parses signed form callbacks.Unknown call yields 204; bad token/signature yields 401. A subpath yields 404, a non-POST 405, and a body failing the callback schema or an unexpected SID 400 (index.mjs call-status handler).
host.usage().reportCostOne PSTN receipt keyed by Twilio CallSid and paying account.No reported cost adds no receipt. Core dedupes and updates existing provider/day and origin rollups.
registerServiceStarts admission and ends/cancels live calls on stop.Calls refuse while the service is stopped. In-memory state cannot resume across process exit.

The WebSocket uses a 32-byte random, constant-time-compared path token bound to one active call. Successful authorization consumes its single stream slot before upgrade. Callbacks use the same secret in the query so hook path logs do not contain it. If configured, Auth Token adds HMAC-SHA1 verification over the URL reconstructed from ctx.publicWebUrl() and sorted form values, never Host or forwarded headers. The signed text lists each form name and each distinct value once (lib/signature.mjs); repeated identical values collapse, as the api.test.mjs duplicate-value fixture asserts. Logs use fixed safe messages, not raw frames, provider bodies, secret values or concrete media paths.

Media and ending

REST create uses inline TwiML Connect/Stream, a fixed 30-second ring timeout, asynchronous answering-machine detection and status callbacks for initiated, ringing, answered and completed. Voice starts speaking immediately on pickup. A machine or fax callback stops the line; a short voicemail can contain part of the greeting.

G.711 mu-law at 8 kHz is converted to PCM16LE at 24 kHz with a stateful 63-tap windowed-sinc FIR low-pass near 3.4 kHz. History and decimation phase survive chunks. Both directions have fixed filter latency. No Twilio clear message is sent for barge-in.

The handler installs listeners synchronously and catches every message and attach failure. During upstream attach it decodes and buffers up to three seconds, at most 150 frames, dropping oldest frames, then replays in order. Each inbound frame is bounded to one second of wire audio. Invalid JSON, encoding, stream identity, base64, unexpected mark or socket backpressure ends the engine and closes the socket with a safe log. Model PCM generates media plus marks named by cumulative original PCM bytes. Each pending mark maps its unique byte position to cumulative whole milliseconds rounded down; only echoes of marks actually sent can advance handle.played. Distinct chunks ending in the same millisecond retain distinct marks, and the core receives only integer positions.

The Stream URL in the create-call TwiML uses xmlEscape from elowen-plugin-shared/xml at the XML attribute boundary. It replaces XML 1.0-forbidden C0 characters, escapes markup and both quote kinds, and preserves Unicode, tab, LF, CR and DEL. It acts only at the XML rendering boundary and does not change stored values or network behavior. The Stream URL attribute is the real consumer; without call creation no TwiML is rendered.

The first schema-valid CallSid from the create response, an authenticated callback or an authorized stream start binds the admitted call. Later ingress must match that SID; a conflicting create response fails explicitly while the original bound line is canceled, reconciled and accounted for.

A received create HTTP 4xx proves rejection only when authenticated ingress has not already identified a placed call. Transport loss, HTTP 5xx and unreadable successful responses are ambiguous. They retain the admitted handle, allowing callbacks or stream start to identify and complete the placed call. Without media, the core's existing 120-second attach deadline (src/voice/live/session.ts:431) bounds this admission; the plugin adds no independent deadline or persistent recovery state.

Engine results are watched independently of media. Engine ending before pickup requests cancellation even when the SID is still unknown. A SID learned later while the operation remains in memory triggers that cancellation before final reconciliation; media is not attached after cancellation was requested. Queued/ringing calls use canceled, active calls use completed. Concurrent stops share one request. A failed stop is ignored only after a resource read proves the call final; other failures are logged.

After the engine result, the final Twilio resource is polled up to ten times one second apart. HTTP/network/schema errors fail explicitly. Machine/fax detection has precedence, then an attached stream means answered, then busy or no-answer, otherwise failed. Callback order prompts ending but does not replace the final resource. Finished calls leave the in-memory map and late callbacks get 204.

Core's daemon_restart reason and interrupted outcome are preserved. Consumers must give interruption priority over any dial classification.

Verification and open assumptions

Focused plugin tests use fake host handles, sockets and mocked fetch. Integration regressions load the actual manifest through the candidate core loader/registry and run the actual call engine with a scripted voice provider. They cover configuration snapshots/reload, non-millisecond PCM marks, authenticated SID arrival before/after lost create responses, the core attach deadline, late cancellation, mismatches and once-only PSTN reporting. No real credentials or external calls are used. Protocol fixture shapes follow Twilio Media Streams documentation, not a recording from a live phone call. The published Twilio HMAC fixture verifies the signing algorithm. The actual WebSocket signed URL and signature have not been observed live. Twilio's security documentation notes a possible trailing slash on the signed WSS URL; the plugin uses exactly its configured Stream URL. Verify that representation in the pilot before enabling Auth Token. Lookup's current basic price and the example destination rates require owner verification before release; a paid call was not used to verify them.

A real pilot must verify pickup latency, audio quality, barge-in, voicemail greeting length and Twilio final resource/AMD timing. The host-rendered settings form still needs mobile/desktop browser validation for save/reload, errors, secret masking and help in cs/sk/en. No production plugin was installed for these tests.

Authority and cost reporting

The plugin is user-grantable and declares only voice, usage and network capabilities. The daemon's third-party seam requires the paying account's explicit Twilio grant and voice permission, enabled voice and available budget. Third-party callers receive no owner history, tools or backend access. Consumers own any meeting, phone-source or scheduling policy.

The plugin reports { accountUserId, eventId: CallSid, item: 'pstn-call', costMicrousd } through core. Core stamps provider twilio and origin platform:twilio, writes its existing voice receipt, usage_by_provider_day and usage_by_origin, and includes this in the account's monthly limit. No parallel store or accounting query exists. Live voice is metered separately by core. A consumer's conversation turn has its own normal account usage.

Minute rounding

For positive final Twilio duration:

Math.round(Math.ceil(duration / 60) * pricePerMinuteUsd * 1e6)