Build a Lookup form with FastAPI
Check a destination, display its returned normalized number or original email address, and choose a next step. This local example calls the real Bird Lookup API; it does not send a verification code.
Prepare the application
Create a key with the Lookup scope and review Lookup pricing. Phone base lookups are billed; this example also requests the score property, which is billed when answered. Email lookup results are billed as described in the email guide. Use destinations you control.
Create a Python 3.10+ virtual environment and install messagebird-sdk, fastapi and uvicorn. Export BIRD_API_KEY, save the server as app.py, and run uvicorn app:app --host 127.0.0.1 --port 8000. Open http://127.0.0.1:8000/lookup.
Add the server handler
Codebeispiel
import logging
import os
from pathlib import Path
from contextlib import asynccontextmanager
from uuid import UUID
from bird import AsyncBird
from fastapi import FastAPI, Header, HTTPException, Response
from fastapi.responses import HTMLResponse
from pydantic import BaseModel, Field
@asynccontextmanager
async def lifespan(app):
async with AsyncBird(api_key=os.environ["BIRD_API_KEY"]) as client:
app.state.bird = client
yield
app = FastAPI(lifespan=lifespan)
class Destination(BaseModel):
kind: str
value: str = Field(min_length=1, max_length=254)
@app.post("/lookup")
async def lookup(body: Destination, response: Response, idempotency_key: str = Header()):
import re
response.headers["Cache-Control"] = "no-store"
try:
UUID(idempotency_key)
except ValueError:
raise HTTPException(400, "Supply an Idempotency-Key UUID.")
if body.kind == "phone" and not re.fullmatch(r"\+[1-9]\d{6,14}", body.value):
raise HTTPException(400, "Use an international number beginning with + and its country code.")
if body.kind not in {"phone", "email"} or (body.kind == "email" and len(body.value) < 3):
raise HTTPException(400, "Choose phone or email.")
try:
options = {"idempotency_key": idempotency_key}
if body.kind == "phone":
result = await app.state.bird.lookup.phone_number(phone_number=body.value, type=["score"], options=options)
return {
"phone_number": result.phone_number,
"line_type": result.line_type,
"country": result.country_code,
"score": result.score.value if result.score is not None and result.score.status == "ok" else None,
"score_status": result.score.status if result.score is not None else "not_requested",
"next_action": "verify_destination" if result.line_type == "mobile" else "offer_channel_choice",
}
result = await app.state.bird.lookup.email(email=body.value, options=options)
action = {"typo": "ask_about_suggestion", "undeliverable": "ask_for_another_address", "valid": "continue_to_verification"}.get(result.result, "review_result")
return {"email": result.email, "result": result.result, "delivery_confidence": result.delivery_confidence, "suggestion": result.did_you_mean, "next_action": action}
except Exception:
logging.exception("Lookup could not be confirmed")
raise HTTPException(503, "Lookup could not be confirmed. Keep this request key while investigating.")
@app.get("/lookup", response_class=HTMLResponse)
async def form():
return HTMLResponse(HTML, headers={"Cache-Control": "no-store"})
HTML = Path("lookup-form.html").read_text()Add the form
Save lookup-form.html in the application working directory. The form keeps one request key while retrying the same input. Changing the input starts another lookup. Reloading the page discards the in-memory key, so keep an unresolved attempt open while investigating it.
Codebeispiel
<!doctype html>
<html lang="en">
<meta charset="utf-8" /><meta name="viewport" content="width=device-width" /><title>
Bird Lookup example
</title>
<body>
<main>
<h1>Check a destination</h1>
<p>This local example performs a billed lookup. Use a destination you control.</p>
<form>
<label
>Kind
<select name="kind">
<option value="phone">Phone number</option>
<option value="email">Email address</option>
</select></label
>
<label>Destination <input name="value" required autocomplete="off" /></label
><button>Look up</button>
</form>
<pre role="status"></pre>
</main>
<script>
const form = document.querySelector("form"),
output = document.querySelector("pre");
let attempt;
form.addEventListener("submit", async (event) => {
event.preventDefault();
const body = { kind: form.elements.kind.value, value: form.elements.value.value };
const fingerprint = JSON.stringify(body);
if (!attempt || attempt.fingerprint !== fingerprint)
attempt = { fingerprint, key: crypto.randomUUID() };
form.querySelector("button").disabled = true;
try {
const response = await fetch(location.pathname, {
method: "POST",
headers: { "Content-Type": "application/json", "Idempotency-Key": attempt.key },
body: fingerprint,
});
output.textContent = JSON.stringify(await response.json(), null, 2);
} catch {
output.textContent =
"Request outcome unknown. Retry without changing the destination or reloading this page.";
} finally {
form.querySelector("button").disabled = false;
}
});
</script>
</body>
</html>Interpret the response
The application requires an explicit international phone number. It does not guess a country from a local number. Bird returns phone_number in E.164 form; display that value for confirmation before creating a verification.
The optional score has its own status. An unanswered score is returned as null here, with its actual status beside it. The sample routing policy offers verification for a mobile line and a channel choice for other line types; these are application choices, not a claim that line type proves reachability or ownership.
For email, preserve the address as entered. A typo offers the returned suggestion for confirmation; it never silently changes the address. undeliverable asks for another address. Unknown, neutral and risky results use the review path. Even valid does not guarantee delivery or establish consent.
Connect the verified customer action
After the customer confirms the destination, bind it to the authenticated application session and follow the Verify application flow. Do not accept a browser-supplied success flag as verification.
This is a local teaching application. Before exposing it, add application authentication, per-customer request limits, durable request-key storage and a policy for personal-data retention. The example returns 503 on upstream failure; inspect the server error and preserve the same key/body while resolving uncertainty. See idempotency.
Next steps
Verwandte Ressourcen
Weiter mit der Dokumentation, Anleitungen und Beispielen zu diesem Thema. Die Ressourcen sind auf Englisch.
Anleitung ansehenPhone number lookup: check a number before you sendDie Funktion erkundenLookupImplementierungsleitfadenLookup overview
Implementierungs-Briefing erhalten