Migration

Move compatible voice workflows in measured steps.

Keep the call-control model your application already uses, point supported voice requests at VoiceTel, verify signed webhooks, and cut over only after behavior and cost are proven.

Check compatibility Run calculator

What stays the same; what is checked

  • Same model — XML voice markup, form-encoded webhooks, request signatures, account-style credentials, and voice-call lifecycle resources stay familiar.
  • Checked before launch — Any payment, non-voice, phone-number provisioning, or messaging API calls is identified and routed to native VoiceTel products or removed from the compatible voice path.
  • Measured economics — Compare a real monthly workload, not a broad slogan.

Four-step migration

  1. Get account credentials — VoiceTel onboarding returns an account SID (Twilio-format AC + 32 hex) and an API key. The API key is shown once at create or rotate time — capture it on your side immediately.
  2. Override the SDK host — Subclass your Twilio helper library's HTTP client and rewrite outbound URLs from api.twilio.com to the VoiceTel host. About five lines per language. Authentication, signing, request shapes, and response shapes match Twilio exactly — only the destination host differs.
  3. Verify webhook signatures — VoiceTel signs every webhook with X-Twilio-Signature using the per-tenant API key. The Twilio SDK's verification middleware works without modification: pass the API key wherever the SDK asks for the auth token.
  4. Cut over a single call flow — Run a real call against VoiceTel; verify origination, webhook delivery, recordings if used, and the per-call billing line. Expand to additional flows once the test passes.

Direct HTTP test call

A one-call test proves credentials, routing, webhook reachability, and basic call control before deeper migration work. The URL shape is identical to Twilio's — only the host changes.

SID="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
KEY="..."  # api_key from onboarding
HOST="voiceml.voicetel.com"

curl -u "$SID:$KEY" \
  -d "To=+18005551234" \
  -d "From=+18005550000" \
  -d "Url=https://your-app/voicemarkup" \
  -d "MachineDetection=Enable" \
  https://$HOST/2010-04-01/Accounts/$SID/Calls

HTTP Basic auth: username = SID, password = API key — the same convention Twilio's docs use for AccountSid:AuthToken.

SDK examples

Each SDK accepts a custom HTTP client. Subclass the bundled client and rewrite outbound URLs from api.twilio.com to voiceml.voicetel.com — about five lines per language. Authentication, request shapes, and webhook signing match Twilio exactly; only the destination host differs.

// npm install twilio
const twilio = require('twilio');
const RequestClient = require('twilio/lib/base/RequestClient');

class VoiceTelClient extends RequestClient {
	async request(opts) {
		opts.uri = opts.uri.replace(
			/https:\/\/[^/]+\.twilio\.com/,
			'https://voiceml.voicetel.com'
		);
		return super.request(opts);
	}
}

const client = twilio(
	process.env.VOICETEL_ACCOUNT_SID,
	process.env.VOICETEL_API_KEY,
	{ httpClient: new VoiceTelClient() }
);

const call = await client.calls.create({
	to:   '+18005551234',
	from: '+18005550000',
	url:  'https://your-app/voicemarkup',
});
console.log(call.sid);
# pip install twilio
import os
from twilio.rest import Client
from twilio.http.http_client import TwilioHttpClient

class VoiceTelClient(TwilioHttpClient):
	def request(self, method, url, *args, **kwargs):
		url = url.replace(
			'https://api.twilio.com',
			'https://voiceml.voicetel.com',
		)
		return super().request(method, url, *args, **kwargs)

client = Client(
	os.environ['VOICETEL_ACCOUNT_SID'],
	os.environ['VOICETEL_API_KEY'],
	http_client=VoiceTelClient(),
)

call = client.calls.create(
	to='+18005551234',
	from_='+18005550000',
	url='https://your-app/voicemarkup',
)
print(call.sid)
// go get github.com/twilio/twilio-go
package main

import (
	"net/http"
	"net/url"
	"os"
	"strings"

	"github.com/twilio/twilio-go"
	"github.com/twilio/twilio-go/client"
	openapi "github.com/twilio/twilio-go/rest/api/v2010"
)

type voiceTelClient struct{ inner *http.Client }

func (c *voiceTelClient) Do(req *http.Request) (*http.Response, error) {
	if strings.HasSuffix(req.URL.Host, ".twilio.com") {
		rewritten, _ := url.Parse("https://voiceml.voicetel.com" +
			req.URL.RequestURI())
		req.URL = rewritten
		req.Host = rewritten.Host
	}
	return c.inner.Do(req)
}

func main() {
	httpClient := &voiceTelClient{inner: http.DefaultClient}

	tw := twilio.NewRestClientWithParams(twilio.ClientParams{
		Username: os.Getenv("VOICETEL_ACCOUNT_SID"),
		Password: os.Getenv("VOICETEL_API_KEY"),
		Client: &client.Client{
			HTTPClient: httpClient,
			Credentials: client.NewCredentials(
				os.Getenv("VOICETEL_ACCOUNT_SID"),
				os.Getenv("VOICETEL_API_KEY"),
			),
		},
	})

	params := &openapi.CreateCallParams{}
	params.SetTo("+18005551234")
	params.SetFrom("+18005550000")
	params.SetUrl("https://your-app/voicemarkup")

	call, err := tw.Api.CreateCall(params)
	if err != nil { panic(err) }
	println(*call.Sid)
}
// dotnet add package Twilio
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Twilio;
using Twilio.Clients;
using Twilio.Http;
using Twilio.Rest.Api.V2010.Account;

public class VoiceTelHttpClient : SystemNetHttpClient
{
	public VoiceTelHttpClient(HttpClient http) : base(http) { }

	public override Response MakeRequest(Request request)
	{
		var rewritten = request.Url.ToString().Replace(
			"https://api.twilio.com",
			"https://voiceml.voicetel.com");
		request.Url = new Uri(rewritten);
		return base.MakeRequest(request);
	}
}

class Program
{
	static async Task Main()
	{
		var http = new HttpClient();
		TwilioClient.Init(
			Environment.GetEnvironmentVariable("VOICETEL_ACCOUNT_SID"),
			Environment.GetEnvironmentVariable("VOICETEL_API_KEY"),
			new VoiceTelHttpClient(http));

		var call = await CallResource.CreateAsync(
			to:   new Twilio.Types.PhoneNumber("+18005551234"),
			from: new Twilio.Types.PhoneNumber("+18005550000"),
			url:  new Uri("https://your-app/voicemarkup"));

		Console.WriteLine(call.Sid);
	}
}
// composer require twilio/sdk
<?php
require __DIR__ . '/vendor/autoload.php';

use Twilio\Rest\Client;
use Twilio\Http\CurlClient;
use Twilio\Http\Response;

class VoiceTelClient extends CurlClient {
	public function request(
		string $method, string $url, array $params = [],
		array $data = [], array $headers = [],
		?string $user = null, ?string $password = null,
		?int $timeout = null
	): Response {
		$url = preg_replace(
			'#https://[^/]+\.twilio\.com#',
			'https://voiceml.voicetel.com',
			$url);
		return parent::request(
			$method, $url, $params, $data, $headers,
			$user, $password, $timeout);
	}
}

$client = new Client(
	getenv('VOICETEL_ACCOUNT_SID'),
	getenv('VOICETEL_API_KEY'),
	null, null,
	new VoiceTelClient()
);

$call = $client->calls->create(
	'+18005551234',
	'+18005550000',
	['url' => 'https://your-app/voicemarkup']
);
echo $call->sid . "\n";
// Gradle: implementation 'com.twilio.sdk:twilio:10.+'
import com.twilio.Twilio;
import com.twilio.http.NetworkHttpClient;
import com.twilio.http.Request;
import com.twilio.http.Response;
import com.twilio.http.TwilioRestClient;
import com.twilio.rest.api.v2010.account.Call;
import com.twilio.type.PhoneNumber;
import java.net.URI;

public class VoiceTelApp {
	static class VoiceTelClient extends NetworkHttpClient {
		@Override
		public Response makeRequest(Request request) {
			String original = request.constructURL().toString();
			String rewritten = original.replaceFirst(
				"https://[^/]+\\.twilio\\.com",
				"https://voiceml.voicetel.com");
			Request copy = new Request(request.getMethod(), rewritten);
			request.getQueryParams().forEach(
				(k, v) -> v.forEach(val -> copy.addQueryParam(k, val)));
			request.getPostParams().forEach(
				(k, v) -> v.forEach(val -> copy.addPostParam(k, val)));
			return super.makeRequest(copy);
		}
	}

	public static void main(String[] args) {
		String sid = System.getenv("VOICETEL_ACCOUNT_SID");
		String key = System.getenv("VOICETEL_API_KEY");
		Twilio.init(sid, key);
		Twilio.setRestClient(new TwilioRestClient.Builder(sid, key)
			.httpClient(new VoiceTelClient()).build());

		Call call = Call.creator(
			new PhoneNumber("+18005551234"),
			new PhoneNumber("+18005550000"),
			URI.create("https://your-app/voicemarkup")
		).create();

		System.out.println(call.getSid());
	}
}
# gem install twilio-ruby
require 'twilio-ruby'

class VoiceTelHttpClient < Twilio::HTTP::Client
	def request(method, host, port, uri, params, data, headers,
				auth, timeout, allow_redirects)
		rewritten = uri.sub(
			%r{https://[^/]+\.twilio\.com},
			'https://voiceml.voicetel.com'
		)
		super(method, 'voiceml.voicetel.com', port,
			  rewritten, params, data, headers,
			  auth, timeout, allow_redirects)
	end
end

client = Twilio::REST::Client.new(
	ENV['VOICETEL_ACCOUNT_SID'],
	ENV['VOICETEL_API_KEY'],
	nil, nil,
	VoiceTelHttpClient.new
)

call = client.calls.create(
	to:   '+18005551234',
	from: '+18005550000',
	url:  'https://your-app/voicemarkup'
)
puts call.sid