skills/twilio/twilio-lookup/SKILL.md
Phone intelligence: number validation, carrier lookup, caller ID, line type mobile/landline/VoIP, CNAM
npx skillsauth add alphaonedev/openclaw-graph twilio-lookupInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
twilio-lookup enables production-grade phone intelligence workflows using Twilio Lookup: validating phone numbers, normalizing to E.164, retrieving carrier metadata, line type (mobile/landline/VoIP), and caller ID / CNAM (where supported). Engineers use this to:
This guide assumes you are integrating Lookup into a larger Twilio production stack (Messaging/Voice/Verify/Studio/SendGrid), with strong error handling, rate limiting, and security controls.
ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxCreate API Key:
lookups.twilio.com (and api.twilio.com for auth/other services).ECONNRESET/handshake failures.+14155552671) and validate plausibility.line_type_intelligence (line type + carrier)caller_name (CNAM-like data where available)Typical production flow:
phone string.e164, country_code, national_formatline_type, carrier_name, mobile_country_code, mobile_network_codecaller_name (if used)lookup_timestamp, lookup_version, risk_flagsfields=line_type_intelligence adds extra lookup work; cache results.fields=caller_name can be slower and less reliable; use only when needed.Repository: https://github.com/twilio/twilio-python
PyPI: pip install twilio · Supported: Python 3.7–3.13
from twilio.rest import Client
client = Client()
# Basic lookup
phone = client.lookups.v2.phone_numbers("+15558675309").fetch()
print(phone.country_code, phone.phone_number)
# With add-ons
phone = client.lookups.v2.phone_numbers("+15558675309").fetch(
fields=["line_type_intelligence", "caller_name", "sim_swap"]
)
print(phone.line_type_intelligence) # {"type": "mobile", "error_code": null}
print(phone.caller_name) # {"caller_name": "Alice", "error_code": null}
Source: twilio/twilio-python — lookups
curl -sSL https://twilio-cli-prod.s3.amazonaws.com/twilio-cli-linux-x86_64.tar.gz -o /tmp/twilio.tar.gz
sudo tar -xzf /tmp/twilio.tar.gz -C /usr/local/bin twilio
twilio --version
curl -sSL https://twilio-cli-prod.s3.amazonaws.com/twilio-cli-linux-x86_64.tar.gz -o /tmp/twilio.tar.gz
sudo tar -xzf /tmp/twilio.tar.gz -C /usr/local/bin twilio
twilio --version
brew update
brew install twilio/brew/twilio
twilio --version
Preferred: API Key
twilio login --apikey YOUR_API_KEY_SID --apisecret 'your_api_key_secret' --profile prod
twilio profiles:list
Alternative: Account SID + Auth Token
twilio login --sid YOUR_ACCOUNT_SID --token 'your_auth_token' --profile prod
node --version
npm --version
mkdir -p twilio-lookup-demo-node && cd twilio-lookup-demo-node
npm init -y
npm install [email protected] [email protected]
python3 --version
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip==24.0
pip install twilio==9.4.1 httpx==0.27.0
Set in your shell (dev) or secret manager (prod):
export TWILIO_ACCOUNT_SID="YOUR_ACCOUNT_SID"
export TWILIO_AUTH_TOKEN="your_auth_token"
# or API key auth:
export TWILIO_API_KEY_SID="YOUR_API_KEY_SID"
export TWILIO_API_KEY_SECRET="your_api_key_secret"
curl -sS https://lookups.twilio.com/ -I | head
Expect HTTP/2 404 or similar (root path not found is fine); the goal is TLS connectivity.
countryCode when input is not E.164.Key output fields:
phone_number (E.164)national_formatcountry_codevalid (v1) / v2 semantics vary; treat as “lookup succeeded” + parse results.Use fields=line_type_intelligence to retrieve:
line_type (e.g., mobile, landline, voip, nonFixedVoip, tollFree)carrier_namemobile_country_code (MCC)mobile_network_code (MNC)error_code (carrier-specific issues)Production uses:
mobile/nonFixedVoip depending on policy.voip for fraud scoring (common in account takeovers).Use fields=caller_name (availability varies by country and number type).
Typical use:
Do not treat CNAM as authoritative identity.
Combine Lookup results with:
21211 invalid To and 30003 unreachable.Twilio CLI command names can vary by version/plugins. In CLI 5.19.0, Lookup is typically available via the api surface.
twilio api:lookups:v2:phone-numbers:fetch --phone-number "+14155552671" --profile prod
Flags:
--phone-number (string, required): E.164 or raw input.--profile (string): Twilio CLI profile name.twilio api:lookups:v2:phone-numbers:fetch \
--phone-number "+14155552671" \
--fields "line_type_intelligence,caller_name" \
--profile prod
Flags:
--fields (comma-separated): line_type_intelligence, caller_nametwilio api:lookups:v2:phone-numbers:fetch \
--phone-number "4155552671" \
--country-code "US" \
--fields "line_type_intelligence" \
--profile prod
Flags:
--country-code (ISO-3166-1 alpha-2): e.g., US, CA, GBIf your CLI build does not expose api:lookups:*, use direct HTTPS (below) or helper libraries.
Twilio Lookup v2 endpoint pattern:
GET https://lookups.twilio.com/v2/PhoneNumbers/{PhoneNumber}?Fields=...&CountryCode=...curl -sS -u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" \
"https://lookups.twilio.com/v2/PhoneNumbers/+14155552671" | jq
curl -sS -u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" \
"https://lookups.twilio.com/v2/PhoneNumbers/+14155552671?Fields=line_type_intelligence,caller_name" | jq
curl -sS -u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" \
"https://lookups.twilio.com/v2/PhoneNumbers/4155552671?CountryCode=US&Fields=line_type_intelligence" | jq
Notes:
+ if your tooling mishandles it (%2B14155552671).SK...:secret basic auth.import twilio from "twilio";
const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
const res = await client.lookups.v2
.phoneNumbers("+14155552671")
.fetch();
console.log(res.phoneNumber, res.countryCode, res.nationalFormat);
import twilio from "twilio";
const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
const res = await client.lookups.v2
.phoneNumbers("+14155552671")
.fetch({ fields: "line_type_intelligence,caller_name" });
console.log({
e164: res.phoneNumber,
lineType: res.lineTypeIntelligence?.type,
carrier: res.lineTypeIntelligence?.carrier_name,
callerName: res.callerName?.caller_name,
});
Flags/options:
fetch({ fields: "..." }): comma-separated fieldsfetch({ countryCode: "US" }): for national input (if supported by helper version)import twilio from "twilio";
const client = twilio(
process.env.TWILIO_API_KEY_SID,
process.env.TWILIO_API_KEY_SECRET,
{ accountSid: process.env.TWILIO_ACCOUNT_SID }
);
import os
from twilio.rest import Client
client = Client(os.environ["TWILIO_ACCOUNT_SID"], os.environ["TWILIO_AUTH_TOKEN"])
res = client.lookups.v2.phone_numbers("+14155552671").fetch()
print(res.phone_number, res.country_code, res.national_format)
import os
from twilio.rest import Client
client = Client(os.environ["TWILIO_ACCOUNT_SID"], os.environ["TWILIO_AUTH_TOKEN"])
res = client.lookups.v2.phone_numbers("+14155552671").fetch(
fields="line_type_intelligence,caller_name"
)
print({
"e164": res.phone_number,
"line_type": getattr(res, "line_type_intelligence", None),
"caller_name": getattr(res, "caller_name", None),
})
import os
from twilio.rest import Client
client = Client(
os.environ["TWILIO_API_KEY_SID"],
os.environ["TWILIO_API_KEY_SECRET"],
os.environ["TWILIO_ACCOUNT_SID"],
)
Path (Linux):
/etc/openclaw/twilio/lookup.tomlPath (macOS dev):
~/Library/Application Support/OpenClaw/twilio/lookup.tomlExample:
# /etc/openclaw/twilio/lookup.toml
[twilio]
account_sid = "YOUR_ACCOUNT_SID"
auth_mode = "api_key" # "api_key" or "auth_token"
[twilio.api_key]
sid = "YOUR_API_KEY_SID"
secret_env = "TWILIO_API_KEY_SECRET" # secret is read from env at runtime
[lookup]
base_url = "https://lookups.twilio.com"
default_country_code = "US"
default_fields = ["line_type_intelligence"]
timeout_ms = 2500
max_retries = 2
retry_backoff_ms = 200
retry_jitter_ms = 100
[cache]
enabled = true
backend = "redis" # "redis" or "memory"
ttl_seconds = 604800 # 7 days
negative_ttl_seconds = 3600 # cache invalid numbers for 1 hour
[policy]
allow_countries = ["US", "CA"]
deny_line_types = ["landline"]
flag_line_types = ["voip", "nonFixedVoip"]
Production runtime should support:
TWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKEN (if using auth token)TWILIO_API_KEY_SIDTWILIO_API_KEY_SECRETOPENCLAW_TWILIO_LOOKUP_CONFIG=/etc/openclaw/twilio/lookup.tomlExample systemd drop-in:
# /etc/systemd/system/openclaw.service.d/twilio-lookup.conf
[Service]
Environment="OPENCLAW_TWILIO_LOOKUP_CONFIG=/etc/openclaw/twilio/lookup.toml"
Environment="TWILIO_ACCOUNT_SID=YOUR_ACCOUNT_SID"
Environment="TWILIO_API_KEY_SID=YOUR_API_KEY_SID"
Environment="TWILIO_API_KEY_SECRET=/run/secrets/twilio_api_key_secret"
If TWILIO_API_KEY_SECRET points to a file path, your runtime should read file contents (common pattern). If not supported, store the secret directly in the env var via your secret injector.
Path:
/etc/openclaw/redis.conf.d/lookup.conf# /etc/openclaw/redis.conf.d/lookup.conf
maxmemory 512mb
maxmemory-policy allkeys-lru
timeout 0
tcp-keepalive 300
Key format recommendation:
lookup:v2:{e164}:{fields_hash}:{country_code}Pipeline:
Example (pseudo):
POST /send_sms
-> lookup(+input, fields=line_type_intelligence)
-> if line_type in deny: 422 "unsupported line type"
-> send SMS via Messaging Service SID MGxxxxxxxx...
-> store message SID + lookup snapshot
Compose with Twilio Messaging production patterns:
voip and your fraud policy disallows VoIP for MFA, require alternate channel (TOTP/email) via Verify custom channels.landline, skip SMS and place a Voice call using TwiML <Say> with Polly voice, or <Dial> to connect.line_type, carrier, country).20429 Too Many Requests.Handle errors as first-class: classify, retry safely, and surface actionable diagnostics.
Message (common):
Authenticate"code": 20003, "message": "Authenticate"Root causes:
accountSid in helper clientFix:
accountSid.lookups.twilio.com with correct basic auth.Message:
"code": 20429, "message": "Too Many Requests"Root causes:
Fix:
Message:
The 'To' number +1415555 is not a valid phone number.Root causes:
Fix:
countryCode for national inputs.Message:
Unreachable destination handsetRoot causes:
Fix:
Message (typical):
400 Bad RequestRoot causes:
Fields value (typo)+ in phone numberCountryCode format (must be US, not USA)Fix:
fields against allowlist: line_type_intelligence, caller_name.Message (common JSON):
"status": 404, "message": "The requested resource /PhoneNumbers/... was not found"Root causes:
CountryCode for national format inputFix:
CountryCode if you have user locale.Messages:
ETIMEDOUT, ECONNRESETReadTimeout, ConnectErrorRoot causes:
Fix:
Message:
TypeError: Cannot read properties of undefined (reading 'type')AttributeError: 'PhoneNumberInstance' object has no attribute ...Root causes:
Fix:
res.lineTypeIntelligence?.type.Message:
21610: Message cannot be sent because the recipient has opted out of receiving messages from this number.21614: 'To' number is not a valid mobile numberRoot causes:
Fix:
Masking recommendation:
+1415555****lookups.twilio.comapi.twilio.comchmod 600 for config files with SIDs).Lookup itself is outbound-only, but it composes with Messaging/Voice webhooks:
X-Twilio-Signature) on status callbacks.Expected impact:
Guidelines:
Expected impact:
caller_name unless needed.Example:
line_type_intelligence only.caller_name on-demand.Expected impact:
20429 responses.Recommended:
Expected impact:
07700 900123, you need country context (GB).nonFixedVoip vs voip.nonFixedVoip for low-risk notificationsnational_format for display only; do not use it for sending.Node.js example with strict policy and caching stub:
import twilio from "twilio";
import pino from "pino";
const log = pino();
const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
const ALLOW_COUNTRIES = new Set(["US", "CA"]);
const DENY_LINE_TYPES = new Set(["landline"]);
export async function normalizePhone({ input, countryCode }) {
const res = await client.lookups.v2
.phoneNumbers(input)
.fetch({ fields: "line_type_intelligence", countryCode });
const e164 = res.phoneNumber;
const cc = res.countryCode;
const lineType = res.lineTypeIntelligence?.type ?? "unknown";
if (!ALLOW_COUNTRIES.has(cc)) {
throw Object.assign(new Error(`country_not_allowed: ${cc}`), { status: 422 });
}
if (DENY_LINE_TYPES.has(lineType)) {
throw Object.assign(new Error(`line_type_not_supported: ${lineType}`), { status: 422 });
}
log.info({ e164, cc, lineType }, "phone_normalized");
return { e164, countryCode: cc, lineType };
}
Decision logic:
Pseudo-implementation:
from twilio.rest import Client
import os
client = Client(os.environ["TWILIO_ACCOUNT_SID"], os.environ["TWILIO_AUTH_TOKEN"])
def route_notification(phone: str):
lookup = client.lookups.v2.phone_numbers(phone).fetch(fields="line_type_intelligence")
e164 = lookup.phone_number
lt = lookup.line_type_intelligence.get("type") if lookup.line_type_intelligence else "unknown"
if lt in ("mobile", "nonFixedVoip"):
msg = client.messages.create(
to=e164,
from_="+15005550006",
body="Your package is arriving today."
)
return {"channel": "sms", "sid": msg.sid}
if lt == "landline":
call = client.calls.create(
to=e164,
from_="+15005550006",
twiml="<Response><Say voice='Polly.Joanna'>Your package is arriving today.</Say></Response>"
)
return {"channel": "voice", "sid": call.sid}
raise ValueError(f"unsupported_line_type: {lt}")
Shell + curl + jq example (simple; production should use a proper worker):
set -euo pipefail
INPUT_CSV="phones.csv" # one phone per line
FIELDS="line_type_intelligence"
COUNTRY="US"
while read -r phone; do
# naive throttle: 5 req/s
sleep 0.2
curl -sS -u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" \
"https://lookups.twilio.com/v2/PhoneNumbers/${phone}?CountryCode=${COUNTRY}&Fields=${FIELDS}" \
| jq -c '{phone_number, country_code, line_type_intelligence}'
done < "$INPUT_CSV"
Backend does Lookup, then triggers Studio:
curl -sS -u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" \
-X POST "https://studio.twilio.com/v2/Flows/FW0123456789abcdef0123456789abcdef/Executions" \
--data-urlencode "To=+14155552671" \
--data-urlencode "From=+15005550006" \
--data-urlencode "Parameters={\"line_type\":\"mobile\",\"carrier\":\"T-Mobile USA, Inc.\"}"
Studio can branch on line_type to choose SMS vs Voice.
Flow:
line_type_intelligencevoip/nonFixedVoip → require TOTP or emailPython sketch:
import os
from twilio.rest import Client
client = Client(os.environ["TWILIO_ACCOUNT_SID"], os.environ["TWILIO_AUTH_TOKEN"])
def start_mfa(phone: str):
lookup = client.lookups.v2.phone_numbers(phone).fetch(fields="line_type_intelligence")
e164 = lookup.phone_number
lt = lookup.line_type_intelligence.get("type") if lookup.line_type_intelligence else "unknown"
if lt in ("voip", "nonFixedVoip"):
return {"action": "require_totp", "reason": f"line_type={lt}"}
verification = client.verify.v2.services("YOUR_VERIFY_SERVICE_SID") \
.verifications.create(to=e164, channel="sms")
return {"action": "verify_sms", "sid": verification.sid, "status": verification.status}
Only fetch caller_name when an agent opens a profile:
curl -sS -u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" \
"https://lookups.twilio.com/v2/PhoneNumbers/+14155552671?Fields=caller_name" | jq '.caller_name'
| Task | Command / API | Key flags / params |
|---|---|---|
| Basic lookup | GET /v2/PhoneNumbers/{num} | none |
| Lookup with line type + carrier | GET ...?Fields=line_type_intelligence | Fields |
| Lookup with caller name | GET ...?Fields=caller_name | Fields |
| National input parsing | .../{num}?CountryCode=US | CountryCode |
| CLI fetch | twilio api:lookups:v2:phone-numbers:fetch | --phone-number, --fields, --country-code, --profile |
| Node fetch | client.lookups.v2.phoneNumbers(num).fetch() | { fields, countryCode } |
| Python fetch | client.lookups.v2.phone_numbers(num).fetch() | fields=... |
twilio-core-auth (Account SID/Auth Token or API Key auth patterns)twilio-cli (optional; for debugging and ops workflows)secrets-management (Vault/AWS/GCP secret injection)redis (optional; caching and rate limiting)twilio-messaging (pre-send gating, geo-matching Messaging Services, STOP handling, status webhooks)twilio-voice (fallback routing, TwiML generation, call recording/transcription)twilio-verify (E.164 normalization, VoIP restrictions, fraud guard)twilio-studio (Flow branching based on lookup enrichment)sendgrid-email (fallback channel when phone is invalid/unreachable)libphonenumber (local parsing/formatting; not authoritative for reachability/carrier)numverify / other phone intelligence providers (feature overlap; different coverage/cost)HLR lookup services (carrier/reachability in some regions; different semantics and legality constraints)tools
Root web development: project structure, tooling selection, deployment decisions
development
WebAssembly: Rust/Go/C to WASM, wasm-bindgen, Emscripten, WASM Component Model
development
Vue 3: Composition API script setup, Pinia, Vue Router 4, SFCs, Vite, Nuxt 3
tools
Tailwind CSS 4: utility classes, config, JIT, arbitrary values, darkMode, plugins, shadcn/ui