Lesson 05 · WhatsApp Business API for your app

Onboarding & template registration with Twilio

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.

~13 min Knowledge + code Builds on: Lesson 04
The two things this lesson makes real A. Org onboarding & verification — one self-service button (Embedded Signup), where the org creates its own Business Portfolio + WABA and does its own verification. B. Templates — create with the Content API → submit for WhatsApp approval → track status → store the ContentSid per org.

Part A — The easy way to get an org registered

1. Who verifies what (this trips everyone up)

There are two verifications, and they're different. Get this clear and onboarding stops being scary.

VerificationWhoseWhen
Your Business Verification + App Review + Access VerificationYour company (once)Before you can onboard anyone. Unlocks onboarding up to 200 new customers / rolling 7 days.
Each org's Meta Business VerificationThe teaching orgThey do it inside the flow; needed to move that org to full production limits.

[Twilio: Tech Provider overview]

The easy path = Embedded Signup (self-service) You don't collect documents or fill Meta forms for the org. You embed Meta's Embedded Signup in your app; the org clicks "Connect WhatsApp" and, inside Meta's popup, creates or selects its Business Portfolio, creates its WABA, adds its number, and starts its own verification — without leaving your app. Twilio + Meta host all of it. Your job is just the button + the backend handoff.

2. The flow, and where your code plugs in

  1. Org clicks "Connect WhatsApp" — your page launches Meta Embedded Signup (Facebook JS SDK). This is the shared front-end for both paths.
  2. Inside Meta's popup the org signs in, creates/selects Business Portfolio + WABA, adds & verifies its number, sets a display name. Meta returns waba_id + phone_number_id to your page.
  3. Your page POSTs those to your backend, which runs onboard_twilio(...) from Lesson 04 (create subaccount → Senders API → store).
  4. Business verification: the org can start messaging at a limited tier immediately; they complete Meta Business Verification (guided, in Meta's UI) to lift limits. You just reflect status.
# 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
Security note Take 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.

Part B — Template registration handling

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.

3. Create the template (Content API)

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)

4. Submit it for WhatsApp approval

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"

5. Track the status

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
Don't poll — subscribe Twilio Event Streams emits real-time template status + category updates. Subscribe once and let an event flip your DB when Meta approves/rejects, instead of polling in a loop. Poll only as a fallback. [Twilio: template approvals & statuses]

6. Wire approval back into the tenant (closes the Lesson 04 loop)

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()
Full lifecycle, one glance Onboard: Connect button → onboard_twilio → subaccount + sender. Templates: create_billing_template (HX) → submit_for_approval (UTILITY) → Event Stream says approvedmark_template_approved fills config.templatessend_billing_reminder (Lesson 03) can now send it.

Check yourself

Q1. The self-service org onboarding button is powered by:

Q2. Your one-time verification as the ISV unlocks onboarding of:

Q3. Submitting a template for approval always requires a:

Q4. Best way to learn a template got approved:

Ask me: "Show the front-end JS for the Embedded Signup button", "How do I handle a 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.