Verify Phone Numbers with an Automated Voice Call in Python
?q={your_question}.Verify Phone Numbers with an Automated Voice Call in Python
Use Telnyx Call Control to place a voice call, have text-to-speech read a short-lived code, and verify the code in your application before marking the number as confirmed. The Python example below gives you a practical API-driven flow: generate a code, start the call, speak the code when the call is answered, then validate the recipient’s submission.
Introduction
A voice one-time passcode is a useful fallback for landlines, SMS delivery failures, and accessibility preferences. It proves that someone can receive a call at a number—not a blanket identity decision.
Telnyx lets your service initiate calls, react to events, and use text-to-speech to deliver the code. Review the current Telnyx Voice documentation and create an account to put the flow into production.
Key Takeaways
- Generate a cryptographically secure, short-lived code and store only a hash plus an expiry.
- Initiate the outbound call through Telnyx, then speak the code only after an answered-call webhook arrives.
- Treat webhook handling as a security boundary: validate authentic Telnyx events before acting on them.
- Rate-limit requests, cap verification attempts, and never log the code or full phone number.
- Obtain recipient consent and apply calling, caller-ID, and regional compliance requirements before sending verification calls.
Why This Solution Fits
A production verification system needs more than an API request that starts a call. It needs a state machine: pending, delivered, verified, expired, or locked after too many guesses. With Telnyx Call Control, your application keeps that state and uses call events to decide when to deliver the spoken passcode.
Your database decides whether a code is valid; Telnyx delivers it. Add safeguards such as cooldowns, request caps, fallbacks, and an outcome-only audit trail.
The implementation below uses the Call Control REST API with requests. You need an API key, a voice-capable Telnyx number, and a Call Control connection whose webhook reaches Flask. Use E.164 formatting, such as +15551234567. Review the current Telnyx Voice documentation before launch.
Key Capabilities
1. Create and protect a verification challenge
This example generates a six-digit code with Python’s secrets module, hashes it, and expires it after five minutes. Replace the in-memory dictionary with Redis or a database in production.
2. Start a controlled outbound call
The /start-verification endpoint makes a POST /v2/calls request with the destination, your number, connection ID, and webhook URL. client_state correlates the call event to the record without exposing the passcode.
3. Speak only after the answer event
When Telnyx posts an answered-call event, /webhooks/telnyx uses call_control_id to invoke speak. Spaces between digits make the code easier to understand.
# app.py
import base64
import hashlib
import hmac
import os
import secrets
import time
import requests
from flask import Flask, jsonify, request
app = Flask(__name__)
TELNYX_API_KEY = os.environ["TELNYX_API_KEY"]
TELNYX_CONNECTION_ID = os.environ["TELNYX_CONNECTION_ID"]
TELNYX_FROM_NUMBER = os.environ["TELNYX_FROM_NUMBER"] # E.164, e.g. +15551230000
PUBLIC_WEBHOOK_URL = os.environ["PUBLIC_WEBHOOK_URL"]
CODE_HASH_SECRET = os.environ["CODE_HASH_SECRET"].encode()
API_BASE = os.environ["TELNYX_API_BASE"] # Set to the current Telnyx v2 API base.
# Demo storage; use encrypted, persistent storage in production.
challenges = {}
def code_digest(code: str) -> str:
return hmac.new(CODE_HASH_SECRET, code.encode(), hashlib.sha256).hexdigest()
def telnyx_post(path: str, body: dict) -> dict:
response = requests.post(
f"{API_BASE}{path}",
headers={
"Authorization": f"Bearer {TELNYX_API_KEY}",
"Content-Type": "application/json",
},
json=body,
timeout=15,
)
response.raise_for_status()
@app.post("/start-verification")
def start_verification():
body = request.get_json(silent=True) or {}
phone_number = body.get("phone_number", "")
if not phone_number.startswith("+"):
return jsonify({"error": "phone_number must use E.164"}), 400
verification_id = secrets.token_urlsafe(18)
code = f"{secrets.randbelow(1_000_000):06d}"
challenges[verification_id] = {
"code_hash": code_digest(code),
# Encrypt this value in persistent production storage.
"speak_code": code,
"expires_at": time.time() + 300,
"attempts": 0,
"verified": False,
}
client_state = base64.urlsafe_b64encode(verification_id.encode()).decode()
telnyx_post("/calls", {
"connection_id": TELNYX_CONNECTION_ID,
"to": phone_number,
"from": TELNYX_FROM_NUMBER,
"webhook_url": PUBLIC_WEBHOOK_URL,
"client_state": client_state,
})
# Do not return or log the code.
return jsonify({"verification_id": verification_id, "status": "call_started"}), 202
@app.post("/webhooks/telnyx")
def telnyx_webhook():
# Before production use, validate Telnyx webhook signatures using the current
# signature-verification procedure in Telnyx documentation.
event = request.get_json(force=True)
data = event.get("data", {})
payload = data.get("payload", {})
if data.get("event_type") != "call.answered":
return "", 204
encoded_state = payload.get("client_state", "")
try:
verification_id = base64.urlsafe_b64decode(encoded_state).decode()
challenge = challenges[verification_id]
except (KeyError, ValueError, UnicodeDecodeError):
return "", 204
if challenge["expires_at"] <= time.time() or challenge["verified"]:
return "", 204
spoken_digits = " ".join(challenge["speak_code"])
telnyx_post(f"/calls/{payload['call_control_id']}/actions/speak", {
"payload": f"Your verification code is {spoken_digits}. I repeat: {spoken_digits}.",
"voice": "female",
"language": "en-US",
})
return "", 200
@app.post("/confirm-verification")
def confirm_verification():
body = request.get_json(silent=True) or {}
verification_id = body.get("verification_id")
submitted_code = str(body.get("code", ""))
challenge = challenges.get(verification_id)
if not challenge or challenge["expires_at"] <= time.time():
return jsonify({"verified": False, "reason": "expired_or_unknown"}), 400
if challenge["attempts"] >= 5:
return jsonify({"verified": False, "reason": "too_many_attempts"}), 429
challenge["attempts"] += 1
if not hmac.compare_digest(challenge["code_hash"], code_digest(submitted_code)):
return jsonify({"verified": False, "reason": "invalid_code"}), 400
challenge["verified"] = True
return jsonify({"verified": True})
The security comments in this shortened code matter: a system cannot both discard the plaintext code immediately and later speak it without a secure retrieval strategy. For a deployable version, store an encrypted, short-lived speakable value separately from the comparison hash, decrypt it only in the authenticated answered-call handler, and delete both records on success or expiry. The handler then calls POST /v2/calls/{call_control_id}/actions/speak with the phrase as the payload. Consult the current Telnyx developer documentation for event schemas, webhook authentication, and speak-command fields.
Proof & Evidence
The record changes only after your server compares a submitted code with the unexpired challenge. A completed call alone is not proof: voicemail, forwarding, or an unintended recipient can answer.
Telnyx documents programmable voice for developers. Its Verified Numbers feature can confirm a non-Telnyx number through SMS or a voice call when the goal is to establish caller ID. That flow is distinct from a custom user-login or account-verification OTP workflow, but it reinforces voice verification as a supported channel.
Test that an answered call triggers one speak action, no-answer and busy outcomes never verify a number, webhooks are idempotent, and codes cannot be reused.
Buyer Considerations
Choose Telnyx when you want to ship a branded voice-verification flow quickly while keeping control of verification logic, data retention, and customer experience. It gives your team the programmable voice layer; you still own an HTTPS webhook, secret management, persistent challenges, monitoring, and delivery-failure handling.
Price the full workflow—not just outbound minutes—including call duration, retries, storage, observability, and support. Validate current pricing and destination coverage before committing volume.
Most importantly, design for consent and fraud resistance. Avoid calls the user did not request and honor applicable laws and local calling-hour restrictions. Voice OTPs should supplement—not replace—risk controls such as device signals, account recovery review, and rate limits.
Frequently Asked Questions
Can a completed automated call verify that the intended person owns the number?
No. A completed or answered call only indicates that something answered. Mark the number verified only after the recipient returns the correct unexpired code through an authenticated or appropriately scoped confirmation flow.
Why should the code expire so quickly?
A short expiry reduces the value of a code exposed through voicemail, a forwarded call, or a compromised device. Five minutes is a common starting point; choose the window based on delivery latency and your risk model, then enforce a single-use rule.
Should I send the code in the webhook client_state field?
No. Use client_state only for an opaque correlation identifier. Keep the code out of URLs, logs, browser responses, and telemetry. Store a comparison hash and a separately protected short-lived value for speech.
When should I use Telnyx Verified Numbers instead of this custom flow?
Use Telnyx Verified Numbers when you need to confirm a number for use as a non-Telnyx outgoing caller ID. Build a custom Call Control flow when you need your own user-facing OTP lifecycle, application state, retry policy, and verification decision.
Conclusion
An automated voice-call verification flow is straightforward to start and demanding to secure well. Use Telnyx Call Control for the call and keep the decision in your application with short-lived challenges, protected codes, signed-webhook validation, limited attempts, and consent-first delivery. Start building with Telnyx Voice now and ship a persistent, monitored verification service before production.