Diagnostic Twilio
an approved brand with no trust score is throttled to the floor
Registration is finished. status reads APPROVED, the campaign is VERIFIED, numbers are registered, and messages queue up behind a throughput ceiling that nobody set. Sometimes a campaign is refused outright with "brand not qualified to run Campaign for AT&T". The field that explains all of it is brand_score, and it is null.
Read GET https://messaging.twilio.com/v1/a2p/BrandRegistrations/{BrandSid} and flag any brand where status is APPROVED and brand_score is null. Read brand_type, identity_status and skip_automatic_sec_vet alongside it, then GET /v1/a2p/BrandRegistrations/{BrandSid}/Vettings for vetting_status, vetting_class and vetting_provider.
Only Standard brands are scored. A SOLE_PROPRIETOR or LOW_VOLUME_STANDARD brand has no score by definition and its throughput is fixed by use case, so a null score there is not a finding — reporting it as one buries the Standard brand that genuinely lost its vetting.
The problem in plain words
This is the only A2P problem in this section where everything reads as successful. There is no error code on the brand, no rejected campaign to look at, no failed send to trace. The registration completed, and the consequence is a number that is absent rather than wrong: messages per second toward AT&T, T-Mobile and Verizon scale with the trust score, and a brand without one is treated as the least trusted thing on the network.
What that looks like from the application is a queue. Sends are accepted, they sit, and they arrive late under load, which reads as a Twilio performance problem rather than a registration one. The other face of it is blunter: a campaign submission refused with a message about the brand not qualifying for AT&T, which sends the team to edit campaign copy for a rejection that has nothing to do with the campaign.
Why it happens
Approval and scoring are two separate outcomes. The brand can pass registration and never acquire a score. status answers "is this brand real"; brand_score answers "how much throughput will carriers give it", and the second question has no bearing on the first.
A flag set once at creation stays set. If skip_automatic_sec_vet was true when the brand was submitted — often to avoid the vetting fee during a proof of concept — the automatic vetting never ran, and nothing later in the lifecycle goes back and runs it.
Vetting can fail on its own, quietly. The Vettings subresource is a separate object with its own vetting_status. A record sitting at FAILED or PENDING explains a null score, and it lives one GET away from the brand rather than on it.
Null is a legitimate value for most brand types. Sole Proprietor and Low-Volume Standard brands are never scored. A check that flags every null score produces a report where the real finding is one row among many, which is the same as not having found it.
The fix, as a flow
brand_score is tested against null rather than for truthiness, because the scale starts at zero: a score of 0 is the lowest trust rating there is, and it is not the same as never having been vetted.
How to fix it
Filter to APPROVED before anything else
A brand that is PENDING, FAILED or SUSPENDED has a null score for an obvious reason and belongs in a different report. This check is specifically about the brand that finished registration successfully and is still throttled.
Read brand_type and stop there when it is not Standard
SOLE_PROPRIETOR and LOW_VOLUME_STANDARD brands do not receive a secondary vetting score at all; their throughput is set by the use case. Reporting them alongside the Standard brand that lost its vetting is how the real finding gets lost.
Test brand_score for null, not for falsiness
The score runs from 0 to 100 and 0 is a real, meaningful score — a very low trust rating, not a missing one. A truthiness check treats them as the same thing and reports a scored brand as unvetted, which is the opposite of the truth.
Fetch the Vettings subresource for the reason
GET /v1/a2p/BrandRegistrations/{BrandSid}/Vettings returns the vetting records with vetting_status of PENDING, SUCCESS or FAILED, plus vetting_class and vetting_provider. No records at all, a failed record and a record still pending are three different situations with three different responses.
Request vetting, and treat a SUCCESS with no score as a disagreement
The repair is a vetting request against the brand with VettingProvider=aegis, or campaign-verify plus a VettingId for political brands. If a record already reads SUCCESS and the score is still null, do not request another one — report the disagreement, because a second request costs the fee again for a vetting that already happened.
How to check it worked
Re-run the script. Every approved Standard brand should carry a brand_score, and no vetting record should be sitting at FAILED.
python3 twilio_a2p_brand_vetting_audit.py
# 3 brand(s), 0 approved without a trust score
The full code
One paginated GET over the brands and one per approved Standard brand for its vetting records — the subresource is only worth fetching once the brand type says a score was ever expected. The classifier is pure and takes both, because the distinction that matters is between a score of zero, a score that is missing, and a brand type that was never going to have one.
"""Report approved A2P brands that carry no trust score, and say why.
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 send messages and
spend money.
"""
import argparse
import logging
import os
import sys
import requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("twilio_a2p_brand_vetting_audit")
MSG = "https://messaging.twilio.com/v1"
# Only Standard brands receive a secondary vetting score. Sole Proprietor and
# Low-Volume Standard throughput is fixed by use case, so a null score on those
# is the documented behaviour rather than a finding.
SCORED_TYPE = "STANDARD"
UNSCORED_TYPES = ("SOLE_PROPRIETOR", "LOW_VOLUME_STANDARD")
# Precedence when a brand has several vetting records: an in-flight retry says
# more about what to do next than the failure it is retrying.
VETTING_ORDER = ("SUCCESS", "PENDING", "FAILED")
def vetting_state(vettings):
"""The one vetting_status that decides what to do next. Pure.
Returns success, pending, failed or none. A brand can accumulate several
records, and reporting the newest is less useful than reporting the one that
changes the recommendation.
"""
seen = {str(v.get("vetting_status") or "").upper() for v in vettings or []}
for status in VETTING_ORDER:
if status in seen:
return status.lower()
return "none"
def verdict(brand, vettings=()):
"""Classify one approved brand by whether it has a usable trust score. Pure.
Returns (state, detail).
"""
status = str(brand.get("status") or "").upper()
if status != "APPROVED":
return ("not-approved",
"status is %s: a brand that has not been approved has no score "
"for a reason that has nothing to do with vetting."
% (status or "unset"))
brand_type = str(brand.get("brand_type") or "").upper()
score = brand.get("brand_score")
# 0 is a real score, and the lowest one. A truthiness check here reports a
# scored brand as unvetted, which is exactly backwards.
if score is not None:
return ("scored",
"brand_score is %s; carrier throughput scales with it." % score)
if brand_type in UNSCORED_TYPES:
return ("not-eligible",
"%s brands are never scored and their throughput is fixed by "
"use case, so a null brand_score here is expected." % brand_type)
if brand_type != SCORED_TYPE:
return ("unknown-brand-type",
"brand_type is %s, which this script cannot say is eligible for "
"a score." % (brand_type or "unset"))
state = vetting_state(vettings)
if state == "success":
return ("vetted-without-score",
"a vetting record reads SUCCESS and brand_score is still null. "
"Two objects disagree; do not pay for a second vetting on the "
"strength of one of them.")
if state == "pending":
return ("vetting-pending",
"secondary vetting is PENDING. The score arrives when it "
"resolves; throughput stays at the floor until then.")
if state == "failed":
return ("vetting-failed",
"secondary vetting FAILED, so the brand is APPROVED and "
"untrusted at the same time. Carriers treat it as low trust.")
if brand.get("skip_automatic_sec_vet"):
return ("vetting-skipped",
"skip_automatic_sec_vet was set at creation, so automatic "
"vetting never ran and nothing later runs it.")
return ("unvetted",
"APPROVED Standard brand with no score and no vetting record. "
"Throughput toward AT&T, T-Mobile and Verizon sits at the lowest "
"tier, and campaigns can be refused as unqualified.")
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_v1(session, url, key, limit=500):
"""Page a messaging.twilio.com list. meta.next_page_url is absolute."""
out = []
while url and len(out) < limit:
page = get(session, url, PageSize=50)
out.extend(page.get(key, []))
url = (page.get("meta") or {}).get("next_page_url")
return out[:limit]
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--max-brands", type=int, default=500)
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)
brands = list_v1(session, MSG + "/a2p/BrandRegistrations", "data",
args.max_brands)
if not brands:
log.info("no A2P brand registrations on this account")
return 0
bad = 0
for brand in brands:
sid = brand.get("sid", "?")
vettings = []
# Only worth a request once the brand type says a score was expected.
if (str(brand.get("status") or "").upper() == "APPROVED"
and str(brand.get("brand_type") or "").upper() == SCORED_TYPE
and brand.get("brand_score") is None):
vettings = list_v1(session,
"%s/a2p/BrandRegistrations/%s/Vettings" % (MSG, sid),
"data")
state, detail = verdict(brand, vettings)
line = "%-21s %s %s" % (state, sid, detail)
if state in ("scored", "not-eligible", "not-approved"):
log.info(line)
continue
bad += 1
log.warning(line)
for v in vettings:
log.warning(" %s vetting %s from %s",
v.get("vetting_status", "?"), v.get("vetting_class", "?"),
v.get("vetting_provider", "?"))
if state in ("unvetted", "vetting-skipped", "vetting-failed"):
log.warning(" repair: request secondary vetting on brand %s with "
"VettingProvider=aegis, or campaign-verify plus a "
"VettingId for a political brand. Console -> Messaging -> "
"Regulatory Compliance -> Brand -> Request secondary "
"vetting", sid)
elif state == "vetted-without-score":
log.warning(" repair: none yet. Re-read the brand before requesting "
"anything; a second vetting is charged again")
log.info("%d brand(s), %d approved without a trust score", len(brands), bad)
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())
/**
* Report approved A2P brands that carry no trust score, and say why.
*
* 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 MSG = 'https://messaging.twilio.com/v1';
// Only Standard brands receive a secondary vetting score. Sole Proprietor and
// Low-Volume Standard throughput is fixed by use case.
const SCORED_TYPE = 'STANDARD';
const UNSCORED_TYPES = ['SOLE_PROPRIETOR', 'LOW_VOLUME_STANDARD'];
// Precedence when a brand has several vetting records: an in-flight retry says
// more about what to do next than the failure it is retrying.
const VETTING_ORDER = ['SUCCESS', 'PENDING', 'FAILED'];
/**
* The one vetting_status that decides what to do next. Pure. Returns success,
* pending, failed or none.
*/
export function vettingState(vettings) {
const seen = new Set((vettings ?? []).map(
(v) => String(v.vetting_status ?? '').toUpperCase()));
for (const status of VETTING_ORDER) {
if (seen.has(status)) return status.toLowerCase();
}
return 'none';
}
/**
* Classify one approved brand by whether it has a usable trust score. Pure.
* Returns [state, detail].
*/
export function verdict(brand, vettings = []) {
const status = String(brand.status ?? '').toUpperCase();
if (status !== 'APPROVED') {
return ['not-approved',
`status is ${status || 'unset'}: a brand that has not been approved has ` +
'no score for a reason that has nothing to do with vetting.'];
}
const brandType = String(brand.brand_type ?? '').toUpperCase();
const score = brand.brand_score ?? null;
// 0 is a real score, and the lowest one. A truthiness check here reports a
// scored brand as unvetted, which is exactly backwards.
if (score !== null) {
return ['scored',
`brand_score is ${score}; carrier throughput scales with it.`];
}
if (UNSCORED_TYPES.includes(brandType)) {
return ['not-eligible',
`${brandType} brands are never scored and their throughput is fixed by ` +
'use case, so a null brand_score here is expected.'];
}
if (brandType !== SCORED_TYPE) {
return ['unknown-brand-type',
`brand_type is ${brandType || 'unset'}, which this script cannot say is ` +
'eligible for a score.'];
}
const state = vettingState(vettings);
if (state === 'success') {
return ['vetted-without-score',
'a vetting record reads SUCCESS and brand_score is still null. Two ' +
'objects disagree; do not pay for a second vetting on the strength of ' +
'one of them.'];
}
if (state === 'pending') {
return ['vetting-pending',
'secondary vetting is PENDING. The score arrives when it resolves; ' +
'throughput stays at the floor until then.'];
}
if (state === 'failed') {
return ['vetting-failed',
'secondary vetting FAILED, so the brand is APPROVED and untrusted at the ' +
'same time. Carriers treat it as low trust.'];
}
if (brand.skip_automatic_sec_vet) {
return ['vetting-skipped',
'skip_automatic_sec_vet was set at creation, so automatic vetting never ' +
'ran and nothing later runs it.'];
}
return ['unvetted',
'APPROVED Standard brand with no score and no vetting record. Throughput ' +
'toward AT&T, T-Mobile and Verizon sits at the lowest tier, and campaigns ' +
'can be refused as unqualified.'];
}
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();
}
export async function listV1(auth, url, key, limit = 500) {
const out = [];
let next = url;
while (next && out.length < limit) {
const page = await get(auth, next, { PageSize: 50 });
out.push(...(page[key] ?? []));
next = page.meta?.next_page_url ?? null;
}
return out.slice(0, limit);
}
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 brands = await listV1(auth, `${MSG}/a2p/BrandRegistrations`, 'data');
if (brands.length === 0) {
console.log('no A2P brand registrations on this account');
return;
}
let bad = 0;
for (const brand of brands) {
const sid = brand.sid ?? '?';
let vettings = [];
// Only worth a request once the brand type says a score was expected.
if (String(brand.status ?? '').toUpperCase() === 'APPROVED'
&& String(brand.brand_type ?? '').toUpperCase() === SCORED_TYPE
&& (brand.brand_score ?? null) === null) {
vettings = await listV1(
auth, `${MSG}/a2p/BrandRegistrations/${sid}/Vettings`, 'data');
}
const [state, detail] = verdict(brand, vettings);
const line = `${state.padEnd(21)} ${sid} ${detail}`;
if (['scored', 'not-eligible', 'not-approved'].includes(state)) {
console.log(line);
continue;
}
bad += 1;
console.warn(line);
for (const v of vettings) {
console.warn(` ${v.vetting_status ?? '?'} vetting ` +
`${v.vetting_class ?? '?'} from ${v.vetting_provider ?? '?'}`);
}
if (['unvetted', 'vetting-skipped', 'vetting-failed'].includes(state)) {
console.warn(` repair: request secondary vetting on brand ${sid} with ` +
'VettingProvider=aegis, or campaign-verify plus a VettingId ' +
'for a political brand. Console -> Messaging -> Regulatory ' +
'Compliance -> Brand -> Request secondary vetting');
} else if (state === 'vetted-without-score') {
console.warn(' repair: none yet. Re-read the brand before requesting ' +
'anything; a second vetting is charged again');
}
}
console.log(`${brands.length} brand(s), ${bad} approved without a trust score`);
process.exitCode = bad ? 1 : 0;
}
// Only run when invoked directly. The test file imports this module, and without
// the guard main() would run there too, fail on the missing credentials and set a
// non-zero exit code that fails the whole test file even as every test passes.
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((err) => { console.error(err.message); process.exitCode = 2; });
}
Add a test
The case that earns its place is brand_score of 0. It is a real score, the lowest one on a scale that starts there, and any check written as if not score reports that brand as never vetted — sending somebody to pay for a vetting that already ran and already answered. The rest pin the brand types that are correctly scoreless, so they never crowd the report.
from twilio_a2p_brand_vetting_audit import verdict, vetting_state
STANDARD = {"sid": "BN0123456789", "status": "APPROVED", "brand_type": "STANDARD",
"brand_score": None}
def test_an_approved_standard_brand_with_no_vetting_is_the_finding():
state, detail = verdict(STANDARD)
assert state == "unvetted"
assert "lowest tier" in detail
def test_a_score_of_zero_is_a_score():
# 0 is the bottom of a 0 to 100 scale, not a missing value. A truthiness
# check reports this brand as unvetted and buys a second vetting for it.
state, detail = verdict(dict(STANDARD, brand_score=0))
assert state == "scored"
assert "0" in detail
def test_sole_proprietor_brands_are_never_scored():
state, _ = verdict(dict(STANDARD, brand_type="SOLE_PROPRIETOR"))
assert state == "not-eligible"
def test_low_volume_standard_is_not_reported_either():
assert verdict(dict(STANDARD,
brand_type="LOW_VOLUME_STANDARD"))[0] == "not-eligible"
def test_the_skip_flag_is_named_when_nothing_was_ever_vetted():
state, detail = verdict(dict(STANDARD, skip_automatic_sec_vet=True))
assert state == "vetting-skipped"
assert "skip_automatic_sec_vet" in detail
def test_a_failed_vetting_record_explains_the_null_score():
state, _ = verdict(STANDARD, [{"vetting_status": "FAILED"}])
assert state == "vetting-failed"
def test_a_pending_retry_outranks_the_failure_it_retries():
assert vetting_state([{"vetting_status": "FAILED"},
{"vetting_status": "PENDING"}]) == "pending"
def test_success_with_no_score_is_reported_as_a_disagreement():
state, detail = verdict(STANDARD, [{"vetting_status": "SUCCESS"}])
assert state == "vetted-without-score"
assert "disagree" in detail
def test_an_unapproved_brand_is_a_different_report():
assert verdict(dict(STANDARD, status="PENDING"))[0] == "not-approved"
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { verdict, vettingState }
from './twilio-a2p-brand-vetting-audit.mjs';
const STANDARD = { sid: 'BN0123456789', status: 'APPROVED',
brand_type: 'STANDARD', brand_score: null };
test('an approved standard brand with no vetting is the finding', () => {
const [state, detail] = verdict(STANDARD);
assert.equal(state, 'unvetted');
assert.match(detail, /lowest tier/);
});
test('a score of zero is a score', () => {
const [state, detail] = verdict({ ...STANDARD, brand_score: 0 });
assert.equal(state, 'scored');
assert.match(detail, /0/);
});
test('sole proprietor brands are never scored', () => {
assert.equal(verdict({ ...STANDARD, brand_type: 'SOLE_PROPRIETOR' })[0],
'not-eligible');
});
test('low volume standard is not reported either', () => {
assert.equal(verdict({ ...STANDARD, brand_type: 'LOW_VOLUME_STANDARD' })[0],
'not-eligible');
});
test('the skip flag is named when nothing was ever vetted', () => {
const [state, detail] = verdict({ ...STANDARD, skip_automatic_sec_vet: true });
assert.equal(state, 'vetting-skipped');
assert.match(detail, /skip_automatic_sec_vet/);
});
test('a failed vetting record explains the null score', () => {
assert.equal(verdict(STANDARD, [{ vetting_status: 'FAILED' }])[0],
'vetting-failed');
});
test('a pending retry outranks the failure it retries', () => {
assert.equal(vettingState([{ vetting_status: 'FAILED' },
{ vetting_status: 'PENDING' }]), 'pending');
});
test('success with no score is reported as a disagreement', () => {
const [state, detail] = verdict(STANDARD, [{ vetting_status: 'SUCCESS' }]);
assert.equal(state, 'vetted-without-score');
assert.match(detail, /disagree/);
});
test('an unapproved brand is a different report', () => {
assert.equal(verdict({ ...STANDARD, status: 'PENDING' })[0], 'not-approved');
});
FAQ
What is a brand score and what does it change?
It is an external secondary vetting score from 0 to 100, produced by a third-party provider such as Aegis. Messages per second toward AT&T, T-Mobile and Verizon scale with it, so a brand without one is given the lowest throughput tier and some campaign types are refused outright.
My brand is APPROVED. Why is there no score?
Because approval and scoring are separate outcomes. Approval says the business is real; the score says how much the carriers will let it send. Either skip_automatic_sec_vet was set when the brand was created, or a vetting record exists and did not succeed. The Vettings subresource distinguishes the two.
Do Sole Proprietor brands need vetting?
No. Sole Proprietor and Low-Volume Standard brands are not scored at all and their throughput is fixed by use case, so a null brand_score on those is the documented behaviour. Requesting vetting for them spends the fee for nothing.
Is brand_score of 0 the same as no score?
No, and confusing them is the expensive mistake here. Zero is the bottom of the scale and a real answer from a vetting that ran. Null means no vetting produced an answer. A check written as a truthiness test reports the first as the second and sends somebody to buy a vetting they already have.
Can I request vetting more than once?
You can, and each request is charged. That is why a SUCCESS vetting record alongside a null brand_score is reported as a disagreement to investigate rather than as a reason to request another one: the vetting already happened and paying twice will not change the field.
Related field notes
- A brand at FAILED blocks every campaign
- A SUSPENDED brand suspends every campaign under it
- A campaign parked at IN_PROGRESS is not live
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.
- BrandVetting resource — Twilio Docs
- BrandRegistration resource — Twilio Docs
- Troubleshooting and rectifying A2P Standard and LVS brands — Twilio Docs
- A2P 10DLC overview — Twilio Docs
If your setup is misbehaving in a way this note does not cover, message me on LinkedIn with what you are seeing.