Facebook Page Messenger channel for OpenClaw — webhook in, Graph API out
The plugin only moves messages. Replies are generated by your OpenClaw agent and model provider (e.g. 9Router) — exactly like the Telegram and Zalo channels.
fb-messenger adds Facebook Page Messenger as a native OpenClaw channel, side by side with Telegram and Zalo. It handles only the plumbing — inbound events and outbound replies — while the OpenClaw agent does the thinking.
📥 Inbound — a public webhook (/webhooks/messenger) receives Meta events, verifies the X-Hub-Signature-256 signature, and dispatches each message to the agent.
📤 Outbound — replies stream back through the Graph API (POST /me/messages), auto-chunked to fit Messenger's limit.
🪄 Self-healing — accepts a User token (auto-exchanged for the Page token), resolves the real Page ID, and auto-subscribes the Page to the webhook on startup — no manual Meta dashboard step.
Rendering diagram...
📦 Requirements
OpenClaw
>= 2026.6.10
Public HTTPS URL
pointing at your OpenClaw gateway (VPS domain, or ngrok/cloudflared for testing). Meta only delivers webhooks over HTTPS.
Meta side
a Facebook Page and a Meta App with the Messenger product enabled.
⬇️ Install
bash
# From ClawHub:
openclaw plugins install clawhub:tuanminhhole/openclaw-fb-messenger
# Update to the latest version later (force re-pull):
openclaw plugins install clawhub:tuanminhhole/openclaw-fb-messenger --force
The ClawHub package ships the prebuilt dist/, so there is nothing to build — install, configure (below), restart the gateway.
💡 Prefer a guided UI? openclaw-setup installs and configures this channel (Page token, webhook) straight from its dashboard.
Under Messenger → Settings, generate a Page Access Token for your Page.
Note your App Secret (App Settings → Basic).
Choose any string as your Verify Token (you'll reuse the same value below).
2. Environment variables
bash
FB_MESSENGER_PAGE_ACCESS_TOKEN=EAAB... # any token — even a short-lived User token from the Graph API Explorer
FB_MESSENGER_APP_ID=1234567890 # Meta App ID — required to mint a never-expiring Page token
FB_MESSENGER_APP_SECRET=xxxxxxxx # App Secret (signature verification + token exchange)
FB_MESSENGER_VERIFY_TOKEN=my-verify-token # any string you choose
🔑 Never-expiring token, automatically. On startup the plugin exchanges whatever you paste (short-lived User token → long-lived User token → permanent Page token) and caches the Page token under .openclaw/credentials/fb-messenger/, so the bot keeps working after the input token expires. Providing FB_MESSENGER_APP_ID + FB_MESSENGER_APP_SECRET is what makes the result permanent.
🔐 Secrets (pageAccessToken, appSecret, verifyToken) are read from the environment first, so they don't need to live in openclaw.json. pageId is optional — the plugin resolves it from the token.
4. Expose a public HTTPS endpoint
Meta only delivers webhooks to a public HTTPS URL, but your gateway listens on a local port (e.g. :18789, often inside Docker). You need a tunnel/reverse proxy in front of it.
⚠️ Use a stable URL. The quick one-liners below (ngrok http 18789, cloudflared tunnel --url …) hand out a random URL that changes every time the tunnel restarts — and each change forces you to re-edit the Callback URL in Meta. Pick one of the permanent options instead so you configure Meta once.
🐳 The tunnel runs on the host, not inside the container — so restarting Docker does not change the URL as long as the tunnel process stays up. The URL only changes if the tunnel itself restarts in ephemeral mode.
Option A — ngrok static domain (free, fastest):
bash
# 1. dashboard.ngrok.com → Domains → claim 1 free static domain, e.g. mybot.ngrok-free.app
# 2. Run bound to that domain (URL never changes):
ngrok http --url=mybot.ngrok-free.app 18789
Verify Token: the same value as FB_MESSENGER_VERIFY_TOKEN
Subscription fields: at minimum messages (and messaging_postbacks if you use buttons).
💡 The plugin auto-subscribes the Page to the app webhook on startup, so you can skip the "Edit Page Subscriptions" step. You only declare the Callback URL + Verify Token above.
🔎 Sanity check before pasting into Meta — the GET handshake should echo the challenge: