Navigace
ELOWEN · Dokumentace

Poslední aktualizace: 20. října 2026

Procházet dokumentaci · twilio implementation

Tento článek zatím není přeložen. Je dostupný v angličtině.

Developer reference

twilio implementation

Problem audiences

The API producer attaches the core InstanceConditionError cause for rejected credentials, quota, known caller/destination restrictions and remote service unavailability. Call start, stop and state logs choose local-only from that typed cause, including a lost create response after reconciliation. Unknown API rejections, response-schema defects and SID invariants remain reportable. Media input validation and socket backpressure stay local; engine and codec failures remain reportable. Diagnostic text and reconciliation thresholds are unchanged. The typed cause requires the declared core floor, 0.30.7, which provides InstanceConditionError through the public elowen-plugin-shared/errors export.

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. The Lookup host follows the configured region, as the REST host does. 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

Seam Minimal use and effect Absent behavior and limits
ctx.config Read 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 mode media/:callId/:token, mandatory pre-upgrade authorize. A refusal yields HTTP 401 before upgrade. No Elowen identity or ticket. Tokens are consumed on authorization.
registerHttpRoute Exact 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().reportCost One 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.
registerService Starts admission and ends/cancels live calls on stop. Calls refuse while the service is stopped. In-memory state cannot resume across process exit.

Twilio declares shared API 14 and core 0.30.7. Its call-status callback uses the public registerHttpRoute seam, which still prefix-matches under /hooks/twilio/. Core passes a suffix in PluginHttpRequest.path, so the callback retains its own empty-path check to return 404 before token lookup or form parsing. The authenticated API complete-pattern contract does not apply to this public hook.

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. 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 120-second attach timer in src/voice/live/session.ts bounds this admission; the plugin adds no independent deadline or persistent recovery state. twilio.call_start_failed is reported at once for a proven 4xx rejection or a conflicting create SID. An ambiguous create failure is logged locally only, and reported once when the engine result arrives without any CallSid ever being bound.

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

The tests in tests/ use fake host handles, fake sockets and mocked fetch (tests/fixture.mjs). They cover refusals without a dial request, the order of admission and dialling, cancellation of ringing calls, SID reconciliation after a lost or late create response, Twilio 4xx rejections, machine detection, final-state polling and its exhaustion, once-only PSTN reporting, token and signature checks, stop behaviour and invalid stored settings. tests/manifest.test.mjs checks the manifest. 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 repository-level tests/twilioIntegration.test.ts uses the candidate's real call engine and scripted realtime provider. Its engine dependencies include the candidate's fakeVoiceRoute and a limit recorder, matching the account-bound CallEngineDeps contract. The route resolves before media attachment; an absent route is a fixture error, not a telephone or audio failure. Integer playback marks, authenticated SID adoption and once-only PSTN charges remain asserted through the real plugin and engine.

The published Twilio HMAC fixture verifies the signing algorithm. The signed WebSocket URL and its signature have not been observed on a live call. Twilio's security documentation notes a possible trailing slash on the signed WSS URL; the plugin signs exactly its configured Stream URL. Pickup latency, audio quality, barge-in, voicemail greeting length and final resource and AMD timing have not been measured on a live call. The code does not check Lookup prices or destination rates against Twilio's price list; the settings help asks the operator to verify them.

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)

Safe HTTP errors

TwilioApiError(status, code) retains the HTTP status and a safe standard Error message. The optional numeric provider code is included in the message but is not exposed as a separate code property. Without a code, the message contains only the HTTP status; without an HTTP rejection, this error is not produced. Twilio's safeError path consumes the message, and call creation uses status to distinguish a proven HTTP refusal from an ambiguous transport outcome. Provider message bodies are not included. This error contract adds no request or retry.