Log inStart Free Trial
Meta Tech Provider

Put an OpenAI Agents SDK agent on WhatsApp

Set the number's webhook destination to a route in your Python app. For each incoming message, run the agent with Runner.run and a session keyed on the sender's number, then send result.final_output back through the Cloud API with the number's token and phone number id. The WhatsApp Business app keeps working on the same number.

SQLiteSession

The SDK's built-in session, file-backed or in-memory, that remembers earlier turns under a session id.

params["auth"]

MCPServerStreamableHttp accepts an httpx.Auth here, or an Authorization header, for server authentication.

v25.0

The Graph API version used in Meta's current send message examples.

How does a WhatsApp message become an agent run?

Through a route in your own application that Meta calls. Set the number's webhook destination to it, with a long random secret in the path. The route answers Meta's verification GET by returning hub.challenge as plain text, accepts each POST, responds 200 immediately and queues the work, because Meta retries slow responses and you would run the agent twice.

A worker then reads the sender and the text from the payload and calls Runner.run. Pass a session whose id is the sender's number: the SDK stores the conversation under that id, so the next message from the same customer continues where the last one stopped, and two customers never share a history. SQLiteSession is enough to start; the SDK also ships Redis and SQLAlchemy sessions for several workers.

The secret in the URL is the authentication here. Meta signs deliveries with the Tech Provider's app secret, so the signature header cannot be validated by the customer.

Worker: run the agent for one message and send the answer
import os, httpx
from agents import Agent, Runner, SQLiteSession

agent = Agent(name="Front desk", instructions="Answer briefly and politely.")
URL = f"https://graph.facebook.com/v25.0/{os.environ['PHONE_NUMBER_ID']}/messages"

async def handle(sender: str, text: str) -> None:
    session = SQLiteSession(sender, "whatsapp.db")
    result = await Runner.run(agent, text, session=session)
    async with httpx.AsyncClient() as http:
        await http.post(
            URL,
            headers={"Authorization": f"Bearer {os.environ['WA_TOKEN']}"},
            json={"messaging_product": "whatsapp", "to": sender,
                  "type": "text", "text": {"body": result.final_output}},
        )

Why should your code send the reply, not the agent?

Because the one rule that must never be broken is a timing rule, and code enforces it better than a prompt. Meta opens a 24 hour customer service window when a customer writes. Inside it, free text goes through. Outside it, only an approved template does, and free text comes back as error 131047.

A worker that answers the message that just arrived is always inside the window, so sending result.final_output as text is safe by construction. Work that happens later, a follow up the next morning or a reminder, should go through a separate path that sends a template and never free text.

The agent can still have tools. Give it function tools for looking things up, such as an order or a booking, and keep the WhatsApp send in the worker. That also keeps the access token out of anything the model can call with arbitrary arguments.

Can the agent use the EasyCoexistence MCP server?

It can, through MCPServerStreamableHttp, with more setup than a header. The SDK accepts either an Authorization header or an httpx.Auth in params["auth"]. The EasyCoexistence MCP server issues its own OAuth tokens after a browser sign-in, with dynamic client registration and PKCE, so a static key does not exist. The MCP Python SDK ships OAuthClientProvider, an HTTP auth handler that runs that flow with PKCE and token refresh, and that is what goes in params["auth"].

Worth it when the agent should decide for itself: check a number's health, read template status, or choose between send_message and send_template using the rules in the tool descriptions. For a front desk that only answers, the worker above is shorter and has less to go wrong.

Either way, the webhook stays. MCP is request and response, and a customer's message can only reach the agent through the route Meta calls.

Common mistakes

  • Running the agent before returning 200 to Meta. A slow response is retried, and the customer gets two answers.
  • Using one session for every customer. Key it on the sender's number, or histories mix.
  • Letting a delayed follow up send free text. After 24 hours it fails with 131047; send a template instead.
Doing this with EasyCoexistence

Connect the number at easycoexistence.com, set the webhook destination to your route, and read the token and phone number id from the dashboard. From US$ 9 per number per month, falling to US$ 1 as you add numbers, with the first 3 days free.

Frequently asked questions

Does this need the OpenAI Responses API hosted MCP tool?

No. The worker runs the agent locally with Runner.run, and the WhatsApp side is a plain HTTPS call your code makes.

Which Graph API version should I use?

Meta's current examples use v25.0. Pin a version in one place in your code so an upgrade is a one line change.

Can a person take over from the phone?

Yes. Coexistence keeps the WhatsApp Business app working, and a phone reply arrives at your route as an echo event the worker can use to pause the agent.

Does EasyCoexistence store the conversation?

No. Meta delivers to your route directly. Whatever the session stores lives in your own database.

Keep reading

Ready to get started?

Set up WhatsApp Coexistence in minutes, not months. The app keeps working on the phone.

Start Free Trial

Verified on

OpenAI Agents SDK on WhatsApp, Cloud API