Référence API
API Partenaire
Une API REST pour tout piloter par programmation : clients, connexions, templates, campagnes, messages et abonnement. Construisez votre propre interface 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 au nom des clients 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 (tous vos clients), soit rattachée à un seul client. Choisissez le client au moment de générer la clé dans Clés API. Une clé client ne résout que ce client : les listes ne renvoient que ses lignes, un id appartenant à un autre client répond 404, et GET /api/v1/subscription répond 403 — vous pouvez donc la remettre à ce client (ou l'utiliser pour son intégration) sans exposer vos autres clients. Les clés globales fonctionnent exactement comme avant.
Conventions#
URL de base https://wha-platform.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 partenaire. Les erreurs utilisent les codes de statut HTTP avec { "error": "code", "message": "…" }.
Entreprises (clients)#
| GET | /api/v1/companies | Lister les clients onboardés avec leurs connexions |
| GET | /api/v1/companies/{id} | Un client unique + connexions + décomptes |
curl https://wha-platform.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/connections | Lister 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/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 |
| 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 :
| 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 unexample 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://wha-platform.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é.
| 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 messagetemplate avec le raccourci otp — voir Messages → template d'authentification.Campagnes#
| 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://wha-platform.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://wha-platform.genuka.com/api/v1/campaigns/cmp_9/launch \
-H "Authorization: Bearer pk_live_xxx"
{ "data": { "sent": 2, "failed": 0, "skipped": 0 } }Limites
La plateforme ne facture ni ne comptabilise les messages — Meta facture directement le WABA de chaque client. Si la limite de campagnes de votre abonnement est atteinte en cours d'envoi, les destinataires restants sontskipped (relancez à la période suivante ou passez à un forfait supérieur). 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/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 wamidd'un message reçu) pour citer/répondre à un message.
| TEMPLATE | billable | Template pré-approuvé. Le seul moyen de démarrer une conversation en dehors de la fenêtre de 24 h. |
| TEXT | free* | { "text": "Hi" } or { "text": { "body": "…", "previewUrl": true } } |
| IMAGE / VIDEO / AUDIO / DOCUMENT / STICKER | free* | { "image": { "link": "…" } } or { "id": "<media-id>" }; caption/filename optional |
| LOCATION | free* | { "location": { "latitude": …, "longitude": …, "name": "…", "address": "…" } } |
| CONTACTS | free* | { "contacts": [ … Meta contact objects … ] } |
| REACTION | free* | { "reaction": { "messageId": "wamid…", "emoji": "👍" } } |
| BUTTONS / LIST / CTA | free* | Boutons de réponse interactifs, un menu liste, ou un bouton URL d'appel à l'action. |
| LOCATIONREQUEST | free* | { "locationRequest": { "body": "Where should we deliver?" } } — affiche un bouton Envoyer la position. |
| RAW | free* | É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 envoistemplate 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 (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://wha-platform.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#
| GET | /api/v1/subscription | Plan, limites et usage actuels |
{
"plan": { "code": "pro", "name": "Pro" },
"interval": "monthly",
"currency": "XAF",
"status": "active",
"currentPeriodEnd": "2026-07-18T00:00:00.000Z",
"usage": { "clients": 7, "numbers": 9, "campaigns": 142 },
"limits": { "maxClients": 20, "maxNumbers": 20, "monthlyCampaigns": 2000 }
}Erreurs#
401 { "error": "missing_bearer_token" } // no Authorization header
401 { "error": "invalid_token" } // unknown / revoked key, or inactive partner
403 { "error": "account_deactivated", "message": "Deactivated Account" }
// the client is suspended: its own key stops
// working, and any request naming it is refused
// — including with a partner-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_clients" } // subscription client limit exceeded
402 { "error": "plan_limit_numbers" } // subscription number limit exceeded
402 { "error": "plan_limit_campaigns" } // subscription campaign limit exceeded
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