- Create the WhatsApp connector with its credential and resources (prebuilt or from scratch)
- Write the complete agent with the Channels module
- Grab the agent URL and enable raw mode (
&raw=true) - Pass Meta’s webhook verification handshake
- Send a message and watch the agent reply
Prerequisites
- A Meta developer app (developers.facebook.com) with the WhatsApp product added. The API Setup page gives you a test phone number for free.
- The Phone Number ID — shown on WhatsApp → API Setup (it is not the phone number itself).
- An access token. The temporary token from API Setup works for testing (expires in 24h); for anything real, create a System User token in Meta Business Settings with the
whatsapp_business_messagingpermission — it doesn’t expire. - A Runflow agent project (
rf create) with@runflow-ai/sdkv1.5.0+.
1
Create the connector and credential
The channel provider sends every outbound message through a connector, so the Meta token lives server-side — your agent code never sees it. Two ways to get the connector:Whichever path you took, note the instance slug of your connector (e.g.
- Prebuilt connector (recommended)
- Custom connector (from scratch)
In the portal, open Connectors and pick WhatsApp Business Cloud from the prebuilt catalog. The template comes preconfigured with the Graph API base URL (
https://graph.facebook.com/v18.0) and the auth shape — you only fill in the Access Token credential, which is sent as Authorization: Bearer <token> on every call.The prebuilt connector already ships the two resources the channel provider uses:If your WhatsApp connector was created before the
send-message raw passthrough existed, add it yourself with the exact definition from the “Custom connector” tab — the older per-type resources (send-text-message, etc.) are not what the provider uses.whatsapp-acme) — you’ll pass it to meta() in the next step.2
Write the agent (the complete project)
This is the entire agent — four files, all shown in full. Copy them as-is, change the connector slug, phone number id and verify token, and you have a working WhatsApp agent.The entry point does two jobs: answer Meta’s verification handshake and delegate everything else to the channel handler.The handshake module. Three details matter here: a request without The Agent — instructions, model and conversation memory:The turn. The platform may deliver a burst of quick messages as one turn with several events — flatten them into a single message, call the agent once, reply once:Deploy it:
main.ts
hub.* params is a normal event, not an error; the verify token must be checked (otherwise anyone who finds your URL can pass verification); and query values arrive as arrays (one entry per repeated param), so normalize both shapes:meta-handshake.ts
agent/agent.ts
agent/runner.ts
Want real audio support? Resolve the media with
fetchMedia and transcribe it with the SDK — see Channels → Media. On Meta the download URL is Bearer-gated, so plan for a mediaResource that returns the bytes.rf agents deploy always targets staging — you’ll validate there first and promote to production later.3
Grab the agent URL and add raw=true
Open your agent in the portal (Agents → your agent) and copy the endpoint URL from the info panel. It looks like:(Staging and production have different hosts — copy the URL for the environment you deployed to.)Now append Why raw mode matters here: by default the endpoint wraps your agent’s return value in a JSON envelope (If that prints
&raw=true:{ message, metadata, ... }). Meta’s webhook validator expects the bare challenge string back — 12345, not {"message":"12345"}. With raw=true:- The response body is exactly what
mainreturned — a string goes out astext/plain, unquoted - Execution metadata moves to response headers (
X-Runflow-Execution-Id,X-Runflow-Duration-Ms, …)
&raw=true the handshake will fail — this is the most common mistake in this whole flow.You can verify the handshake yourself before involving Meta:12345 (and nothing else), you’re ready. Try it with a wrong hub.verify_token too — it must not return the challenge.4
Configure the webhook in Meta
In your Meta app: WhatsApp → Configuration → Webhook → Edit:
- Callback URL — your agent URL including
&raw=true - Verify token — the exact
VERIFY_TOKENvalue frommeta-handshake.ts
GET handshake; your agent echoes the challenge; Meta accepts.Then, still on the Configuration page, subscribe to the messages webhook field — without this subscription Meta verifies fine but never delivers a single message (the second most common mistake).The verification handshake runs a real execution — you’ll see it in Observability, which is also a nice first confirmation that the wiring works.
5
Send a message
Message the WhatsApp number (on a test number, add your phone to the recipient allowlist on API Setup first). You should get the agent’s reply within a few seconds.Behind the scenes: Meta POSTs the webhook → the
meta() provider parses it into a text event with userId = the sender’s number (Brazilian numbers normalized automatically) → your runTurn runs → reply.text(...) is rendered to the Graph API format and POSTed through your connector’s send-message resource.Check Observability → Executions to watch the full trace, including the outbound connector call.Troubleshooting
Going to production
Using the Conversation Hub addon? After the first connection, new numbers registered on your WABA are imported automatically by the periodic sync — no need to repeat this setup per number. Set a default agent for the WABA so imported numbers route immediately: see Channels → Automatic number sync.
- Promote the agent to production and update Meta’s callback URL to the production endpoint (
executor.runflow.ai) — or use a separate Meta app per environment. - This direct URL is synchronous: Meta waits while your turn runs, and retries on timeout — there is no dedup or burst batching on this path. It’s perfect to get live and for moderate traffic; for high-volume production, front the agent with an HTTP trigger (debounce/coalescing delivers the burst as
input.events[]) — the channel handler works unchanged on both paths. - Media: inbound audio/images arrive as media events; resolve them with
fetchMediaand theget-media-urlresource — see Channels → Media.
Next Steps
Channels
Buttons, media, typing indicators, multi-channel routing
Connectors
How connector instances, credentials and resources work
Memory
Persist the conversation across messages
Observability
Trace every execution, including connector calls