Webhooks
Receive real-time HTTP notifications when events occur in MessengerFlow.
Webhooks
Webhooks are available on the Momentum plan and above.
Webhooks deliver real-time HTTP POST requests to your server whenever something happens in MessengerFlow — a message is sent, a lead replies, a campaign finishes, or an account status changes.
How It Works
- You register an HTTPS endpoint and choose which events to subscribe to
- When a subscribed event occurs, MessengerFlow sends a signed HTTP POST to your endpoint
- Your server verifies the signature, processes the payload, and returns a
2xxresponse - If delivery fails, the system retries with exponential backoff up to 5 times
Setting Up
Register an endpoint
- Go to Settings > Developers > Webhooks
- Click Add Endpoint
- Enter your HTTPS URL
- Select the events you want to receive
- Click Create
- Copy the signing secret — it is shown only once and cannot be retrieved later
You can also register endpoints programmatically via the API.
Endpoint requirements
Your endpoint must:
- Be accessible over HTTPS (HTTP is not accepted)
- Accept POST requests with a JSON body
- Return a
2xxstatus code within 10 seconds - Be idempotent — the same event may be delivered more than once in rare cases
Events
Message events
| Event | Trigger |
|---|---|
message.sent | A campaign message was successfully delivered to a lead |
message.failed | A campaign message failed to deliver |
Conversation events
| Event | Trigger |
|---|---|
conversation.created | A new conversation was detected during inbox sync |
conversation.reply | An inbound reply was detected during inbox sync |
conversation.booked | A conversation was marked as booked in the app or via API |
Account events
| Event | Trigger |
|---|---|
account.status_changed | An account status flag changed (suspended, rate limited, invalid credentials, etc.) |
Campaign events
| Event | Trigger |
|---|---|
campaign.completed | All leads in a campaign have been processed |
Payload Format
Every webhook delivery is a JSON POST with this structure:
{
"event": "event.type",
"timestamp": "2026-02-10T14:30:00.000Z",
"data": { }
}
| Field | Type | Description |
|---|---|---|
event | string | The event type that triggered this delivery |
timestamp | string | ISO 8601 timestamp of when the event occurred |
data | object | Event-specific payload (see below) |
message.sent
{
"event": "message.sent",
"timestamp": "2026-02-10T14:30:00.000Z",
"data": {
"campaign_id": "a1b2c3d4-...",
"lead_id": "e5f6a7b8-...",
"account_id": "c9d0e1f2-...",
"message": "Hey John, I noticed your page and wanted to reach out..."
}
}
message.failed
{
"event": "message.failed",
"timestamp": "2026-02-10T14:30:00.000Z",
"data": {
"campaign_id": "a1b2c3d4-...",
"lead_id": "e5f6a7b8-...",
"account_id": "c9d0e1f2-...",
"error": "Account rate limited by Facebook"
}
}
conversation.created
{
"event": "conversation.created",
"timestamp": "2026-02-10T14:30:00.000Z",
"data": {
"conversation_id": 12345,
"account_id": "c9d0e1f2-...",
"recipient_id": "100001234567890"
}
}
conversation.reply
{
"event": "conversation.reply",
"timestamp": "2026-02-10T14:30:00.000Z",
"data": {
"conversation_id": 12345,
"account_id": "c9d0e1f2-...",
"recipient_id": "100001234567890",
"message": "Hi! Yes, I'd love to learn more."
}
}
conversation.booked
{
"event": "conversation.booked",
"timestamp": "2026-02-10T14:30:00.000Z",
"data": {
"conversation_id": "uuid",
"booked_at": "2026-02-15T10:00:00.000Z"
}
}
account.status_changed
Fired when any account status flag is modified, either by the automation worker or manually through the app.
{
"event": "account.status_changed",
"timestamp": "2026-02-10T14:30:00.000Z",
"data": {
"account_id": "c9d0e1f2-...",
"email": "[email protected]",
"field": "banned",
"value": true
}
}
The field indicates which status changed. Possible values:
| Field | Meaning |
|---|---|
banned | Account suspended by Facebook |
rate_limited_until | Rate limit applied or cleared |
invalid_credentials | Password no longer valid |
invalid_2fa | 2FA secret incorrect |
error | Generic error flag |
unsupported_auth_flow | Facebook requires unsupported verification |
requires_pin | Account needs PIN verification |
enabled | Account enabled or disabled by user |
campaign.completed
{
"event": "campaign.completed",
"timestamp": "2026-02-10T14:30:00.000Z",
"data": {
"campaign_id": "a1b2c3d4-...",
"name": "Q1 Outreach"
}
}
Signature Verification
Every delivery includes headers that let you verify the request is genuinely from MessengerFlow:
| Header | Description |
|---|---|
X-MessengerFlow-Event | Event type (e.g. message.sent) |
X-MessengerFlow-Signature | HMAC signature: sha256=<hex> |
X-MessengerFlow-Delivery | Unique delivery ID (use for idempotency) |
X-MessengerFlow-Timestamp | Unix timestamp of the request |
The signature is computed as:
HMAC-SHA256(signing_secret, timestamp + "." + raw_json_body)
Node.js example
const crypto = require('crypto');
function verifySignature(secret, timestamp, rawBody, signature) {
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return signature === `sha256=${expected}`;
}
app.post('/webhooks/messengerflow', (req, res) => {
const signature = req.headers['x-messengerflow-signature'];
const timestamp = req.headers['x-messengerflow-timestamp'];
const rawBody = JSON.stringify(req.body);
if (!verifySignature(process.env.WEBHOOK_SECRET, timestamp, rawBody, signature)) {
return res.status(401).send('Invalid signature');
}
const { event, data } = req.body;
switch (event) {
case 'message.sent':
console.log(`Message delivered to lead ${data.lead_id}`);
break;
case 'conversation.reply':
console.log(`New reply from ${data.recipient_id}: ${data.message}`);
break;
case 'conversation.booked':
// Push to your CRM
break;
}
res.status(200).send('OK');
});
Python example
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "your_signing_secret"
def verify_signature(secret, timestamp, body, signature):
expected = hmac.new(
secret.encode(),
f"{timestamp}.{body}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, f"sha256={expected}")
@app.route("/webhooks/messengerflow", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-MessengerFlow-Signature", "")
timestamp = request.headers.get("X-MessengerFlow-Timestamp", "")
raw_body = request.get_data(as_text=True)
if not verify_signature(WEBHOOK_SECRET, timestamp, raw_body, signature):
abort(401)
payload = request.get_json()
event = payload["event"]
data = payload["data"]
if event == "conversation.reply":
print(f"New reply from {data['recipient_id']}")
elif event == "conversation.booked":
# Push to your CRM
pass
return "OK", 200
Retry Logic
When a delivery fails (non-2xx response or timeout), the system retries with increasing delays:
| Attempt | Retry after |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 24 hours |
After 5 failed attempts, the delivery is marked as failed and the endpoint's failure counter increments.
Auto-disable: If an endpoint accumulates 5 consecutive delivery failures, it is automatically disabled to prevent further wasted requests. You can re-enable it from Settings > Developers > Webhooks after fixing the issue.
Testing
Every endpoint has a Send Test button that delivers a ping event with a valid HMAC signature. Use it to confirm:
- Your endpoint is publicly reachable
- HTTPS is working correctly
- Signature verification passes
- Your server returns a
2xxresponse
You can also send a test via the API.
Best Practices
- Return 200 immediately — acknowledge receipt before doing any heavy processing. Queue the payload for async handling in your application.
- Verify every signature — always validate the HMAC to confirm the request is from MessengerFlow, not a malicious third party.
- Use the delivery ID for idempotency — in rare cases, the same event may be delivered more than once. Use the
X-MessengerFlow-Deliveryheader to deduplicate. - Monitor your endpoint health — check the delivery history in Settings > Developers > Webhooks to catch failures early. Repeated failures will auto-disable your endpoint.
- Keep your endpoint fast — responses must arrive within 10 seconds. If processing takes longer, store the payload and handle it asynchronously.
- Log raw payloads — during development, log the full request body and headers for debugging. This helps diagnose signature verification issues.
Troubleshooting
Endpoint not receiving events
- Verify the URL is publicly accessible (not
localhostor behind a firewall) - Confirm HTTPS is configured correctly with a valid certificate
- Check that the endpoint is enabled and not auto-disabled
- Ensure you're subscribed to the correct events
Signature verification failing
- Make sure you're using the raw JSON body string, not a re-serialized version
- Verify the signing secret matches what was shown at endpoint creation
- Check that the timestamp header is included in the HMAC computation
- Ensure you're comparing the full
sha256=<hex>prefix
Endpoint auto-disabled
Your endpoint returned non-2xx responses 5 consecutive times. To recover:
- Check your server logs for the root cause
- Fix the issue (server error, timeout, certificate problem, etc.)
- Go to Settings > Developers > Webhooks
- Click into the endpoint and re-enable it
- Use Send Test to confirm it's working before live events resume
Events arriving late
- Worker-originated events (messages, conversations) are delivered on a 10-second polling cycle
- If previous deliveries are in retry backoff, new events queue behind them
- Check the delivery history to see if earlier failures are causing delays
