Migrating from Meta direct#
If your code already calls the WhatsApp Cloud API on graph.facebook.com, moving to kenari takes two changes to your requests and one change to how you receive webhooks. Your payloads, paths and response handling stay the same.
Before you start#
- Create a kenari account and connect the numbers you send from. If you already have a Meta app and a system user token, use Bring your own account in the dashboard. See Connecting a number.
- Create an API key for each environment.
- Register a webhook endpoint in the dashboard and keep its signing secret.
1. Change the host#
Replace https://graph.facebook.com with https://api.kenari.dev. Keep the version and the rest of the path as they are.
# Beforehttps://graph.facebook.com/v23.0/$PHONE_NUMBER_ID/messages# Afterhttps://api.kenari.dev/v23.0/$PHONE_NUMBER_ID/messageskenari forwards the version in your path to Meta as it is. It doesn't upgrade it. See Base URL & versions.
2. Change the token#
Keep the Authorization: Bearer header. Put your kenari API key in it instead of the Meta access token.
# Before-H "Authorization: Bearer $META_ACCESS_TOKEN"# After-H "Authorization: Bearer $KENARI_API_KEY"If your code passes the Meta token as an access_token query parameter, move the key into the header. kenari drops access_token and appsecret_proof from the query string and doesn't read keys from it.
In most codebases both changes are two configuration values:
// Beforeconst GRAPH_BASE = "https://graph.facebook.com";const TOKEN = process.env.META_ACCESS_TOKEN;// Afterconst GRAPH_BASE = "https://api.kenari.dev";const TOKEN = process.env.KENARI_API_KEY;await fetch(`${GRAPH_BASE}/v23.0/${phoneNumberId}/messages`, { method: "POST", headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify(payload),});What stays the same#
- Paths.
/{version}/{phone-number-id}/messages,/{version}/{waba-id}/message_templatesand the other supported endpoints keep Meta's shape. - Request bodies. Send Meta's Cloud API JSON unchanged.
- Responses. Meta's status code and body are returned as they are, errors included. Code that reads
messages[0].idor Meta'serror.codekeeps working. - Meta's headers.
x-fb-trace-id,x-business-use-case-usageandx-app-usageare passed through. - The auth header. Still
Authorization: Bearer ….
What's different#
| Area | Meta direct | kenari |
|---|---|---|
| Endpoints | The whole Graph API | A WhatsApp allow-list. Other paths return 404 KenariRoutingException. See Supported endpoints. |
| IDs | Any ID your token can see | Only numbers, WABAs and media on your kenari account |
| Media by ID | GET /{version}/{media-id} | Add ?phone_number_id={id} so kenari knows which number the media belongs to |
| kenari's own errors | n/a | 401, 402, 404, 413, 422, 429, 502, 503 with a Kenari*Exception type. See Errors. |
| Rate limits | Meta's limits | kenari's per-number and per-WABA limits first, with RateLimit-* and Retry-After headers. See Rate limits. |
| Request ID | x-fb-trace-id | Also X-Kenari-Request-Id |
kenari's own errors use the same { "error": { … } } envelope as Meta, with a type starting with Kenari. Check error.type to tell them apart from Meta's errors:
{ "error": { "message": "This account is suspended.", "type": "KenariBillingException", "code": 1002, "fbtrace_id": "req_..." }}3. Move your webhooks#
Meta sends events for a connected number to kenari, not to your server. kenari then delivers them to the endpoints you register under Webhooks in the dashboard.
- Embedded Signup numbers. kenari subscribes the WABA to Meta's webhooks when you connect. There's nothing to change in Meta.
- Bring-your-own numbers. The dashboard shows a callback URL (
https://api.kenari.dev/webhook/byo/…) and a verify token. Set both in your Meta app's WhatsApp webhook configuration, in place of your own server's URL.
The body kenari delivers is Meta's webhook payload. If one Meta batch holds events for WABAs on different kenari accounts, each account only receives its own entries. Your parsing code stays the same. What changes is how you verify the request:
| Meta direct | kenari | |
|---|---|---|
| Signature | X-Hub-Signature-256 with your app secret | Standard Webhooks: webhook-id, webhook-timestamp, webhook-signature, with the endpoint's signing secret |
| Verification handshake | GET with hub.challenge on your server | None on your endpoint. kenari handles Meta's handshake. |
Whenever the delivered body is exactly the body Meta sent, kenari also forwards Meta's original signature as X-Meta-Signature-256. That is most useful for bring-your-own numbers, where you can check it with your own app secret.
Cutover checklist#
- Connect every sending number in kenari and check each one shows Connected.
- Register your webhook endpoint and deploy Standard Webhooks verification.
- Send a test message through
https://api.kenari.devfrom staging. - Switch the host and token in production.
- For bring-your-own numbers, point the Meta app's callback URL at kenari.
- Watch Logs in the dashboard for
401,404and422responses in the first hour. - Once traffic is flowing, stop using your Meta access token directly.