Search for a command to run...
Core concept
HMAC-SHA256Receive only the events you need through separate signed sandbox and production endpoints, with channel and client_ref included for deterministic routing.
In the dashboard's Webhooks section, select Sandbox or Production and save its URL and event subscriptions. Both configurations are retained when you switch Test/Live. Each has its own signing secret: copy the matching secret into that environment's secret manager. Configuration is managed through the portal, not a public /v1/webhooks route.
Real Meta events follow the client's saved webhook environment. Choose it under Clients → Edit; changing the browser's Test/Live mode does not reroute production traffic. Newly connected clients use the selected mode; reconnecting preserves their environment. Existing clients, URLs and secrets remain in production after the upgrade. Configure a sandbox URL before testing; a missing URL never falls back to the other environment.
Select individual events, all events (including future types), or none to pause delivery. Subscriptions are independent per environment. Simulations use the selected environment and respect its subscriptions; the signed connectivity test bypasses subscriptions. Logs identify the environment, and resending stays in the original environment with its current configuration.
Configure webhooksThe default selection list shows the eight normalized event types. Provider-specific raw Meta fields are available under the collapsed Advanced section; existing raw subscriptions remain selected. Raw subscriptions also match normalized events received on that field: for example, selecting messages can enable normalized message types even when their individual boxes are unchecked. “All events” includes both groups and future event types. Subscribe only to events your product handles. New fields can be added over time, so ignore unknown properties.
| event | Channel | Meaning |
|---|---|---|
| message.received | WhatsApp, Instagram | An end user sent a message. |
| message.status | A sent message moved to sent, delivered, read, or failed. | |
| message.reaction | WhatsApp, Instagram | An end user added or removed a reaction. |
| message.echo | The connected professional account sent a message. | |
| message.read | A user read a message. | |
| message.postback | A user activated a postback action. | |
| template.status | Meta finished reviewing a message template. Carries template.status (APPROVED, REJECTED, PAUSED, …) and template.reason. | |
| user.preferences | A user opted in or out of marketing messages. | |
| webhook.test | All | A signed diagnostic event from the dashboard. |
Use event to select a handler, channel for the provider-specific body, and client_ref to find your customer. Provider data stays close to the source payload so you can preserve message fidelity.
{
"event": "message.received",
"channel": "whatsapp",
"timestamp": "2026-07-24T09:42:18.000Z",
"client_ref": "customer_42",
"phone_number": "+254711111111",
"contacts": [
{
"profile": { "name": "Amina" },
"wa_id": "254700000000",
"user_id": "KE.18827367240033871294"
}
],
"messages": [
{
"from": "254700000000",
"from_user_id": "KE.18827367240033871294",
"id": "wamid.xxx",
"type": "text",
"text": { "body": "Is my order ready?" }
}
]
}Customers who hide their phone number
from and no contacts[].wa_id — only the business-scoped user ID, messages[].from_user_id and contacts[].user_id (plus contacts[].profile.username when they have one). Reply by passing that ID as to on any messages endpoint; it is routed to Meta's recipient parameter for you. The ID is sent for every customer, hidden number or not, so it is the safer key for your own records. On message.status the matching field is statuses[].recipient_user_id.{
"event": "message.received",
"channel": "instagram",
"timestamp": "2026-07-24T09:44:01.000Z",
"client_ref": "customer_42_instagram",
"contact": {
"id": "17841400000000000",
"username": "ama_mensah",
"name": "Ama Mensah"
},
"messages": [
{
"id": "mid.xxx",
"sender": {
"id": "17841400000000000",
"username": "ama_mensah",
"name": "Ama Mensah"
},
"recipient": { "id": "17841499999999999" },
"text": "Can you help?"
}
]
}Downloading media a customer sent
messages[].image.id and friends. Fetching the file needs the client's Meta credentials, which you never hold, so request it from us: GET /v1/media/<media_id>?client_ref=<ref> with your API key (clients:read scope). It streams the bytes back with the real Content-Type, plus Content-Length and an X-Media-Sha256 you can verify against the webhook. Meta keeps inbound media for 30 days.attachments[].payload.url. It is short-lived, so download on receipt rather than when someone opens the conversation.Instagram users arrive with their handle
username and name, and the person the event is about is repeated at the top level as contact. Both are omitted when Instagram has no profile to return, so treat them as optional and keep keying your records on the ID.Store provider message IDs
Read X-Intelli-Signature, compute HMAC-SHA256 over the exact raw request bytes, prefix the hex digest with sha256=, and compare in constant time.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyIntelliSignature(
rawBody: Buffer,
signature: string,
secret: string,
) {
const expected =
"sha256=" +
createHmac("sha256", secret).update(rawBody).digest("hex");
const received = Buffer.from(signature);
const calculated = Buffer.from(expected);
return (
received.length === calculated.length &&
timingSafeEqual(received, calculated)
);
}Do not re-serialize JSON
Verify, persist or enqueue, and return a 2xx response within five seconds. Move database fan-out, AI work, outbound replies, and third party calls into an asynchronous worker.
Immediate
10 seconds
60 seconds
5 minutes
Non-2xx responses and timeouts are retried on this schedule. After the final attempt, the delivery remains available in logs for inspection and manual redelivery.
Copy the integration contract into your assistant, then let it tailor the implementation to your stack.
You are helping me receive Intelli Partner API webhooks in my product — events from the WhatsApp and Instagram accounts connected through Intelli (my clients' accounts, or my own).
Before writing code, ask me:
1. My backend framework/language.
2. Whether I already have a public HTTPS endpoint for webhooks.
Setup:
- I configure separate Sandbox and Production receiver URLs, signing secrets and event subscriptions in the Intelli portal (Webhooks page). Use the secret for the receiving environment as INTELLI_WEBHOOK_SECRET; keep it server-side. Real Meta events follow the client's saved environment (Clients → Edit), not the browser's Test/Live selector. Never reuse production credentials in development.
A) Verify EVERY delivery before parsing:
- Read the X-Intelli-Signature header, compute HMAC-SHA256 over the RAW request body with INTELLI_WEBHOOK_SECRET, and constant-time compare against "sha256=<hex digest>". Reject mismatches with 401. Never parse the JSON before the signature checks out, and never verify against a re-serialized body — byte-for-byte raw only.
B) Respond fast, process async, stay idempotent:
- Return 2xx immediately and hand the payload to a queue/worker. Failed or slow deliveries are retried on my configured retry policy, so deliveries can arrive more than once and out of order — dedupe on the message id inside the payload.
C) Events — every payload carries event, channel and client_ref so I can route to the right account:
- message.received (WhatsApp, Instagram): an end user wrote in. WhatsApp media arrives as an id — fetch it with GET /media/{media_id}?client_ref=... while it's valid (30 days). Instagram media carries a short-lived URL at attachments[].payload.url — download on receipt. Acknowledge it before replying: POST /messages/typing (shows typing and marks it read) or POST /messages/read, with client_ref, to = messages[].from (WhatsApp) / messages[].sender.id (Instagram) and message_id = messages[].id (required on WhatsApp). A WhatsApp customer who hides their number (usernames) has no from or contacts[].wa_id: I get messages[].from_user_id and contacts[].user_id instead (a business-scoped user ID like US.13491208655302741918, plus contacts[].profile.username when they have one) and I send to that ID in to. Meta also sends from_user_id alongside from for everyone else, so I key customers on it. On message.status the matching field is statuses[].recipient_user_id.
- message.status (WhatsApp): a message I sent moved to sent, delivered, read or failed — this is where ASYNCHRONOUS delivery failures surface (e.g. a header media link Meta couldn't fetch). Store the status against my message_id and alert on failed.
- message.reaction (WhatsApp, Instagram): a reaction was added or removed.
- message.echo (Instagram): the connected professional account itself sent something.
- message.read (Instagram): the user read my message.
- message.postback (Instagram): the user activated a postback action.
- template.status (WhatsApp): Meta finished reviewing a template — carries template.status (APPROVED, REJECTED, PAUSED, …) and template.reason. Flip my UI to sendable on APPROVED; show the reason on REJECTED.
- user.preferences (WhatsApp): the user opted in or out of marketing — honor it before sending marketing templates.
- webhook.test: a signed diagnostic event I can fire from the dashboard.
D) Tooling while I build:
- The portal's webhook simulator fires real signed sample events at my endpoint, and Delivery Logs show every attempt (with a resend button) so I can debug without waiting for real traffic.
Write idiomatic, production-quality code for my stack: a verified receiver, an async processor with idempotency, and handlers for the events I use.