- 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
- 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