Skip to main content
This guide walks the whole path to a working WhatsApp agent — nothing assumed, nothing skipped:
  1. Create the WhatsApp connector with its credential and resources (prebuilt or from scratch)
  2. Write the complete agent with the Channels module
  3. Grab the agent URL and enable raw mode (&raw=true)
  4. Pass Meta’s webhook verification handshake
  5. Send a message and watch the agent reply
The flow you are building:

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_messaging permission — it doesn’t expire.
  • A Runflow agent project (rf create) with @runflow-ai/sdk v1.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. whatsapp-acme) — you’ll pass it to meta() in the next step.
Smoke-test the credential before writing any code: on the connector’s detail page, open the send-message resource in the request builder and send a minimal body ({ "messaging_product": "whatsapp", "to": "<your number>", "type": "text", "text": { "body": "ping" } }) with your phone_number_id as the path param. If Graph returns 401, fix the token now — not after deploying.
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.
main.ts
The handshake module. Three details matter here: a request without 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
The Agent — instructions, model and conversation memory:
agent/agent.ts
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:
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.
If tsc fails with TS2307: Cannot find module '@runflow-ai/sdk/channels' ... under your current 'moduleResolution' setting, set "moduleResolution": "bundler" in your tsconfig.json. Older rf create scaffolds ship "moduleResolution": "node", which cannot resolve the SDK’s subpath exports.
Deploy it:
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 &raw=true:
Why raw mode matters here: by default the endpoint wraps your agent’s return value in a JSON envelope ({ 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 main returned — a string goes out as text/plain, unquoted
  • Execution metadata moves to response headers (X-Runflow-Execution-Id, X-Runflow-Duration-Ms, …)
Without &raw=true the handshake will fail — this is the most common mistake in this whole flow.
The token in the URL is your agent’s access token. The full URL is a secret — anyone holding it can execute your agent.
You can verify the handshake yourself before involving Meta:
If that prints 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_TOKEN value from meta-handshake.ts
Click Verify and save. Meta sends the 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 fetchMedia and the get-media-url resource — 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