Référence API

API REST

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": "…" }.

Entreprises (comptes connectés)#

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)#

GET/api/v1/connectionsLister les connexions WhatsApp (optionnel ?companyId=)

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).

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

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).
i

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 (named)
{
  "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é.

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 (authentication)
{
  "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..." }].

i

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.

Campagnes#

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 } }
i

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.

Messages#

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 wamidd'un message reçu) pour citer/répondre à un message.

TEMPLATEbillableTemplate pré-approuvé. Le seul moyen de démarrer une conversation en dehors de la fenêtre de 24 h.
TEXTfree*{ "text": "Hi" } or { "text": { "body": "…", "previewUrl": true } }
IMAGE / VIDEO / AUDIO / DOCUMENT / STICKERfree*{ "image": { "link": "…" } } or { "id": "<media-id>" }; caption/filename optional
LOCATIONfree*{ "location": { "latitude": …, "longitude": …, "name": "…", "address": "…" } }
CONTACTSfree*{ "contacts": [ … Meta contact objects … ] }
REACTIONfree*{ "reaction": { "messageId": "wamid…", "emoji": "👍" } }
BUTTONS / LIST / CTAfree*Boutons de réponse interactifs, un menu liste, ou un bouton URL d'appel à l'action.
LOCATIONREQUESTfree*{ "locationRequest": { "body": "Where should we deliver?" } } — affiche un bouton Envoyer la position.
RAWfree*Échappatoire pour tout le reste (Flows, adresse, catalogue, appel vocal) : un fragment de type Cloud API complet.
i

La fenêtre de 24 heures

Seuls les envois template peuvent démarrer une conversation. Tout ce qui est marqué free* 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 (sentdeliveredread, 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, header + body + button)
{
  "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 (authentication)
{
  "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 (with caption)
{ "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" } }
interactive reply buttons
{
  "connectionId": "con_1",
  "to": "+237690000001",
  "buttons": {
    "body": "Confirm your order?",
    "footer": "Acme Coffee",
    "buttons": [
      { "id": "yes", "title": "Confirm" },
      { "id": "no",  "title": "Cancel" }
    ]
  }
}
interactive list
{
  "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" }
      ] }
    ]
  }
}
call-to-action URL button
{
  "connectionId": "con_1",
  "to": "+237690000001",
  "cta": { "body": "Your receipt is ready.", "displayText": "View receipt", "url": "https://acme.co/r/1024" }
}
reply in-thread + reaction
{ "connectionId": "con_1", "to": "+237690000001", "replyTo": "wamid.HBg…", "text": "On its way!" }

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

Abonnement#

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,                                  // numbers paid for this period
  "usage": { "numbers": 9, "messages": 12480, "seats": 3 },
  "limits": { "maxNumbers": 9, "monthlyMessages": 45000, "maxSeats": 5 }
}

Erreurs#

Exemples
401 { "error": "missing_bearer_token" }      // no Authorization header
401 { "error": "invalid_token" }             // unknown / revoked key, or inactive account
403 { "error": "account_deactivated", "message": "Deactivated Account" }
                                              // that business is suspended: its own key stops
                                              // working, and any request naming it is refused
                                              // — including with an account-wide key
400 { "error": "missing_fields", "message": "…" }
400 { "error": "missing_content", "message": "…" }  // no content field on a message send
400 { "error": "invalid_media", "message": "…" }    // media without an id or link
402 { "error": "plan_limit_numbers" }         // every number the plan paid for is in use
402 { "error": "plan_limit_messages" }        // the period's message allowance is exhausted
402 { "error": "subscription_past_due" }      // trial or paid period lapsed without renewal
404 { "error": "connection_not_found" }
404 { "error": "template_not_found" }
409 { "error": "template_exists" }
502 { "error": "send_failed", "message": "…" }     // Meta rejected the send
502 { "error": "meta_rejected", "message": "…" }   // Meta rejected the template