Genuka WA docs

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 :

En-tête
Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxx

Gardez 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éthodeEndpointDescription
GET/api/v1/companiesLister les entreprises connectées avec leurs connexions
GET/api/v1/companies/{id}Une entreprise + connexions + décomptes
GET /api/v1/companies
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éthodeEndpointDescription
GET/api/v1/connectionsLister 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éthodeEndpointDescription
GET/api/v1/templatesLister les templates (optionnel ?companyId=)
POST/api/v1/templatesCréer et soumettre un template à Meta
GET/api/v1/templates/{id}Un template + son historique de statuts
POST/api/v1/templates/syncRelire 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 :

TypeFormeDescription
HEADERformat: TEXT | IMAGE | VIDEO | DOCUMENT | LOCATIONOptionnel. Un seul par template. Les en-têtes média nécessitent un handle d'exemple.
BODYtext + exampleRequis (sauf AUTHENTICATION). Contient les variables {{1}}… ou {{name}}.
FOOTERtextPied de page court optionnel. Pas de variables.
BUTTONSQUICK_REPLY | URL | PHONE_NUMBER | COPY_CODE | OTPOptionnel. 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)

POST /api/v1/templates
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.

POST /api/v1/templates (nommé)
{
  "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é.

TypeFormeDescription
COPY_CODEotp_type: COPY_CODELe client touche pour copier le code. Le plus simple, fonctionne partout.
ONE_TAPotp_type: ONE_TAPSaisie automatique Android. Nécessite package_name + signature_hash.
ZERO_TAPotp_type: ZERO_TAPCode délivré silencieusement à l'application. Nécessite la même liaison d'application.
POST /api/v1/templates (authentification)
{
  "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.

POST /api/v1/templates/sync
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.

200 OK
{
  "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éthodeEndpointDescription
GET/api/v1/campaignsLister les campagnes (optionnel ?companyId=)
POST/api/v1/campaignsCréer une campagne avec des destinataires
GET/api/v1/campaigns/{id}Une campagne avec ses statistiques de livraison
GET/api/v1/campaigns/{id}/recipientsStatut par destinataire (?status=, ?limit=)
POST/api/v1/campaigns/{id}/launchEnvoyer à 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.

POST /api/v1/campaigns
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

POST /api/v1/campaigns/{id}/launch
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.

POST /api/v1/media (multipart)
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.

RouteDescription
POST /api/v1/mediaTéléverser. Multipart : file (requis), companyId (requis avec une clé partenaire), filename (optionnel).
GET /api/v1/mediaLister. 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}/contentLes octets, authentifiés par votre clé.
DELETE /api/v1/media/{id}Supprimer sur-le-champ, sans attendre l'expiration.
POST /api/v1/media/header-handleProduire 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.

POST /api/v1/media/header-handle (multipart)
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 :

POST /api/v1/templates
{
  "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.

POST /api/v1/messages
{
  "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éthodeEndpointDescription
POST/api/v1/messagesEnvoyer 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.

ChampFenêtreDescription
templatefacturableTemplate pré-approuvé. Le seul moyen de démarrer une conversation en dehors de la fenêtre de 24 h.
textlibre*{ "text": "Hi" } ou { "text": { "body": "…", "previewUrl": true } }
image / video / audio / document / stickerlibre*{ "image": { "assetId": "ast_1" } } (recommandé), { "link": "…" } ou { "id": "<media-id>" } ; caption/filename optionnels
locationlibre*{ "location": { "latitude": …, "longitude": …, "name": "…", "address": "…" } }
contactslibre*{ "contacts": [ … objets contact Meta … ] }
reactionlibre*{ "reaction": { "messageId": "wamid…", "emoji": "👍" } }
buttons / list / ctalibre*Boutons de réponse interactifs, un menu liste, ou un bouton URL d'appel à l'action.
locationRequestlibre*{ "locationRequest": { "body": "Où livrer ?" } } — affiche un bouton Envoyer la position.
rawlibre*É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.

POST /api/v1/messages (template, simple)
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…" } }
POST /api/v1/messages (template, en-tête + corps + bouton)
{
  "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).

POST /api/v1/messages (authentification)
{
  "connectionId": "con_1",
  "to": "+237690000001",
  "template": { "name": "verification_code", "language": "en_US", "otp": "472913" }
}

Messages de session en format libre

text
{ "connectionId": "con_1", "to": "+237690000001", "text": "Thanks, talk soon!" }
image (avec légende)
{ "connectionId": "con_1", "to": "+237690000001",
  "image": { "link": "https://acme.co/promo.jpg", "caption": "New arrivals 🎉" } }
document
{ "connectionId": "con_1", "to": "+237690000001",
  "document": { "link": "https://acme.co/invoice.pdf", "filename": "invoice-1024.pdf" } }
boutons de réponse interactifs
{
  "connectionId": "con_1",
  "to": "+237690000001",
  "buttons": {
    "body": "Confirm your order?",
    "footer": "Acme Coffee",
    "buttons": [
      { "id": "yes", "title": "Confirm" },
      { "id": "no",  "title": "Cancel" }
    ]
  }
}
liste interactive
{
  "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" }
      ] }
    ]
  }
}
bouton URL d'appel à l'action
{
  "connectionId": "con_1",
  "to": "+237690000001",
  "cta": { "body": "Your receipt is ready.", "displayText": "View receipt", "url": "https://acme.co/r/1024" }
}
réponse dans le fil + réaction
{ "connectionId": "con_1", "to": "+237690000001", "replyTo": "wamid.HBg…", "text": "On its way!" }

{ "connectionId": "con_1", "to": "+237690000001",
  "reaction": { "messageId": "wamid.HBg…", "emoji": "👍" } }

Abonnement

MéthodeEndpointDescription
GET/api/v1/subscriptionPlan, limites et usage actuels
GET /api/v1/subscription
{
  "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

Exemples
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 panne

Ce 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 :

422 — refus de Meta
{
  "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.

Sur cette page