Référence API
Une seule API pour tout piloter par programmation : numéros, templates, messages, campagnes et abonnement.
Construisez votre propre produit par-dessus.
Authentification
Toutes les requêtes utilisent une clé API côté serveur. Générez-en une dans votre tableau de bord
sous Clés API — elle ressemble à pk_live_… et n'est affichée qu'une seule fois. Envoyez-la
comme bearer token :
Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxxGardez les clés côté serveur
Une clé peut envoyer des messages depuis chacun des numéros qu'elle couvre. Ne l'exposez jamais dans un navigateur ou une application mobile — appelez l'API uniquement depuis votre backend.
Portée de la clé
Une clé est soit globale (tout le compte), soit rattachée à une seule entreprise connectée.
Choisissez l'entreprise au moment de générer la clé dans Clés API. Une clé restreinte ne résout
que cette entreprise : les listes ne renvoient que ses lignes, un id appartenant à une autre répond
404, et GET /api/v1/subscription répond 403 — vous pouvez donc la lui remettre (ou l'utiliser
pour son intégration) sans exposer le reste de votre compte. Les clés globales atteignent tous vos
numéros connectés.
Conventions
URL de base https://wa.genuka.com/api/v1. Les corps et les réponses sont en JSON. Les endpoints de
liste renvoient { "data": [...] }. Tout est automatiquement limité à votre compte. Les erreurs
utilisent les codes de statut HTTP avec { "error": "code", "message": "…" }.
Appelez wa.genuka.com en https et sans slash final : http:// renvoie un 301 vers https,
/api/v1/messages/ un 308 vers la forme sans slash, et certains clients HTTP retirent l'en-tête
Authorization en suivant une redirection. Aucun autre nom d'hôte ne sert cette API.
Déclarez un User-Agent
Notre CDN refuse quelques signatures de robots connues avant d'atteindre l'API. La plus courante
chez nos intégrateurs est Python-urllib/3.x, l'en-tête que urllib envoie par défaut : elle
répond 403 avec le corps error code: 1010, qui ne vient pas de nous et ne porte donc pas de
JSON. Envoyez un User-Agent qui identifie votre intégration — c'est de toute façon ce qui vous
permettra de vous retrouver dans vos journaux :
request.add_header("User-Agent", "acme-crm/1.4 (+https://acme.co)")requests, curl, axios, node-fetch, les clients Go et Java passent sans réglage.
Entreprises (comptes connectés)
| Méthode | Endpoint | Description |
|---|---|---|
GET | /api/v1/companies | Lister les entreprises connectées avec leurs connexions |
GET | /api/v1/companies/{id} | Une entreprise + connexions + décomptes |
curl https://wa.genuka.com/api/v1/companies -H "Authorization: Bearer pk_live_xxx"
{
"data": [
{
"id": "cmp_123",
"name": "Acme Coffee",
"onboardedAt": "2026-06-01T10:12:00.000Z",
"connections": [
{ "id": "con_1", "wabaId": "1029…", "phoneNumberId": "1065…",
"displayPhoneNumber": "+237 6 90 …", "qualityRating": "GREEN", "status": "connected" }
],
"_count": { "templates": 4 }
}
]
}Connexions (numéros)
| Méthode | Endpoint | Description |
|---|---|---|
GET | /api/v1/connections | Lister les connexions WhatsApp (optionnel ?companyId=, ?externalRef=) |
Une connexion est un WABA + numéro de téléphone. Son id est ce que vous transmettez lors de la
création de templates, de campagnes ou de l'envoi de messages.
Templates
Les templates sont des mises en page de message pré-approuvées. Il vous en faut un pour démarrer
une conversation (c.-à-d. écrire à un client en dehors de la fenêtre de 24 heures — voir
Messages). Vous créez un template ici, Meta l'examine, et l'approbation / le rejet
arrive automatiquement sur GET /templates/{id} (et sur vos webhooks).
| Méthode | Endpoint | Description |
|---|---|---|
GET | /api/v1/templates | Lister les templates (optionnel ?companyId=) |
POST | /api/v1/templates | Créer et soumettre un template à Meta |
GET | /api/v1/templates/{id} | Un template + son historique de statuts |
POST | /api/v1/templates/sync | Relire tous les statuts depuis Meta |
DELETE | /api/v1/templates/{id} | Supprimer sur Meta et localement |
Comment fonctionne la création
Vous transmettez le tableau components de Meta tel quel. Cela garde cet endpoint léger tout en
vous laissant construire n'importe quel template pris en charge par Meta — texte, en-têtes média,
boutons, OTP, etc. Les seuls champs requis sont connectionId, name et components. category
est l'un de MARKETING, UTILITY ou AUTHENTICATION (par défaut MARKETING) ; language est une
locale Meta telle que en_US ou fr (par défaut en).
Référence des composants
Un template est une liste ordonnée de composants. Chacun a un type :
| Type | Forme | Description |
|---|---|---|
HEADER | format: TEXT | IMAGE | VIDEO | DOCUMENT | LOCATION | Optionnel. Un seul par template. Les en-têtes média nécessitent un handle d'exemple. |
BODY | text + example | Requis (sauf AUTHENTICATION). Contient les variables {{1}}… ou {{name}}. |
FOOTER | text | Pied de page court optionnel. Pas de variables. |
BUTTONS | QUICK_REPLY | URL | PHONE_NUMBER | COPY_CODE | OTP | Optionnel. Jusqu'à 10 boutons (les règles varient selon le type). |
Les exemples sont obligatoires
Tout composant avec des variables (ou un en-tête média) doit inclure un example afin que Meta
puisse l'examiner : "example": { "body_text": [["Alice", "#1024"]] } pour le corps,
"example": { "header_handle": ["<id>"] } pour un en-tête média.
Template utilitaire / marketing (en-tête + corps + boutons)
curl -X POST https://wa.genuka.com/api/v1/templates \
-H "Authorization: Bearer pk_live_xxx" -H "Content-Type: application/json" \
-d '{
"connectionId": "con_1",
"name": "order_shipped",
"language": "en_US",
"category": "UTILITY",
"components": [
{ "type": "HEADER", "format": "IMAGE",
"example": { "header_handle": ["4::aW1hZ2Uv..."] } },
{ "type": "BODY",
"text": "Hi {{1}}, order {{2}} just shipped. Track it any time.",
"example": { "body_text": [["Alice", "#1024"]] } },
{ "type": "FOOTER", "text": "Reply STOP to opt out" },
{ "type": "BUTTONS", "buttons": [
{ "type": "URL", "text": "Track order",
"url": "https://acme.co/track/{{1}}", "example": ["https://acme.co/track/1024"] },
{ "type": "QUICK_REPLY", "text": "Need help" }
] }
]
}'
{ "data": { "id": "tpl_123", "status": "pending", "providerId": "12534…" } }L'en-tête média example.header_handle est le handle d'upload reprenable renvoyé par l'upload média
de Meta — au moment de l'envoi vous fournissez l'image réelle par URL ou identifiant de média
(voir Messages → template).
Paramètres nommés
Vous préférez {{name}} au positionnel {{1}} ? Définissez parameterFormat: "NAMED" et donnez à
chaque variable un parameter_name dans l'exemple.
{
"connectionId": "con_1",
"name": "appointment_reminder",
"language": "en_US",
"category": "UTILITY",
"parameterFormat": "NAMED",
"components": [
{ "type": "BODY",
"text": "Hi {{customer_name}}, your appointment is on {{date}}.",
"example": { "body_text_named_params": [
{ "param_name": "customer_name", "example": "Alice" },
{ "param_name": "date", "example": "June 20" }
] } }
]
}Template d'authentification (OTP)
Les templates d'authentification délivrent des codes à usage unique. Le corps et le texte du bouton sont fixés par WhatsApp — vous ne rédigez pas le contenu. Vous choisissez seulement le type de bouton et quelques options. Aucun en-tête, média, URL ou emoji n'est autorisé.
| Type | Forme | Description |
|---|---|---|
COPY_CODE | otp_type: COPY_CODE | Le client touche pour copier le code. Le plus simple, fonctionne partout. |
ONE_TAP | otp_type: ONE_TAP | Saisie automatique Android. Nécessite package_name + signature_hash. |
ZERO_TAP | otp_type: ZERO_TAP | Code délivré silencieusement à l'application. Nécessite la même liaison d'application. |
{
"connectionId": "con_1",
"name": "verification_code",
"language": "en_US",
"category": "AUTHENTICATION",
"messageSendTtlSeconds": 600,
"components": [
{ "type": "BODY", "add_security_recommendation": true },
{ "type": "FOOTER", "code_expiration_minutes": 10 },
{ "type": "BUTTONS", "buttons": [
{ "type": "OTP", "otp_type": "COPY_CODE", "text": "Copy code" }
] }
]
}Pour ONE_TAP / ZERO_TAP, ajoutez la liaison d'application au bouton OTP :
"autofill_text": "Autofill",
"supported_apps": [{ "package_name": "com.acme.app", "signature_hash": "K8a..." }].
Envoyer le code
La création du template n'envoie jamais rien. Pour délivrer un code, vous envoyez un message
template avec le raccourci otp — voir
Messages → template d'authentification.
Actualiser les statuts
Le résultat d'une revue vous parvient en webhook — c'est le chemin sur lequel
construire. Mais un webhook jamais livré (un endpoint tombé, une intégration ajoutée après coup)
laisse le template affiché pending chez nous longtemps après son approbation par Meta, et une
campagne qui refuse de partir sans raison visible. POST /api/v1/templates/sync relit la vérité
chez Meta et la réécrit.
curl -X POST https://wa.genuka.com/api/v1/templates/sync \
-H "Authorization: Bearer pk_live_xxx" -H "Content-Type: application/json" \
-d '{ "companyId": "cmp_1" }'Le corps est optionnel : sans aucun champ, l'actualisation couvre tous les comptes WhatsApp que
votre clé peut atteindre, companyId la limite à un client, et connectionId au WABA d'un seul
numéro. La réponse nomme chaque compte WhatsApp touché — un compte injoignable est donc signalé
plutôt que masqué — et renvoie les lignes fraîchement écrites, ce qui évite un GET de suivi.
{
"data": {
"wabas": [{ "wabaId": "1029…", "companyId": "cmp_1", "ok": true }],
"synced": 1,
"failed": 0,
"templates": [
{ "id": "tpl_1", "name": "order_shipped", "language": "fr", "status": "approved", "…": "…" }
]
}
}Une actualisation où tous les comptes ont refusé répond 502 meta_rejected avec les mêmes
results dans le corps ; un échec partiel reste un 200 que vous devez lire.
GET /api/v1/templates?sync=true effectue la même actualisation avant de lister, et accepte les
mêmes filtres companyId / connectionId.
Un chemin de réparation, pas une boucle de polling
Chaque appel coûte une lecture Meta par compte WhatsApp. Planifiez-le si vous voulez — toutes les heures suffit largement — mais gardez le webhook comme voie normale d'arrivée des statuts.
Campagnes
| Méthode | Endpoint | Description |
|---|---|---|
GET | /api/v1/campaigns | Lister les campagnes (optionnel ?companyId=) |
POST | /api/v1/campaigns | Créer une campagne avec des destinataires |
GET | /api/v1/campaigns/{id} | Une campagne avec ses statistiques de livraison |
GET | /api/v1/campaigns/{id}/recipients | Statut par destinataire (?status=, ?limit=) |
POST | /api/v1/campaigns/{id}/launch | Envoyer à tous les destinataires en attente |
Créer une campagne
Chaque destinataire porte ses propres variables. La forme simple est un tableau positionnel pour
les paramètres du corps {{1}}, {{2}}…. Pour les en-têtes média, les boutons ou les codes OTP,
transmettez plutôt un objet riche — { "body": [...], "header": {...}, "buttons": [...] } — la même
structure acceptée par Messages → template. Le template doit être approved avant le
lancement.
curl -X POST https://wa.genuka.com/api/v1/campaigns \
-H "Authorization: Bearer pk_live_xxx" -H "Content-Type: application/json" \
-d '{
"connectionId": "con_1",
"templateId": "tpl_123",
"name": "June promo",
"recipients": [
{ "to": "+237690000001", "variables": ["Alice", "#1024"] },
{ "to": "+237690000002", "variables": ["Bob", "#1025"] }
]
}'
{ "data": { "id": "cmp_9", "name": "June promo", "status": "draft", "_count": { "recipients": 2 } } }Lancer
curl -X POST https://wa.genuka.com/api/v1/campaigns/cmp_9/launch \
-H "Authorization: Bearer pk_live_xxx"
{ "data": { "sent": 2, "failed": 0, "skipped": 0 } }Limites
Nous ne facturons jamais un message — Meta facture directement votre WABA. Les destinataires sont
en revanche décomptés du quota de messages de la période, et toute la liste est vérifiée avant le
premier envoi : un lancement qui n'y tient pas est refusé d'emblée avec 402 plan_limit_messages
plutôt que de s'arrêter à mi-parcours. Seuls les destinataires réellement acceptés par Meta sont
décomptés. La V1 envoie de façon synchrone — gardez des listes modestes ; l'envoi en file d'attente
pour les grandes listes est prévu sur la feuille de route.
Médias
Téléversez un fichier une fois, envoyez-le autant de fois que nécessaire. Le stockage est privé : rien de ce qui est déposé ici n'est accessible depuis une URL publique, et Meta ne vient jamais chercher le fichier — nous lui transmettons les octets de serveur à serveur au moment de l'envoi.
curl -X POST https://wa.genuka.com/api/v1/media \
-H "Authorization: Bearer pk_live_xxx" \
-F "companyId=cmp_1" \
-F "[email protected]"
{ "data": {
"id": "ast_1",
"url": "https://wa.genuka.com/api/v1/media/ast_1/content",
"expiresAt": "2026-09-18T09:12:00.000Z",
"mimeType": "application/pdf",
"kind": "document",
"sizeBytes": 481203,
"filename": "bulletin.pdf"
},
"deduplicated": false }Deux limites, annoncées d'avance
Rétention — 30 jours. Tout fichier est supprimé définitivement 30 jours après son
téléversement, expiresAt le dit à l'octet près. C'est la fenêtre de Meta elle-même : au-delà,
sa copie a disparu de toute façon. Un envoi qui référence un fichier expiré répond 404 media_not_found.
Stockage — selon la formule. 1 Go sur Starter, 3 Go sur Growth, 5 Go sur Scale, sur mesure
sur Enterprise, tous clients confondus. Le plafond porte sur ce qui est détenu : supprimer un
fichier rend la place immédiatement. Un téléversement qui dépasse répond 402 plan_limit_media, avant que le moindre octet ne parte. GET /api/v1/media renvoie l'état
courant dans storage.
url est un point d'accès de cette API, pas un lien de partage : il exige votre clé et refuse
tout appel hors de sa portée. Des octets identiques déjà déposés pour ce client sont renvoyés tels
quels avec un 200 et deduplicated: true, sans être stockés deux fois.
| Route | Description |
|---|---|
POST /api/v1/media | Téléverser. Multipart : file (requis), companyId (requis avec une clé partenaire), filename (optionnel). |
GET /api/v1/media | Lister. Filtres kind, companyId, pagination par curseur, plus l'état storage. |
GET /api/v1/media/{id} | Métadonnées d'un fichier. |
GET /api/v1/media/{id}/content | Les octets, authentifiés par votre clé. |
DELETE /api/v1/media/{id} | Supprimer sur-le-champ, sans attendre l'expiration. |
POST /api/v1/media/header-handle | Produire le header_handle d'un en-tête de template. Multipart : file (requis), connectionId ou companyId (aucun des deux avec une clé client). |
En-tête média d'un template : le header_handle
Créer un template dont l'en-tête est une IMAGE, une VIDEO ou un DOCUMENT demande un
example.header_handle, que Meta ne délivre que par son API d'upload reprenable. Ce n'est pas
la même chose qu'un assetId : un handle se consomme une seule fois, à la création du template,
et un envoi le refuse. Inversement un assetId ne vaut rien à la création.
curl -X POST https://wa.genuka.com/api/v1/media/header-handle \
-H "Authorization: Bearer pk_live_xxx" \
-F "connectionId=con_1" \
-F "[email protected]"
{ "data": {
"handle": "4::YXBwbGljYXRpb24vcGRm…",
"filename": "catalogue.pdf",
"mimeType": "application/pdf",
"sizeBytes": 481203
} }Nommez le client comme partout ailleurs sur /media (-F "companyId=cmp_1") ou un numéro précis
(-F "connectionId=con_1") ; une clé client n'a rien à préciser. Le handle produit est le même :
la session d'upload appartient à l'app Meta, pas au numéro.
Reportez ce handle dans la définition du template :
{
"connectionId": "con_1",
"name": "brief_du_matin",
"language": "fr",
"category": "UTILITY",
"components": [
{ "type": "HEADER", "format": "DOCUMENT",
"example": { "header_handle": ["4::YXBwbGljYXRpb24vcGRm…"] } },
{ "type": "BODY", "text": "Bonjour {{1}}, votre brief du {{2}} est joint.",
"example": { "body_text": [["Ana", "12 mars"]] } }
]
}Formats acceptés : image/jpeg, image/png, video/mp4, video/3gpp, application/pdf. Taille
maximale 4 Mo — au-delà, la réponse est 413 media_too_large. Rien n'est conservé de notre côté :
le handle est à usage unique, l'archiver n'aurait aucune valeur.
Envoyer un fichier stocké
Passez assetId partout où un média est attendu — en-tête de template compris. Nous résolvons le
media_id Meta pour le bon numéro, et nous le renouvelons tout seuls quand il vieillit.
{
"connectionId": "con_1",
"to": "237600000000",
"document": { "assetId": "ast_1", "filename": "bulletin.pdf" }
}id (un media_id Meta que vous avez créé vous-même) et link (une URL publique que Meta va
chercher) restent acceptés. assetId est celui à préférer : c'est le seul qui n'expose le fichier
sur aucune URL et le seul qui ne se périme pas sous vos pieds au bout de 30 jours.
Messages
| Méthode | Endpoint | Description |
|---|---|---|
POST | /api/v1/messages | Envoyer un message unique de n'importe quel type |
Un seul endpoint envoie tous les types de message WhatsApp. Transmettez toujours connectionId et
to, puis exactement un champ de contenu du tableau ci-dessous. Ajoutez replyTo (le wamid
d'un message reçu) pour citer/répondre à un message.
| Champ | Fenêtre | Description |
|---|---|---|
template | facturable | Template pré-approuvé. Le seul moyen de démarrer une conversation en dehors de la fenêtre de 24 h. |
text | libre* | { "text": "Hi" } ou { "text": { "body": "…", "previewUrl": true } } |
image / video / audio / document / sticker | libre* | { "image": { "assetId": "ast_1" } } (recommandé), { "link": "…" } ou { "id": "<media-id>" } ; caption/filename optionnels |
location | libre* | { "location": { "latitude": …, "longitude": …, "name": "…", "address": "…" } } |
contacts | libre* | { "contacts": [ … objets contact Meta … ] } |
reaction | libre* | { "reaction": { "messageId": "wamid…", "emoji": "👍" } } |
buttons / list / cta | libre* | Boutons de réponse interactifs, un menu liste, ou un bouton URL d'appel à l'action. |
locationRequest | libre* | { "locationRequest": { "body": "Où livrer ?" } } — affiche un bouton Envoyer la position. |
raw | libre* | Échappatoire pour tout le reste (Flows, adresse, catalogue, appel vocal) : un fragment de type Cloud API complet. |
La fenêtre de 24 heures
Seuls les envois template peuvent démarrer une conversation. Tout ce qui est marqué libre* est
un message de session en format libre : il n'est délivré que si le client a écrit à l'entreprise
au cours des dernières 24 heures. En dehors de cette fenêtre, utilisez un template. Tous les
envois renvoient { "data": { "messageId": "wamid…" } }.
Un 200 signifie que Meta a accepté le message, et non qu'il a été livré. Le statut final
(sent → delivered → read, ou failed) arrive de façon asynchrone sur vos
webhooks. Pour to, incluez toujours le + et l'indicatif pays (par ex.
+237690000001) — l'omettre peut mal router le message. Les médias transmis par link sont mis en
cache par Meta pendant ~10 minutes, donc réutilisez la même URL pour le même asset (ou ajoutez une
chaîne de requête unique pour vider le cache).
Message template
Le cas simple est uniquement les variables du corps. variables est un tableau positionnel ; vous
pouvez aussi utiliser bodyNamed pour les templates nommés, header pour un en-tête média/texte,
et buttons pour des paramètres de bouton dynamiques.
curl -X POST https://wa.genuka.com/api/v1/messages \
-H "Authorization: Bearer pk_live_xxx" -H "Content-Type: application/json" \
-d '{
"connectionId": "con_1",
"to": "+237690000001",
"template": { "name": "order_shipped", "language": "en_US", "variables": ["Alice", "#1024"] }
}'
{ "data": { "messageId": "wamid.HBg…" } }{
"connectionId": "con_1",
"to": "+237690000001",
"template": {
"name": "order_shipped",
"language": "en_US",
"header": { "image": { "link": "https://acme.co/orders/1024.png" } },
"variables": ["Alice", "#1024"],
"buttons": [
{ "type": "url", "text": "1024" }
]
}
}Le buttons[].text remplit la partie dynamique d'un bouton URL (le {{1}} dans
https://acme.co/track/{{1}}). Pour une réponse rapide, utilisez
{ "type": "quick_reply", "payload": "…" } ; pour un code de coupon, utilisez
{ "type": "copy_code", "code": "SAVE20" }. Paramètres de corps nommés :
"bodyNamed": { "customer_name": "Alice" }.
Template d'authentification
Transmettez le code une seule fois via le raccourci otp — nous remplissons à la fois le corps et
le bouton OTP pour vous (le format requis par Meta).
{
"connectionId": "con_1",
"to": "+237690000001",
"template": { "name": "verification_code", "language": "en_US", "otp": "472913" }
}Messages de session en format libre
{ "connectionId": "con_1", "to": "+237690000001", "text": "Thanks, talk soon!" }{ "connectionId": "con_1", "to": "+237690000001",
"image": { "link": "https://acme.co/promo.jpg", "caption": "New arrivals 🎉" } }{ "connectionId": "con_1", "to": "+237690000001",
"document": { "link": "https://acme.co/invoice.pdf", "filename": "invoice-1024.pdf" } }{
"connectionId": "con_1",
"to": "+237690000001",
"buttons": {
"body": "Confirm your order?",
"footer": "Acme Coffee",
"buttons": [
{ "id": "yes", "title": "Confirm" },
{ "id": "no", "title": "Cancel" }
]
}
}{
"connectionId": "con_1",
"to": "+237690000001",
"list": {
"body": "Pick a delivery slot",
"button": "Choose",
"sections": [
{ "title": "Today", "rows": [
{ "id": "t1", "title": "12:00–14:00" },
{ "id": "t2", "title": "14:00–16:00", "description": "Most popular" }
] }
]
}
}{
"connectionId": "con_1",
"to": "+237690000001",
"cta": { "body": "Your receipt is ready.", "displayText": "View receipt", "url": "https://acme.co/r/1024" }
}{ "connectionId": "con_1", "to": "+237690000001", "replyTo": "wamid.HBg…", "text": "On its way!" }
{ "connectionId": "con_1", "to": "+237690000001",
"reaction": { "messageId": "wamid.HBg…", "emoji": "👍" } }Abonnement
| Méthode | Endpoint | Description |
|---|---|---|
GET | /api/v1/subscription | Plan, limites et usage actuels |
{
"plan": { "code": "growth", "name": "Growth" },
"interval": "monthly",
"currency": "XAF",
"status": "active",
"currentPeriodEnd": "2026-07-18T00:00:00.000Z",
"billedNumbers": 9,
"usage": { "numbers": 9, "messages": 12480, "seats": 3 },
"limits": { "maxNumbers": 9, "monthlyMessages": 45000, "maxSeats": 5 }
}Erreurs
401 { "error": "missing_bearer_token" } // pas d'en-tête Authorization
401 { "error": "invalid_token" } // clé inconnue / révoquée, ou compte inactif
403 { "error": "account_deactivated", "message": "Deactivated Account" }
// cette entreprise est suspendue : sa propre clé
// cesse de fonctionner, et toute requête la nommant
// est refusée — y compris avec une clé globale
400 { "error": "missing_fields", "message": "…" }
400 { "error": "missing_content", "message": "…" } // aucun champ de contenu sur un envoi
400 { "error": "invalid_media", "message": "…" } // média sans assetId, id ni link
402 { "error": "plan_limit_media", "message": "…" } // plafond de stockage de la formule atteint
404 { "error": "media_not_found", "message": "…" } // assetId inconnu, ou passé ses 30 jours
402 { "error": "plan_limit_numbers" } // tous les numéros payés par le plan sont utilisés
402 { "error": "plan_limit_messages" } // le quota de messages de la période est épuisé
402 { "error": "subscription_past_due" } // essai ou période payée échu sans renouvellement
404 { "error": "connection_not_found" }
404 { "error": "template_not_found" }
409 { "error": "template_exists" }
413 { "error": "media_too_large", "message": "…" } // en-tête de template au-delà de 4 Mo
422 { "error": "meta_rejected", "message": "…", "meta": { … } }
// Meta a lu la charge utile et l'a refusée
502 { "error": "meta_rejected", "message": "…" } // Graph injoignable ou en panneCe que Meta a refusé renvoie 422, pas 502
Quand Graph lit une charge utile et la refuse — un template malformé, une variable sans exemple,
un numéro non provisionné — la réponse est un 422, et son corps porte un objet meta avec le
code d'erreur de Meta, son traceId et sa classe :
{
"error": "meta_rejected",
"message": "Invalid parameter",
"meta": {
"errorClass": "template",
"retryable": false,
"code": 100,
"details": "body_text example count does not match the number of variables",
"traceId": "AbC…"
}
}Le traceId est ce que le support Meta demande : citez-le tel quel.
Un 502 ne subsiste que pour ce que le mot décrit — Graph injoignable, ou Graph lui-même en
échec. Renvoyer un refus de charge utile en 502 rendait le diagnostic impossible : notre CDN
répond aux 5xx d'origine par sa propre page d'erreur, remplaçant ce corps JSON par la seule ligne
error code: 502. Le motif du refus ne vous parvenait jamais. Les corps 4xx, eux, passent intacts.
Journaux de requêtes
Chaque réponse porte un en-tête x-request-id. Ce même identifiant retrouve l'appel dans votre
tableau de bord, section Journaux, où chaque requête est conservée avec son statut, sa durée, son
corps et — en cas d'échec — la réponse renvoyée. Les livraisons de webhooks sont listées à côté,
avec leur charge utile signée exacte et un bouton de renvoi.
La profondeur d'historique dépend de votre plan : 7 jours en Starter, 30 en Growth, 90 en Scale.
Champs masqués
Les corps sont conservés pour rendre un échec reproductible, sauf ce qui ressemble à un
identifiant : les valeurs password, token, secret et api_key sont remplacées avant
l'écriture de la ligne.