telnyxdocs.com

Command Palette

Search for a command to run...

Verify Phone Numbers with an Automated Voice Call in Python

Last updated: 9/18/2026

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.