voicetel / voiceml
Official PHP SDK for the VoiceML REST API (Twilio-compatible voice + AMD service from VoiceTel)
Requires
- php: ^8.1
- ext-json: *
- guzzlehttp/guzzle: ^7.8
Requires (Dev)
- phpunit/phpunit: ^10.5
README
The official PHP client for the VoiceML REST API β Twilio-compatible outbound voice and answering-machine-detection from VoiceTel, with modern PHP 8.1+ ergonomics and battle-tested Guzzle transport.
π Table of Contents
- Features
- Installation
- Quickstart
- Authentication
- Resource Reference
- Error Handling
- Pagination
- Migration from twilio-php
- Rate Limits
- Development
- API Documentation
- Contributors
- Sponsors
- License
β¨ Features
π‘οΈ Modern PHP, Strictly Typed
- PHP 8.1+ end-to-end β
readonlyproperties, named arguments, constructor property promotion,declare(strict_types=1)everywhere. - Typed request and response models for every one of the 81 API operations across 9 resource families β
Call->sid,Recording->duration,Queue->currentSizeall hint cleanly in your IDE. - Twilio-compatible wire shapes β
AccountSid,From,To, status callbacks, pagination envelopes β match what Twilio's Programmable Voice API documents. - PSR-4 autoloading under
VoiceML\β plays nicely with Symfony, Laravel, and every other Composer-aware framework.
π Production-Grade Transport
- Built on Guzzle 7 (
guzzlehttp/guzzle: ^7.8) β the de-facto HTTP client in the PHP ecosystem. - Automatic retry with exponential backoff on 429 / 5xx β honors
Retry-Afterheaders. - Configurable timeout per client (defaults to 30s) and configurable
maxRetries(defaults to 2). - HTTP Basic auth with
AccountSid:ApiKeyβ exactly what the Twilio SDK uses, so existing credentials work unchanged. - Structured exception hierarchy β
RateLimitException,AuthenticationException,NotFoundException, etc. all subclasses ofApiExceptionyou can catch broadly or narrowly.
π Complete API Coverage
- Calls β originate, fetch, terminate, update + per-call recordings, streams, siprec, transcriptions, notifications, events, and the
/Calls/{sid}/Paymentslifecycle (Pay TwiML companion). - Conferences β list, fetch, end conferences, plus participants (mute / hold / kick) and conference-scoped recordings.
- Queues β create, list, update, delete, peek, dequeue (front or specific member).
- Applications β CRUD on stored TwiML + callback bundles.
- Recordings β account-wide list, metadata fetch, audio fetch (follows S3 redirect), delete.
- Messages β create, fetch, list (To/From/DateSent filters + pagination), update (Body redaction; Status=canceled), delete.
- IncomingPhoneNumbers β list, fetch, update.
- Notifications β fetch, list.
- SIP β SIP Trunking: Domains (CRUD), CredentialLists + Credentials (CRUD), IpAccessControlLists + IpAddresses (CRUD), DomainβACL/CredentialList mappings (historical, Auth/Calls, Auth/Registrations namespaces).
- Routes V2 β Twilio Inbound Processing Region API:
$client->routesV2->sipDomains->fetch($name)/update($name, voiceRegion: 'us1', friendlyName: 'ingress'). - Diagnostics β
/healthdeep probe, OpenAPI spec.
π§ͺ Tested
- 110 unit tests with mocked Guzzle handlers β every method and every error path exercised, no network in CI.
- Conformance test suite that validates wire shapes against the published OpenAPI document β spec drift gets caught at parse time.
π¦ Clean Distribution
- Zero codegen footprint β every byte hand-written.
- Single Composer package, single namespace, no surprise transitive deps beyond Guzzle.
π Installation
composer require voicetel/voiceml
Requires PHP 8.1 or later and the json extension (bundled with PHP).
π Quickstart
<?php require __DIR__ . '/vendor/autoload.php'; use VoiceML\Client; use VoiceML\Model\CreateCallRequest; $client = new Client( accountSid: getenv('VOICEML_ACCOUNT_SID'), apiKey: getenv('VOICEML_API_KEY'), ); $call = $client->calls->create(new CreateCallRequest( to: '+18005551234', from: '+18005550000', url: 'https://example.com/twiml', machineDetection: 'DetectMessageEnd', )); echo $call->sid, ' ', $call->statusRaw, PHP_EOL; foreach ($client->queues->list()->queues as $q) { echo $q->friendlyName, ' ', $q->currentSize, PHP_EOL; }
π Authentication
Every endpoint uses HTTP Basic with your AccountSid as the username and your per-tenant API key as the password β identical to Twilio's auth shape, so credentials issued for Twilio code work here unchanged.
<?php use VoiceML\Client; $client = new Client( accountSid: 'ACβ¦', apiKey: 'β¦', ); $health = $client->diagnostics->health(); // uses your AccountSid + key on every call
For ports from the Twilio PHP SDK, the credential may be passed as authToken: instead of apiKey: β they are aliases:
$client = new VoiceML\Client(accountSid: $sid, authToken: $token);
Passing both raises ConfigurationException.
Don't have credentials yet? See voicetel.com/docs/api/v0.9/voiceml/ for issuance and rotation.
π Per-product hosts
VoiceML mirrors Twilio's product-per-subdomain model. Most resources answer on the default host (voiceml.voicetel.com), but two products ride their own subdomains, derived automatically from your baseUrl:
- Conversations (
$client->conversationsV1) βconversations.voicetel.com - Messaging Service (
$client->messagingV1) βmessaging.voicetel.com
Derivation swaps the voiceml label for a recognised *.voicetel.com host (e.g. east-1.us.voiceml.voicetel.com β east-1.us.messaging.voicetel.com); any other base URL (a self-hosted instance) keeps every product on that single host. Self-hosters can point a product at a custom host explicitly:
$client = new VoiceML\Client( accountSid: 'ACβ¦', apiKey: 'β¦', baseUrl: 'https://pbx.example.com', messagingBaseUrl: 'https://msg.example.com', conversationsBaseUrl: 'https://conv.example.com', );
πΊοΈ Resource Reference
| Resource | Methods | Covers |
|---|---|---|
$client->calls |
originate, fetch, list, terminate, update | + per-call recordings, streams, siprec, transcriptions, notifications, events, payments |
$client->conferences |
list, fetch, end | participants (mute / hold / kick), conference-scoped recordings |
$client->queues |
create, list, update, delete | peek, dequeue (front or specific member) |
$client->applications |
CRUD on TwiML + callback bundles | |
$client->recordings |
account-wide list, metadata, audio fetch, delete | follows S3 redirect for audio |
$client->messages |
create, fetch, list, update, delete | To/From/DateSent filters; Body redaction; Status=canceled |
$client->incomingPhoneNumbers |
list, fetch, update | |
$client->notifications |
fetch, list | |
$client->conversationsV1 |
conversations, messages, participants, webhooks, roles, users, credentials, configuration, services | rides conversations.voicetel.com |
$client->messagingV1->services |
create, list, fetch, update, delete | Messaging Service (MGβ¦); rides messaging.voicetel.com |
$client->pricing |
v1/v2 voice, messaging, phoneNumbers, trunking β countries + numbers |
read-only pricing lookups |
$client->diagnostics |
/health, OpenAPI spec |
Every method that takes a request body accepts a typed model imported from VoiceML\Model:
<?php use VoiceML\Client; use VoiceML\Model\CreateCallRequest; use VoiceML\Model\StartPaymentRequest; $client = new Client(accountSid: 'ACβ¦', apiKey: 'β¦'); $call = $client->calls->create(new CreateCallRequest( to: '+18005551234', from: '+18005550000', url: 'https://example.com/twiml', )); // On a live call, open a Pay session: $session = $client->calls->startPayment($call->sid, new StartPaymentRequest( idempotencyKey: 'order-482917', statusCallback: 'https://example.com/pay-status', )); echo $session->sid, ' ', $session->status, PHP_EOL;
Recording audio
The audio fetch follows VoiceML's single 302βS3 redirect transparently:
$audio = $client->recordings->getAudio($recordingSid); file_put_contents("{$recordingSid}.wav", $audio->content);
If the recording is gone (no local file and no S3 key), GoneException is raised.
π¨ Error Handling
All HTTP errors extend VoiceML\Exception\ApiException, which extends VoiceML\Exception\VoiceMLException (a RuntimeException). Catch broadly or narrowly:
| Status | Exception |
|---|---|
| 400 | VoiceML\Exception\BadRequestException |
| 401 | VoiceML\Exception\AuthenticationException |
| 403 | VoiceML\Exception\PermissionDeniedException |
| 404 | VoiceML\Exception\NotFoundException |
| 409 | VoiceML\Exception\ConflictException |
| 410 | VoiceML\Exception\GoneException |
| 429 | VoiceML\Exception\RateLimitException |
| 501 | VoiceML\Exception\NotImplementedApiException |
| 5xx | VoiceML\Exception\ServerException |
| other | VoiceML\Exception\ApiException |
<?php use VoiceML\Client; use VoiceML\Exception\NotFoundException; use VoiceML\Exception\RateLimitException; $client = new Client(accountSid: 'ACβ¦', apiKey: 'β¦'); try { $call = $client->calls->get('CA0000000000000000000000000000aaaa'); } catch (NotFoundException $e) { echo "That call isn't on your account.", PHP_EOL; } catch (RateLimitException $e) { $retry = $e->body['retry_after'] ?? '?'; echo "Slow down β retry in {$retry}s", PHP_EOL; }
The Twilio-compatible error body (code, message, more_info, status) is parsed into $e->errorCode, $e->getMessage(), $e->moreInfo (also $e->getMoreInfo()), and $e->body.
π Pagination
List operations return a β¦List model with a Twilio-compatible pagination envelope (page, pageSize, total, nextPageUri, previousPageUri, β¦). For /Calls and /Messages, use the iterate() generator helper to walk all pages transparently:
foreach ($client->calls->iterate(status: 'completed', pageSize: 200) as $call) { process($call); } foreach ($client->messages->iterate(from: '+18005550000', pageSize: 200) as $msg) { archive($msg); }
For other resources, page manually with $client-><resource>->list(new ListXParams(page: $n)) and walk nextPageUri until null.
π Migration from twilio-php
The accountSid + API token pair Twilio's SDK validates in its constructor works unchanged here:
<?php // Before β Twilio // use Twilio\Rest\Client as TwilioClient; // $client = new TwilioClient('ACβ¦', '<token>'); // $call = $client->calls->create('+18005551234', '+18005550000', ['url' => '...']); // After β VoiceML (Twilio-compatible) use VoiceML\Client; use VoiceML\Model\CreateCallRequest; $client = new Client(accountSid: 'ACβ¦', apiKey: '<api-key>'); $call = $client->calls->create(new CreateCallRequest( to: '+18005551234', from: '+18005550000', url: 'https://example.com/twiml', ));
Method names follow the resource map above ($client->calls->create(...), $client->queues->list(), β¦) rather than Twilio's $client->api->v2010->accounts($sid)->calls->create(...) chain β flatter, fewer keystrokes, same wire format on the way out. Request bodies are typed VoiceML\Model\β¦ objects instead of associative arrays, so your IDE catches typos before runtime.
β±οΈ Rate Limits
VoiceML applies per-tenant rate limits at the edge. The SDK automatically retries 429 responses with Retry-After honored, up to maxRetries (default 2). To bump it:
$client = new VoiceML\Client( accountSid: 'ACβ¦', apiKey: 'β¦', maxRetries: 4, timeout: 60.0, );
π οΈ Development
git clone https://github.com/voicetel/voiceml-php-sdk cd voiceml-php-sdk composer install # Unit tests (fast, no network) vendor/bin/phpunit # Conformance test (OpenAPI shape coverage) vendor/bin/phpunit tests/ConformanceTest.php
π API Documentation
- Reference docs: voicetel.com/docs/api/v0.9/voiceml/
- Validator: voicetel.com/voiceml/validator/
- SDK catalogue: voicetel.com/docs/voiceml-sdks/
- Type definitions: see the
VoiceML\Modelnamespace β every wire shape has a typed PHP class.
π Contributors
- Michael Mavroudis β Lead Developer
Contributions welcome. Open an issue describing the change you want to make, or send a pull request against main.
π Sponsors
| Sponsor | Contribution |
|---|---|
| VoiceTel Communications | Primary development and production hosting |
π License
MIT with the Commons Clause restriction. See LICENSE and voicetel.com/legal/.