Diagnostic Twilio
Dial rejected with 13214 on a passed-through caller ID
Call forwarding works. It has worked all week. Then a handful of calls fail, and they fail without a pattern anyone can see — not one number, not one hour, not one destination. What they have in common is invisible from the outside: the inbound leg arrived carrying a caller ID the terminating carrier will not accept, your <Dial> passed it through unchanged, and Twilio logged 13214 Dial: Invalid callerId value against a call nobody was watching.
Sweep GET https://monitor.twilio.com/v1/Alerts at both LogLevel=error and LogLevel=warning, because several of the 132xx Dial attribute errors are logged as warnings and an error-only sweep reports a clean account. Keep the alerts whose error_code is 13214.
Take each alert's resource_sid and read GET /2010-04-01/Accounts/{AccountSid}/Calls/{CallSid}.json. When direction is inbound and from is not valid E.164, you are looking at pass-through: <Dial> with no callerId hands the inbound From straight to the outbound leg. When from is well formed, compare it against GET /2010-04-01/Accounts/{AccountSid}/OutgoingCallerIds.json and the account's own numbers instead.
The problem in plain words
The intermittency is the whole difficulty. Most inbound calls carry a clean E.164 From, so forwarding works, so the code that does it looks correct. The calls that fail are the ones where some upstream carrier delivered something else: a national-format number with no country code, a number with spaces in it, a literal anonymous, a SIP URI. That garbage is not yours and you cannot predict it, but <Dial> without an explicit callerId will faithfully forward it to a terminating provider that rejects it.
And it is logged somewhere nobody reads. The failure is on the outbound child leg of an inbound call, so the parent call often shows as completed. The alert exists, but if your monitoring queries the Alerts API at LogLevel=error only, some of the 132xx family never appear at all. The result is a failure mode that has both a specific error code and no visibility, which is the worst of both.
Why it happens
Pass-through is the default and it reads as correct. Forwarding a call while preserving the original caller's number is exactly what most people want, and omitting callerId is how you ask for it. Nothing in the TwiML suggests you have taken a dependency on the formatting habits of every carrier that might route a call to you.
Twilio only presents caller IDs it can vouch for. The callerId on a <Dial> has to be a number on the account or a verified outgoing caller ID. A passed-through From from an arbitrary inbound caller is neither, so even a perfectly formatted number can be refused for a reason that has nothing to do with formatting.
Some of the 132xx family are warnings. The Alerts API separates error from warning, and a dashboard or script built around the error level will show nothing while these accumulate. This is the single most common reason a team believes they have no Dial problems.
The parent call looks fine. The inbound leg connects, executes TwiML and ends normally. The rejected leg is a child call, and unless you are joining alerts to calls, nothing puts the two together for you.
The fix, as a flow
The script sweeps Alerts at the error and the warning level and merges on the alert sid, because several of the 132xx Dial errors are warnings and an error only query reports a clean account while the calls keep failing.
How to fix it
Sweep the Alerts API at both log levels
GET https://monitor.twilio.com/v1/Alerts?LogLevel=error&StartDate=YYYY-MM-DD&PageSize=1000, then the same request with LogLevel=warning, following meta.next_page_url — on this API the next page is an absolute URL, not the relative next_page_uri the 2010-04-01 API uses. De-duplicate on sid. Alerts are retained 30 days, so a longer window is the same window with a misleading label.
Filter to 13214, reading error_code as an integer
The Monitor API returns error_code as a string, unlike the Messages list. Compare it as a number, or compare both as strings consistently, but do not mix the two — a filter that silently matches nothing is indistinguishable from a healthy account.
Resolve each alert to its call
GET /2010-04-01/Accounts/{AccountSid}/Calls/{CallSid}.json using the alert's resource_sid. Cache by SID: one bad forwarding rule produces many alerts against a small number of calls, and re-fetching the same call fifty times is fifty requests you did not need.
Classify the caller ID, then check it is one you may present
Not valid E.164 on an inbound call is pass-through, and the fix is in your TwiML. Valid E.164 that is not one of the account's numbers and not in GET /2010-04-01/Accounts/{AccountSid}/OutgoingCallerIds.json is a different problem with the same error code, and the fix is a verification.
Set an explicit callerId and validate the pass-through
Put a real number on every <Dial callerId="+1...">. If you must preserve the original caller, validate the inbound From against E.164 in your webhook first and substitute one of your own numbers when it fails. Then re-run this sweep over the following week: the count going to zero is the only confirmation that matters, because you cannot reproduce this on demand.
How to check it worked
Re-run the sweep over a window that starts after the deploy. The 13214 count should be zero.
python3 twilio_dial_caller_id_audit.py --days 7
# 0 alert(s) with error_code 13214 in the last 7 day(s)
The full code
Two paginated sweeps of the Alerts API, one per log level, then one cached GET per distinct call and one listing of the account's usable caller IDs. Every request is a GET; an API Key with read access is enough. Two pure functions carry the diagnosis: one classifies a caller ID string on its own terms, and one decides what a 13214 on a given call actually means. Both are the kind of rule that is easy to write approximately and worth writing exactly.
"""Report Twilio 13214 alerts and say why each caller ID was rejected.
Read only. GET requests and nothing else: give this an API Key with read access
rather than the account auth token. The repair is printed, never performed,
because this script holds a credential to an account that can place calls and
spend money.
"""
import argparse
import datetime as dt
import logging
import os
import sys
import requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("twilio_dial_caller_id_audit")
HOST = "https://api.twilio.com"
BASE = HOST + "/2010-04-01"
MONITOR = "https://monitor.twilio.com/v1"
DIAL_CALLER_ID = 13214
# ITU E.164 allows at most 15 digits after the plus. The lower bound is a
# judgement: nothing routable is shorter than a country code plus a few digits,
# and being generous here is better than flagging a valid short number.
E164_MAX = 15
E164_MIN = 7
WITHHELD = {"anonymous", "unavailable", "restricted", "unknown", "private",
"unknown caller", "not available"}
def caller_id_state(value):
"""Classify a caller ID string on its own, with no account context.
The states are the shapes carriers actually deliver on an inbound From,
each of which fails differently: nothing at all, a SIP URI, a withheld
marker, a national-format number, and a digit string outside E.164.
"""
v = str(value or "").strip()
if not v:
return "absent"
low = v.lower()
if low.startswith("sip:") or low.startswith("sips:") or "@" in v:
return "sip-uri"
if low.startswith("client:"):
return "client"
if low in WITHHELD:
return "withheld"
if not v.startswith("+"):
return "not-e164"
digits = v[1:]
if not digits.isdigit():
return "not-e164"
if len(digits) < E164_MIN or len(digits) > E164_MAX:
return "out-of-range"
return "e164"
def verdict(call, verified=()):
"""Explain one 13214 given the call it was raised against.
verified is every caller ID this account may present: its own phone numbers
plus its verified OutgoingCallerIds. Pure, so both the string rules and the
account rule can be tested without a network.
Returns (state, detail).
"""
frm = str(call.get("from") or "").strip()
shape = caller_id_state(frm)
direction = str(call.get("direction") or "").strip().lower()
if shape != "e164":
if direction == "inbound":
return ("passthrough",
"the inbound leg arrived with from=%s (%s) and a <Dial> "
"with no callerId passed it straight to the outbound leg, "
"which the terminating carrier refused."
% (frm or "<empty>", shape))
return ("malformed",
"callerId %s is %s, so it was rejected before the call was "
"placed." % (frm or "<empty>", shape))
if frm not in set(verified):
return ("unverified",
"%s is well formed but is not a number on this account and is "
"not a verified outgoing caller ID, so Twilio will not present "
"it." % frm)
return ("presentable",
"%s is a caller ID this account may present, so the 13214 came from "
"something else on the <Dial>: check the callerId attribute for "
"whitespace, and check the TwiML that generated it." % frm)
def get(session, url, **params):
r = session.get(url, params=params, timeout=30)
if r.status_code in (401, 403):
raise SystemExit("%d from Twilio: check TWILIO_ACCOUNT_SID and that the "
"API key belongs to that account with read access"
% r.status_code)
r.raise_for_status()
return r.json()
def list_alerts(session, since, limit, log_level):
"""Page the Monitor alerts at one log level. next_page_url is absolute here."""
url = MONITOR + "/Alerts"
params = {"LogLevel": log_level, "StartDate": since, "PageSize": 1000}
out = []
while url and len(out) < limit:
page = get(session, url, **params)
out.extend(page.get("alerts", []))
url = (page.get("meta") or {}).get("next_page_url")
params = {}
return out[:limit]
def sweep_alerts(session, since, limit, levels):
"""Both log levels, de-duplicated on sid.
Several of the 132xx Dial attribute errors are logged at warning rather than
error. A sweep that reads only the error level reports a clean account while
the calls keep failing, which is the reason this function exists at all.
"""
seen = {}
for level in levels:
for a in list_alerts(session, since, limit, level):
seen.setdefault(a.get("sid"), a)
return list(seen.values())
def page_2010(session, url, key):
params = {"PageSize": 1000}
out = []
while url:
body = get(session, url, **params)
out.extend(body.get(key, []))
nxt = body.get("next_page_uri")
url, params = (HOST + nxt) if nxt else None, {}
return out
def presentable_caller_ids(session, account):
"""Every caller ID this account may present: its numbers plus verified ones."""
numbers = page_2010(session, "%s/Accounts/%s/IncomingPhoneNumbers.json"
% (BASE, account), "incoming_phone_numbers")
verified = page_2010(session, "%s/Accounts/%s/OutgoingCallerIds.json"
% (BASE, account), "outgoing_caller_ids")
out = {str(n.get("phone_number") or "").strip() for n in numbers}
out |= {str(v.get("phone_number") or "").strip() for v in verified}
out.discard("")
return out
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--days", type=int, default=7,
help="how far back to sweep (alerts are retained 30 days)")
ap.add_argument("--max-alerts", type=int, default=10000,
help="stop after this many alerts per log level")
ap.add_argument("--errors-only", action="store_true",
help="skip the warning level, which will under-report")
args = ap.parse_args()
account = os.environ.get("TWILIO_ACCOUNT_SID")
key = os.environ.get("TWILIO_API_KEY")
secret = os.environ.get("TWILIO_API_SECRET")
if not (account and key and secret):
log.error("set TWILIO_ACCOUNT_SID, TWILIO_API_KEY and TWILIO_API_SECRET "
"(an API Key with read access, not the auth token)")
return 2
session = requests.Session()
session.auth = (key, secret)
days = min(args.days, 30)
since = (dt.date.today() - dt.timedelta(days=days)).isoformat()
levels = ["error"] if args.errors_only else ["error", "warning"]
alerts = sweep_alerts(session, since, args.max_alerts, levels)
hits = [a for a in alerts
if str(a.get("error_code") or "").strip() == str(DIAL_CALLER_ID)]
if not hits:
log.info("0 alert(s) with error_code %d in the last %d day(s)",
DIAL_CALLER_ID, days)
return 0
verified = presentable_caller_ids(session, account)
calls = {}
counts = {}
for a in hits:
sid = a.get("resource_sid") or ""
if not sid.startswith("CA"):
log.warning("13214 alert %s has no call sid to resolve", a.get("sid"))
continue
if sid not in calls:
calls[sid] = get(session, "%s/Accounts/%s/Calls/%s.json"
% (BASE, account, sid))
state, detail = verdict(calls[sid], verified)
counts[state] = counts.get(state, 0) + 1
log.warning("%-12s %s %s", state, sid, detail)
log.warning("%d alert(s) with error_code %d across %d call(s): %s",
len(hits), DIAL_CALLER_ID, len(calls),
", ".join("%s=%d" % kv for kv in sorted(counts.items())))
log.warning(" repair: set an explicit callerId on every <Dial>, using one "
"of this account's numbers, and validate the inbound From "
"against E.164 before forwarding it")
log.warning(" verified caller IDs: GET %s/Accounts/%s/OutgoingCallerIds.json",
BASE, account)
return 1
if __name__ == "__main__":
sys.exit(main())
/**
* Report Twilio 13214 alerts and say why each caller ID was rejected.
*
* Read only. GET requests and nothing else: give this an API Key with read
* access rather than the account auth token. The repair is printed, never
* performed.
*/
const HOST = 'https://api.twilio.com';
const BASE = `${HOST}/2010-04-01`;
const MONITOR = 'https://monitor.twilio.com/v1';
const DIAL_CALLER_ID = 13214;
// ITU E.164 allows at most 15 digits after the plus. The lower bound is a
// judgement: being generous beats flagging a valid short number.
const E164_MAX = 15;
const E164_MIN = 7;
const WITHHELD = new Set(['anonymous', 'unavailable', 'restricted', 'unknown',
'private', 'unknown caller', 'not available']);
/**
* Classify a caller ID string on its own, with no account context. The states
* are the shapes carriers actually deliver on an inbound From.
*/
export function callerIdState(value) {
const v = String(value ?? '').trim();
if (!v) return 'absent';
const low = v.toLowerCase();
if (low.startsWith('sip:') || low.startsWith('sips:') || v.includes('@')) return 'sip-uri';
if (low.startsWith('client:')) return 'client';
if (WITHHELD.has(low)) return 'withheld';
if (!v.startsWith('+')) return 'not-e164';
const digits = v.slice(1);
if (!/^[0-9]+$/.test(digits)) return 'not-e164';
if (digits.length < E164_MIN || digits.length > E164_MAX) return 'out-of-range';
return 'e164';
}
/**
* Explain one 13214 given the call it was raised against. `verified` is every
* caller ID this account may present: its own numbers plus its verified
* OutgoingCallerIds. Pure. Returns [state, detail].
*/
export function verdict(call, verified = []) {
const frm = String(call.from ?? '').trim();
const shape = callerIdState(frm);
const direction = String(call.direction ?? '').trim().toLowerCase();
if (shape !== 'e164') {
if (direction === 'inbound') {
return ['passthrough',
`the inbound leg arrived with from=${frm || '<empty>'} (${shape}) and a ` +
'<Dial> with no callerId passed it straight to the outbound leg, which ' +
'the terminating carrier refused.'];
}
return ['malformed',
`callerId ${frm || '<empty>'} is ${shape}, so it was rejected before the ` +
'call was placed.'];
}
if (!new Set(verified).has(frm)) {
return ['unverified',
`${frm} is well formed but is not a number on this account and is not a ` +
'verified outgoing caller ID, so Twilio will not present it.'];
}
return ['presentable',
`${frm} is a caller ID this account may present, so the 13214 came from ` +
'something else on the <Dial>: check the callerId attribute for whitespace, ' +
'and check the TwiML that generated it.'];
}
function authHeader(key, secret) {
return `Basic ${Buffer.from(`${key}:${secret}`).toString('base64')}`;
}
async function get(auth, url, params = {}) {
const u = new URL(url);
for (const [k, v] of Object.entries(params)) u.searchParams.set(k, v);
const res = await fetch(u, { headers: { Authorization: auth } });
if (res.status === 401 || res.status === 403) {
throw new Error(`${res.status} from Twilio: check TWILIO_ACCOUNT_SID and ` +
'that the API key belongs to that account with read access');
}
if (!res.ok) throw new Error(`${res.status} from ${u.pathname}`);
return res.json();
}
async function listAlerts(auth, since, limit, logLevel) {
let url = `${MONITOR}/Alerts`;
let params = { LogLevel: logLevel, StartDate: since, PageSize: 1000 };
const out = [];
while (url && out.length < limit) {
const page = await get(auth, url, params);
out.push(...(page.alerts ?? []));
url = page.meta?.next_page_url ?? null;
params = {};
}
return out.slice(0, limit);
}
/**
* Both log levels, de-duplicated on sid. Several of the 132xx Dial attribute
* errors are logged at warning rather than error, so an error-only sweep
* reports a clean account while the calls keep failing.
*/
export async function sweepAlerts(auth, since, limit, levels) {
const seen = new Map();
for (const level of levels) {
for (const a of await listAlerts(auth, since, limit, level)) {
if (!seen.has(a.sid)) seen.set(a.sid, a);
}
}
return [...seen.values()];
}
async function page2010(auth, url, key) {
let params = { PageSize: 1000 };
const out = [];
while (url) {
const body = await get(auth, url, params);
out.push(...(body[key] ?? []));
url = body.next_page_uri ? HOST + body.next_page_uri : null;
params = {};
}
return out;
}
async function presentableCallerIds(auth, account) {
const numbers = await page2010(
auth, `${BASE}/Accounts/${account}/IncomingPhoneNumbers.json`, 'incoming_phone_numbers');
const verified = await page2010(
auth, `${BASE}/Accounts/${account}/OutgoingCallerIds.json`, 'outgoing_caller_ids');
const out = new Set();
for (const n of [...numbers, ...verified]) {
const v = String(n.phone_number ?? '').trim();
if (v) out.add(v);
}
return out;
}
function arg(name, fallback) {
const i = process.argv.indexOf(name);
return i === -1 ? fallback : Number(process.argv[i + 1]);
}
async function main() {
const account = process.env.TWILIO_ACCOUNT_SID;
const key = process.env.TWILIO_API_KEY;
const secret = process.env.TWILIO_API_SECRET;
if (!account || !key || !secret) {
console.error('set TWILIO_ACCOUNT_SID, TWILIO_API_KEY and TWILIO_API_SECRET ' +
'(an API Key with read access, not the auth token)');
process.exitCode = 2;
return;
}
const auth = authHeader(key, secret);
const days = Math.min(arg('--days', 7), 30);
const since = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
const levels = process.argv.includes('--errors-only') ? ['error'] : ['error', 'warning'];
const alerts = await sweepAlerts(auth, since, 10000, levels);
const hits = alerts.filter((a) => String(a.error_code ?? '').trim() === String(DIAL_CALLER_ID));
if (hits.length === 0) {
console.log(`0 alert(s) with error_code ${DIAL_CALLER_ID} in the last ${days} day(s)`);
return;
}
const verified = await presentableCallerIds(auth, account);
const calls = new Map();
const counts = new Map();
for (const a of hits) {
const sid = a.resource_sid ?? '';
if (!sid.startsWith('CA')) {
console.warn(`13214 alert ${a.sid} has no call sid to resolve`);
continue;
}
if (!calls.has(sid)) {
calls.set(sid, await get(auth, `${BASE}/Accounts/${account}/Calls/${sid}.json`));
}
const [state, detail] = verdict(calls.get(sid), verified);
counts.set(state, (counts.get(state) ?? 0) + 1);
console.warn(`${state.padEnd(12)} ${sid} ${detail}`);
}
const summary = [...counts.entries()].sort().map(([k, v]) => `${k}=${v}`).join(', ');
console.warn(`${hits.length} alert(s) with error_code ${DIAL_CALLER_ID} across ` +
`${calls.size} call(s): ${summary}`);
console.warn(' repair: set an explicit callerId on every <Dial>, using one of ' +
'this account\'s numbers, and validate the inbound From against ' +
'E.164 before forwarding it');
console.warn(` verified caller IDs: GET ${BASE}/Accounts/${account}/OutgoingCallerIds.json`);
process.exitCode = 1;
}
// Only run when invoked directly, so importing this module from the test file
// does not execute main() and fail on the missing credentials.
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((err) => { console.error(err.message); process.exitCode = 2; });
}
Add a test
The caller ID rules are worth pinning one shape at a time, because each of them is something a real carrier has actually delivered on an inbound leg: a national-format number, a number with spaces, a literal anonymous, a SIP URI, sixteen digits. The verdict cases then pin the part that is easy to get backwards — that a perfectly formatted number can still be a 13214, because Twilio will only present a caller ID the account owns or has verified.
from twilio_dial_caller_id_audit import caller_id_state, verdict
OWNED = {"+15005550006"}
def test_plain_e164_is_accepted():
assert caller_id_state("+15005550006") == "e164"
def test_national_format_has_no_country_code():
assert caller_id_state("5005550006") == "not-e164"
def test_spaces_and_punctuation_are_not_e164():
assert caller_id_state("+1 500 555-0006") == "not-e164"
def test_withheld_markers_are_their_own_state():
assert caller_id_state("anonymous") == "withheld"
assert caller_id_state("Restricted") == "withheld"
def test_sip_uri_and_client_identity_are_distinguished():
assert caller_id_state("sip:alice@example.com") == "sip-uri"
assert caller_id_state("client:alice") == "client"
def test_sixteen_digits_is_outside_e164():
assert caller_id_state("+1234567890123456") == "out-of-range"
def test_empty_is_absent():
assert caller_id_state("") == "absent"
assert caller_id_state(None) == "absent"
def test_bad_from_on_an_inbound_call_is_passthrough():
state, detail = verdict({"from": "5005550006", "direction": "inbound"}, OWNED)
assert state == "passthrough"
assert "no callerId" in detail
def test_bad_from_on_an_outbound_call_is_not_passthrough():
state, _ = verdict({"from": "anonymous", "direction": "outbound-api"}, OWNED)
assert state == "malformed"
def test_well_formed_but_unowned_number_is_still_a_13214():
# The case that reads as a false positive and is not: valid E.164 is not
# the same as a caller ID this account is allowed to present.
state, detail = verdict({"from": "+15005550999", "direction": "inbound"}, OWNED)
assert state == "unverified"
assert "verified outgoing caller ID" in detail
def test_owned_number_points_the_investigation_elsewhere():
state, _ = verdict({"from": "+15005550006", "direction": "inbound"}, OWNED)
assert state == "presentable"
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { callerIdState, verdict } from './twilio-dial-caller-id-audit.mjs';
const OWNED = ['+15005550006'];
test('plain e164 is accepted', () => {
assert.equal(callerIdState('+15005550006'), 'e164');
});
test('national format has no country code', () => {
assert.equal(callerIdState('5005550006'), 'not-e164');
});
test('spaces and punctuation are not e164', () => {
assert.equal(callerIdState('+1 500 555-0006'), 'not-e164');
});
test('withheld markers are their own state', () => {
assert.equal(callerIdState('anonymous'), 'withheld');
assert.equal(callerIdState('Restricted'), 'withheld');
});
test('sip uri and client identity are distinguished', () => {
assert.equal(callerIdState('sip:alice@example.com'), 'sip-uri');
assert.equal(callerIdState('client:alice'), 'client');
});
test('sixteen digits is outside e164', () => {
assert.equal(callerIdState('+1234567890123456'), 'out-of-range');
});
test('empty is absent', () => {
assert.equal(callerIdState(''), 'absent');
assert.equal(callerIdState(null), 'absent');
});
test('bad from on an inbound call is passthrough', () => {
const [state, detail] = verdict({ from: '5005550006', direction: 'inbound' }, OWNED);
assert.equal(state, 'passthrough');
assert.match(detail, /no callerId/);
});
test('bad from on an outbound call is not passthrough', () => {
assert.equal(verdict({ from: 'anonymous', direction: 'outbound-api' }, OWNED)[0],
'malformed');
});
test('well formed but unowned number is still a 13214', () => {
const [state, detail] = verdict({ from: '+15005550999', direction: 'inbound' }, OWNED);
assert.equal(state, 'unverified');
assert.match(detail, /verified outgoing caller ID/);
});
test('owned number points the investigation elsewhere', () => {
assert.equal(verdict({ from: '+15005550006', direction: 'inbound' }, OWNED)[0],
'presentable');
});
FAQ
Why does the script sweep the warning level as well as error?
Because several of the 132xx Dial attribute errors are logged at LogLevel=warning rather than error. A sweep filtered to the error level returns nothing and reads as a clean account while the calls keep failing. Both levels are swept and the results de-duplicated on the alert sid, which costs one extra paginated read.
Is a valid E.164 number ever rejected as 13214?
Yes, and it is the case people call a false positive. The callerId on a Dial has to be a number on the account or a verified outgoing caller ID; anything else is refused however well formatted it is. That is why the script builds the set from IncomingPhoneNumbers and OutgoingCallerIds before judging anything.
Why look at the parent call rather than the child leg?
Because the parent is where the evidence is. The alert's resource_sid resolves to the call whose TwiML ran the Dial, and that call's from is the value that was passed through. Its direction tells you whether pass-through was even possible, which is what separates a TwiML bug from an unverified number.
How far back can this look?
Thirty days, because that is how long Twilio retains alerts, and at most 10,000 alerts per request. The script caps the window at 30 days rather than accepting a larger number and quietly returning the same data under a misleading label.
What is the actual fix in the TwiML?
Set callerId explicitly on Dial, to one of your own numbers. If you need to preserve the original caller's number, validate the inbound From against E.164 in your webhook and fall back to your own number when it fails, which is the same rule this script uses to classify.
Related field notes
- Outbound calls quietly failing more often
- A SIP Domain with no auth_type accepts nothing
- A status callback failing with 11200
Sources
Every figure in this note is traced to one of these. Prices are list rates and change — check them for your own region before acting.
- Error 13214: Dial: Invalid callerId value — Twilio Docs
- Call resource — Twilio Docs
- Alert resource (Monitor) — Twilio Docs
- OutgoingCallerId resource — Twilio Docs
If your setup is misbehaving in a way this note does not cover, message me on LinkedIn with what you are seeing.