# Twilio compatibility

OneML is compatible with Twilio's TwiML, and PressOne's REST surface mirrors Twilio's `2010-04-01` Calls API, so most existing TwiML documents and Twilio integrations run against PressOne unmodified. This page is the exception to the rest of this documentation: it names Twilio and TwiML directly, and exists specifically to help developers migrating an existing Twilio integration understand where PressOne's behavior differs.

## Supported verbs

Each OneML verb below links to its full reference page.

| OneML verb | Twilio verb(s) | Notes |
|------------|-----------------|-------|
| [Say](/voice/oneml/say) | `<Say>` | Voice and language defaults differ. See [behavioral divergences](#behavioral-divergences). |
| [Play](/voice/oneml/play) | `<Play>` | |
| [Gather](/voice/oneml/gather) | `<Gather>` | |
| [Record](/voice/oneml/record) | `<Record>` | |
| [Dial](/voice/oneml/dial) | `<Dial>` | Only a bare number or `<Number>` children are dialed. See [not supported](#not-supported). |
| [Redirect](/voice/oneml/redirect) | `<Redirect>` | |
| [Pause](/voice/oneml/pause) | `<Pause>` | |
| [Hangup](/voice/oneml/hangup) | `<Hangup>` | |
| [Reject](/voice/oneml/reject) | `<Reject>` | |
| [Stream](/voice/oneml/stream) | `<Connect><Stream>` | Bidirectional, blocking media session. |
| [Stream](/voice/oneml/stream) | `<Start><Stream>` / `<Stop><Stream>` | One-way, non-blocking media session. |
| [Disconnect](/voice/oneml/disconnect) | No Twilio equivalent | PressOne extension. Returns control of the call to the platform dialplan without hanging up. |

## Not supported

The elements and attributes below are not fully implemented. They fail in different ways depending on the case: some reject the document outright, others are silently skipped, and others are accepted but never take effect. Knowing which applies to a given document matters when migrating: a hard error is visible immediately, while a silent skip is not.

### Rejected with a validation error

| Verb | What happens |
|------|---------------|
| `<Pay>` | Parses as valid XML, but fails semantic validation with the error `<Pay> is not supported yet` when PressOne executes the document: the call ends as failed. This applies wherever a `<Pay>`-containing document reaches the engine: an initial webhook response, a `<Redirect>` target, or an inline `Twiml` document. For inline `Twiml` supplied on create-call specifically, this check does not run synchronously against the request: the parser only confirms the document is well-formed XML at that point, so `POST .../Calls.json` still returns `201 Created`. The `<Pay>` rejection only happens later, when the engine actually executes the call, and surfaces as `status: failed` on the call resource or on a status callback, not as a create-call error. |

### Accepted, skipped without effect

The following elements parse and validate successfully (a document containing them passes OneML's strict parser), but the executor has no handler for them. Each falls through to a generic case that logs a warning on the server and continues with the next verb. There is no error, no `action` callback, and no signal to your webhook or to the caller that anything was skipped.

| Element | Context | Notes |
|---------|---------|-------|
| `<Enqueue>` | Direct child of `<Response>` | |
| `<Leave>` | Direct child of `<Response>` | |
| `<Refer>` | Direct child of `<Response>` | Its `sendDigits` attribute is validated for format, but the verb itself never executes. |
| `<Siprec>` | Child of `<Start>` or `<Stop>` | |
| `<Transcription>` | Child of `<Start>` or `<Stop>` | |
| `<ConversationRelay>` | Child of `<Connect>` | Only `<Connect><Stream>` is implemented; other `<Connect>` targets fall through the same way. |
| `<Room>` | Child of `<Connect>` | |
| `<VirtualAgent>` | Child of `<Connect>` | |

`<Start>` and `<Stop>` still require at least one recognized child (`<Siprec>`, `<Stream>`, or `<Transcription>`) to pass validation: an empty `<Start>` or `<Stop>` is rejected before execution. Once past that check, only the `<Stream>` child actually runs.

### Dial targets dropped with a warning

`<Dial>` executes only a bare phone number or `<Number>` children. The following nested targets parse without error, but PressOne drops them with a server-side warning when it builds the list of numbers to dial. If a `<Dial>` contains only unsupported targets, PressOne skips the dial entirely and continues with the next verb, as if `<Dial>` were never present.

| Target | Notes |
|--------|-------|
| `<Conference>` | |
| `<Client>` | |
| `<Sip>` | |
| `<Queue>` | |
| `<Application>` | |

### Dial attributes parsed but ignored

The following `<Dial>` attributes are recognized and validated on the parsed document, but setting them has no effect on the placed call.

| Attribute | Notes |
|-----------|-------|
| `trim` | Recording trim behavior is not configurable through `<Dial>`. |
| `referUrl` | |
| `recordingTrack` | |

## REST API differences

PressOne's REST API mirrors Twilio's `2010-04-01` Calls resource closely enough that most existing integrations run unmodified, with the following differences.

- **`ApplicationSid` is rejected.** Twilio's create-call endpoint accepts `ApplicationSid` to run a pre-configured Twilio Application instead of `Url`/`Twiml`. PressOne has no equivalent concept: supplying `ApplicationSid` always fails the request with error `21605`. Use `Url` or `Twiml` instead.
- **`Timeout` defaults to 60 seconds.** The ring timeout on create-call defaults to 60 seconds before PressOne treats the call as unanswered, matching Twilio's default.
- **Updating a live call requires a JSON body, not form encoding.** Every other endpoint in this API accepts `application/x-www-form-urlencoded` with PascalCase field names, matching Twilio's convention. The update-call endpoint is the one exception: it requires an `application/json` body with snake_case fields (`voice_url`, `voice_method`, `twiml`). Twilio's official helper libraries send update-call requests form-encoded, so they cannot update a live call against PressOne without modification: issue a custom HTTP request with a JSON body instead of using the helper library's built-in call-update method.
- **The recordings list is not paginated.** Twilio paginates the recordings list. PressOne's equivalent endpoint returns every recording captured during the call in a single response, with no `Page`/`PageSize` parameters and no page metadata in the response body.
- **Error response bodies are not uniform across operations.** Only create-call returns a Twilio-style JSON error body (`code`, `message`, `more_info`, `status`). Every other operation (list calls, get call, update call, end call, list recordings, get recording) returns a plain-text body (`Unauthorized` or `Not Found`) instead of JSON on failure. In addition, update call, end call, and list recordings collapse every failure, including an unknown `CallSid`, to a `401` response; they do not expose a separate `404` for a call that does not exist.

## Behavioral divergences

- **`<Dial record>` dual-channel variants record a single mixed channel.** The from-answer and from-ringing start moments are honored, but `record-from-answer-dual` and `record-from-ringing-dual` do not produce separate channels per party.
- **`<Say>` uses Google Cloud Text-to-Speech, not Amazon Polly.** Twilio's own TwiML reference documents Amazon Polly as the underlying engine for its Twilio-style voice names. PressOne synthesizes every `<Say>` voice, including the `man`/`woman`/`alice` aliases, through Google Cloud Text-to-Speech.
- **`<Say voice>` is not validated against an enum.** PressOne accepts any string in `voice`, not only `man`, `woman`, and `alice`. The bare word `man` (the default) and any unrecognized bare string select a default Google male voice; `woman` and `alice`, matched exactly in lowercase, select a default Google female voice. See [Voice selection](/voice/oneml/say#voice-selection) for the full set of rules.
- **`<Say voice>` supports a `Provider.<name>` namespace with no Twilio equivalent.** Setting `voice` to `Google.<voice-name>` or `Elevenlabs.<voice-id>` selects a specific Google Cloud Text-to-Speech or ElevenLabs voice by name or ID. Twilio's TwiML has no equivalent mechanism for choosing a non-default voice by provider. See [Voice selection](/voice/oneml/say#voice-selection).
- **`<Say language>` is accepted but not applied.** Speech synthesis always uses the default voice's language, regardless of the value you set.
- **`<Number sendDigits>` is accepted but not applied.** PressOne validates the attribute's format but does not send the specified DTMF tones to the dialed party after the call connects.
- **`<Connect>`'s `action` and `method` are accepted but never requested.** When a `<Connect><Stream>` session ends, PressOne continues with the next verb in the document; it does not request the `action` URL.
- **`<Start><Stream>` metadata omits `account_sid`.** The metadata PressOne sends to your WebSocket endpoint when a stream starts includes `account_sid` for `<Connect><Stream>` but not for `<Start><Stream>`. See [Stream](/voice/oneml/stream) for the full metadata comparison.
- **Status callbacks fire only for calls created through the REST API.** Inbound calls never emit status callbacks, regardless of any `StatusCallback` configured elsewhere. See [status callbacks](/voice/status-callbacks).
- **`StatusCallbackEvent` does not validate its values server-side.** Any string is accepted, not only `initiated`, `ringing`, `answered`, and `completed`. Values outside that set are accepted at request time but never match an event PressOne emits, so subscribing to them has no effect.
- **The signature header and webhook user agent keep their Twilio names.** PressOne signs webhook requests with a header literally named `X-Twilio-Signature`, and identifies itself with a `User-Agent` of `TwilioProxy/1.1`. Both are retained as-is so that existing Twilio SDKs and signature-verification libraries work without modification. See [verify webhook signatures](/voice/webhooks).

## Next steps

- [OneML overview](/voice/oneml/overview): every OneML verb and attribute.
- [Webhooks](/voice/webhooks): request format, standard parameters, and signature verification.
- [Status callbacks](/voice/status-callbacks): call lifecycle events and delivery.
- [API reference](/api/voice): complete REST endpoint documentation.
