Meta compatibility
Joinotify exposes the whole Meta Cloud API under the /v1 prefix. If you already have
code written against the Graph, migrating is a two-string change.
The substitution
| From | To |
|---|---|
https://graph.facebook.com/v21.0/ | https://api.joinotify.com/v1/ |
Authorization: Bearer <META_TOKEN> | Authorization: Bearer sk_live_xxx |
Nothing else changes. Paths, request bodies, responses, status codes and error messages stay identical — we pass Meta's response through byte for byte.
curl -X POST https://graph.facebook.com/v21.0/106540352242922/messages \
-H 'Authorization: Bearer EAAG...' \
-H 'Content-Type: application/json' \
-d '{ "messaging_product": "whatsapp", "to": "5541987111527", "type": "text", "text": { "body": "Hello!" } }'
curl -X POST https://api.joinotify.com/v1/106540352242922/messages \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{ "messaging_product": "whatsapp", "to": "5541987111527", "type": "text", "text": { "body": "Hello!" } }'
Where to find the IDs
The mirror needs the same identifiers Meta uses: phone_number_id, waba_id and, for
some endpoints, business_id. If all you have is the Joinotify token, find them with:
curl https://api.joinotify.com/me \
-H 'Authorization: Bearer sk_live_xxx'
The token always goes in the header
Meta's examples often pass the token as ?access_token= in the URL. The mirror does
not accept that. The token has to be in Authorization.
# Correct
curl 'https://api.joinotify.com/v1/{WABA_ID}?fields=analytics' \
-H 'Authorization: Bearer sk_live_xxx'
# Does not work
curl 'https://api.joinotify.com/v1/{WABA_ID}?fields=analytics&access_token=sk_live_xxx'
Internally, each Meta endpoint expects the token in a different way — the proxy sorts that out for you. Your real Meta credential never reaches your code.
Blocked endpoints
Some paths would change the configuration of our Meta integration and affect other
customers. They return 403 with type: forbidden_endpoint:
| Path segment | Why it's blocked |
|---|---|
subscribed_apps | Re-subscribing the app would break webhook routing. |
register · deregister | Number registration happens in the dashboard, during onboarding. |
request_code · verify_code | SMS/voice verification belongs to the connection flow. |
assigned_users · system_users | User management for our Business Manager. |
debug_token · oauth | Would expose metadata about the real credential. |
uploads | Resumable upload; use POST /v1/{phone_number_id}/media. |
A direct POST on a number ID (POST /v1/{phone_number_id} with no sub-resource) is
blocked too.
{
"error": {
"message": "This endpoint is not available through the Joinotify proxy.",
"type": "forbidden_endpoint"
}
}
Resource ownership
The first segment after /v1/ has to be a phone_number_id or waba_id that belongs to
your account. If it isn't, the response is 403 forbidden — the check happens before any
call to Meta.
The exception is media IDs (GET /v1/{media_id}), which cannot be matched up front: in
that case Meta decides ownership.
Proxy errors
When Meta responds, you get its response — errors included, in its format. The Joinotify envelope only shows up for failures that happen before reaching them:
| HTTP | type | Situation |
|---|---|---|
400 | invalid_request | No resource ID after /v1/. |
403 | forbidden_endpoint | Path on the block list. |
403 | forbidden | Resource does not belong to your account. |
502 | upstream_error | Could not reach Meta. |
504 | upstream_error | Meta took longer than 30 seconds. |
What about webhooks?
You don't configure a callback on Meta — we receive the events and forward them to the
endpoints you register, in the same envelope
({ object, entry[].changes[].value }). Handlers written for Meta work unchanged; only
signature validation moves from X-Hub-Signature-256 to X-Joinotify-Signature-256.
Details in Webhooks.
Endpoints not listed here
The reference covers the most-used endpoints. For anything else, check Meta's official documentation and apply the substitution at the top of this page.