Lesson 05 · WhatsApp Business API for your app
The two operational things you asked about: the easiest way to get an organisation registered and verified, and how to create, submit, and track your message templates — end to end, in Python.
ContentSid per org.
There are two verifications, and they're different. Get this clear and onboarding stops being scary.
| Verification | Whose | When |
|---|---|---|
| Your Business Verification + App Review + Access Verification | Your company (once) | Before you can onboard anyone. Unlocks onboarding up to 200 new customers / rolling 7 days. |
| Each org's Meta Business Verification | The teaching org | They do it inside the flow; needed to move that org to full production limits. |
[Twilio: Tech Provider overview]
waba_id + phone_number_id to your page.onboard_twilio(...) from Lesson 04 (create subaccount → Senders API → store).# routes.py — the FastAPI endpoint your Connect button calls back into
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from .db import get_session
from .onboarding_twilio import onboard_twilio
router = APIRouter()
class ConnectPayload(BaseModel):
org_id: str # your tenant (from the logged-in session, not the browser)
waba_id: str # returned by Embedded Signup
phone_number_id: str # returned by Embedded Signup
wa_number_e164: str # the number the org verified, e.g. "+919812345678"
display_name: str
@router.post("/whatsapp/connect")
def whatsapp_connect(p: ConnectPayload, session = Depends(get_session)):
onboard_twilio(
org_id=p.org_id, waba_id=p.waba_id, phone_number_id=p.phone_number_id,
wa_number_e164=p.wa_number_e164, display_name=p.display_name, session=session,
)
return {"status": "connecting"} # sender is provisioning; goes live async
org_id from the authenticated server session, never from the browser
payload — otherwise one org could connect WhatsApp onto another org's tenant. Only the Meta-returned
ids come from the client.
A template is code you deploy to Meta for review. With Twilio you never talk to Meta directly — you use the Content API, and Twilio relays approval. Three moves: create, submit, track.
Create it in the org's subaccount so approval binds to that org's WABA. Use {{1}}
placeholders for the child's name, amount, due date.
# templates.py
from twilio.rest import Client
BILLING_BODY = (
"Hi {{1}}, a friendly reminder that the monthly fee of ₹{{2}} "
"for {{3}} is due by {{4}}. Reply here if you have any questions."
)
def create_billing_template(sub_client: Client) -> str:
content = sub_client.content.v1.contents.create(
friendly_name="billing_reminder_v1",
language="en",
variables={"1": "Aarav's parent", "2": "1500", "3": "Aarav", "4": "5 Oct"}, # sample defaults
types={"twilio/text": {"body": BILLING_BODY}},
)
return content.sid # -> "HX…" (the ContentSid)
Submitting requires a category. Yours is UTILITY (Lesson 02 — 7.5×
cheaper than marketing). Category is declared here and Meta may re-classify if the content looks promotional.
def submit_for_approval(sub_client: Client, content_sid: str) -> str:
req = sub_client.content.v1.contents(content_sid) \
.approval_requests_whatsapp.create(
name="billing_reminder_v1",
category="UTILITY", # UTILITY | MARKETING | AUTHENTICATION
)
return req.status # -> "received"
Approval is usually minutes (ML-assisted); anything routed to a human can take up to
48h. Statuses: received → pending → approved | rejected
(with rejection_reason).
def template_status(sub_client: Client, content_sid: str) -> str:
req = sub_client.content.v1.contents(content_sid).approval_requests.fetch()
return req.whatsapp.status # received | pending | approved | rejected
When a template is approved, record its ContentSid in that org's
config.templates map — the exact field TwilioProvider reads when sending.
# on approval (from an Event Streams webhook, or a status poll)
def mark_template_approved(org_id: str, logical_name: str, content_sid: str, session):
row = session.get(OrgWhatsApp, org_id)
templates = dict(row.config.get("templates", {}))
templates[logical_name] = content_sid # "billing_reminder_v1" -> "HX…"
row.config = {**row.config, "templates": templates}
session.commit()
onboard_twilio → subaccount + sender.
Templates: create_billing_template (HX) → submit_for_approval
(UTILITY) → Event Stream says approved → mark_template_approved fills
config.templates → send_billing_reminder (Lesson 03) can now send it.
rejected template gracefully?", or "What does the Event Streams webhook payload look like?"
Next: Lesson 06 — webhooks (Twilio status callbacks) so you know when a parent actually received the message.
Primary sources: Twilio — Content API quickstart · Template approvals & statuses · Self Sign-up.
Prev: Lesson 04 — The Twilio path · Reference: Glossary · Next: Lesson 06 — webhooks & delivery status.