# Introduction
> Bref Point Inbox est une API REST pour connecter les comptes WhatsApp Business de vos clients, envoyer et recevoir des messages, et être notifié par webhooks signés.
Bref Point Inbox est une **infrastructure de messagerie pour développeurs et éditeurs de logiciels**. Elle vous évite de gérer vous-même la complexité de la plateforme WhatsApp Business de Meta : inscription de vos clients, jetons, webhooks, files d'attente, reprises, suivi des coûts.
Bref Point Inbox est édité par Labred Industry. Il s'appuie sur le statut de Tech Provider Meta de Bref Point, mais **n'est ni édité, ni approuvé par Meta**.
## Ce que vous pouvez faire
- **Connecter les comptes WhatsApp Business de vos clients** par l'inscription intégrée de Meta, y compris en mode coexistence (le client garde l'application WhatsApp Business sur son téléphone). [Guide](/docs/guides/connect-whatsapp)
- **Envoyer des messages** : texte, images, documents, vidéos, audios, localisation et templates. L'envoi est asynchrone, idempotent et repris automatiquement en cas de panne. [Guide](/docs/guides/send-messages)
- **Recevoir les messages et les statuts** par webhooks signés : reçu, envoyé, délivré, lu, échoué. [Guide](/docs/guides/webhooks)
- **Gérer les templates** : soumission à Meta, suivi de la revue, refus explicite avant l'envoi. [Guide](/docs/guides/templates)
- **Tester sans WhatsApp** grâce au bac à sable et à son simulateur. [Guide](/docs/guides/sandbox)
- **Suivre la consommation** : par jour, catégorie, pays ou numéro, avec le coût Meta et les frais Bref Point toujours séparés. [Guide](/docs/guides/usage-billing)
## Les deux règles à connaître avant de commencer
Un message libre (texte, image…) n'est accepté que si le destinataire vous a écrit **depuis moins de 24 heures**. En dehors de cette fenêtre, seul un **template approuvé par Meta** peut être envoyé. L'API refuse un envoi hors fenêtre avec l'erreur `outside_service_window`.
WhatsApp exige que la personne ait accepté de recevoir vos messages. Cette obligation vous incombe : Bref Point Inbox ne peut pas la vérifier à votre place.
## Par où commencer
1. [Démarrage rapide](/docs/quickstart) — votre premier message en quelques minutes, dans le bac à sable.
2. [Authentification et clés](/docs/authentication) — clés API, portées, environnements.
3. [Concepts](/docs/concepts) — comptes, applications, entreprises connectées, numéros, messages.
## Qui paie Meta ?
Meta facture **directement le propriétaire du compte WhatsApp Business**, selon la catégorie du message et le pays du destinataire. Bref Point Inbox ne refacture pas les conversations : il facture ses propres frais (abonnement, et un frais par message au-delà du quota de votre plan), toujours sur une ligne séparée. [Détails](/docs/guides/usage-billing)
## Pour les agents IA et les outils
Toute la documentation est disponible en Markdown brut (ajoutez `.md` à l'adresse d'une page), dans [`/llms.txt`](/llms.txt) et [`/llms-full.txt`](/llms-full.txt), et la spécification OpenAPI est sur [`/openapi.json`](/openapi.json). [En savoir plus](/docs/ai)
---
# Démarrage rapide
> Créez un compte, générez une clé de bac à sable, déclarez un webhook et envoyez votre premier message en quelques minutes.
Ce parcours utilise le **bac à sable** : rien n'est facturé et aucun vrai destinataire n'est contacté. Les exemples s'exécutent dans votre terminal ; chaque bloc existe en curl, Node.js, Python et PHP (votre choix de langage est mémorisé).
Définissez d'abord l'adresse de l'API :
```bash
export INBOX_API="https://VOTRE-HOTE/api/v1"
```
### Créez votre compte
La création d'un compte crée aussi son premier membre (propriétaire) et ouvre une session.
Le mot de passe doit contenir **10 caractères au minimum**. Conservez le `sessionToken` : c'est la clé de la console, valable 30 jours.
```bash
export INBOX_SESSION="ses_…"
```
### Créez une application
Une application a **deux environnements** : `sandbox` pour tester et `production` pour envoyer à de vrais destinataires. Ils sont strictement séparés.
### Générez une clé de sandbox
Choisissez ses **portées** : une clé ne peut faire que ce que vous lui accordez.
Copiez le champ `secret` maintenant. Il n'est stocké nulle part : en cas de perte, générez une nouvelle clé et révoquez l'ancienne.
```bash
export INBOX_KEY="pk_test_…"
```
### Déclarez l'entreprise de votre client
Une **entreprise connectée** représente votre client, propriétaire d'un compte WhatsApp Business. `externalRef` est votre propre identifiant de ce client.
### Connectez son compte WhatsApp
Votre client termine l'inscription intégrée de Meta dans le navigateur ; vous recevez un `code`, un `wabaId` et un `phoneNumberId` que vous transmettez à l'API. Voir [Connecter un compte WhatsApp](/docs/guides/connect-whatsapp).
Notez l'`id` du numéro (`phoneNumber.id`) : c'est lui qui désigne le numéro d'envoi.
### Recevez les événements
Déclarez la destination de vos webhooks. Le secret de signature n'est affiché qu'une fois.
Vérifiez que votre serveur répond bien, avec un événement de test signé :
### Envoyez votre premier message
La réponse est immédiate (`202`, statut `queued`) : l'envoi réel est asynchrone. Suivez le statut avec `GET /messages/{id}` ou, mieux, avec vos webhooks.
Un message libre (`type: text`) n'est accepté que dans la fenêtre de 24 heures suivant un message du destinataire. Un template approuvé fonctionne à tout moment. Voir [Envoyer des messages](/docs/guides/send-messages).
### Simulez une réponse du client
Sans téléphone, faites « arriver » un message : votre webhook `message.received` se déclenche exactement comme en production.
## Et ensuite
- [Authentification et clés](/docs/authentication) : portées, rotation, bonnes pratiques.
- [Webhooks](/docs/guides/webhooks) : vérifier la signature, dédoublonner, rejouer.
- [Fiabilité](/docs/guides/reliability) : idempotence et reprises.
- [Passer en production](/docs/guides/usage-billing) : plan, quotas, coûts.
---
# Authentification et clés
> Clés API, session de la console, portées, environnements et bonnes pratiques de sécurité.
## Deux sortes d'identifiants
| | Session de la console | Clé API |
|---|---|---|
| **Forme** | `ses_…` | `pk_test_…` (sandbox) ou `pk_live_…` (production) |
| **Sert à** | Gérer votre compte : applications, clés, facturation, journaux | Appeler l'API depuis votre serveur : connexions, messages, templates, webhooks, usage |
| **Obtenue par** | `POST /auth/signup` ou `POST /auth/login` | `POST /apps/{appId}/keys` |
| **Durée** | 30 jours, révocable (`POST /auth/logout`) | Jusqu'à révocation |
| **En-tête** | `Authorization: Bearer ses_…` | `Authorization: Bearer pk_…` |
Les routes de la section « Authentification », « Applications », `GET /billing/overview` et `GET /logs/requests` utilisent la **session**. Toutes les autres utilisent une **clé API**. Chaque page de la [référence](/docs/api) indique son mode d'authentification.
### La console utilise les mêmes routes
Les routes de l'API à clé (connexions, messages, templates, webhooks, usage) acceptent **aussi la session de la console**, avec un en-tête `X-Environment-Id` qui désigne l'environnement visé (l'identifiant d'un environnement est renvoyé par `GET /apps`). Le membre connecté a alors **toutes les portées** de son compte. C'est ce que fait la console de Bref Point Inbox. Ces appels ne figurent pas dans le [journal des appels](/docs/api/logs), réservé aux clés API.
Pour un usage depuis votre serveur, utilisez toujours une clé : elle limite ses droits par portée et son environnement est fixé.
## Créer une clé
Seule une empreinte irréversible est conservée. En cas de perte, créez une nouvelle clé et révoquez l'ancienne. La liste des clés n'affiche que leur **préfixe** (`prefix`) pour les reconnaître.
## Les portées
Une clé ne peut faire que ce que vous lui accordez. Un appel qui exige une portée absente de la clé est refusé avec `403 forbidden`, et le message nomme la portée manquante.
| Portée | Autorise |
|---|---|
| `messages:send` | Envoyer des messages (`POST /messages`) et utiliser le simulateur du bac à sable |
| `messages:read` | Lire un message ou l'historique (`GET /messages`) |
| `connections:manage` | Créer des entreprises connectées et connecter des comptes WhatsApp |
| `connections:read` | Lister les entreprises connectées, leurs comptes et leurs numéros |
| `templates:manage` | Créer, lister, synchroniser et supprimer des templates |
| `webhooks:manage` | Gérer les destinations de webhooks et consulter/rejouer les livraisons |
| `usage:read` | Lire la consommation (`GET /usage`) |
Donnez à chaque service **seulement** les portées dont il a besoin : le serveur qui envoie des rappels n'a pas besoin de `webhooks:manage`.
## Les environnements
Chaque application a un environnement **sandbox** et un environnement **production**. Une clé appartient à **un seul** environnement et ne voit que ses données : entreprises connectées, numéros, messages, destinations de webhooks.
- Une clé `pk_test_…` ne peut jamais toucher la production, et inversement.
- Le simulateur du [bac à sable](/docs/guides/sandbox) refuse les clés de production (`403 sandbox_only`).
- Le bac à sable n'est ni limité, ni facturé.
## Utiliser une clé
```bash
curl "$INBOX_API/messages" -H "Authorization: Bearer $INBOX_KEY"
```
Une clé absente, invalide ou révoquée donne `401 unauthorized`.
## Lister et révoquer
`lastUsedAt` indique la dernière utilisation : une clé jamais utilisée depuis des semaines est une bonne candidate à la révocation.
La révocation est **immédiate** et définitive.
## Bonnes pratiques
- **Appelez l'API depuis votre serveur, jamais depuis un navigateur ou une application mobile.** Une clé placée dans du code client est publique. L'API n'autorise d'ailleurs pas les appels depuis un navigateur (pas de CORS).
- Stockez les clés dans des **variables d'environnement** ou un gestionnaire de secrets, jamais dans le code source ni dans un dépôt.
- Utilisez **une clé par service** et par environnement : vous pourrez en révoquer une sans interrompre les autres.
- **Tournez vos clés** : créez la nouvelle, déployez-la, vérifiez que `lastUsedAt` de l'ancienne ne bouge plus, puis révoquez-la.
- Surveillez le [journal des appels](/docs/api/logs) : chaque appel y apparaît avec son statut et son `requestId`.
Le secret `whsec_…` d'une destination de webhook est **différent** d'une clé API : il sert uniquement à vérifier la signature des événements que nous vous envoyons. Voir [Webhooks](/docs/guides/webhooks).
---
# Concepts
> Comptes, applications, environnements, entreprises connectées, numéros, messages et événements : le vocabulaire de Bref Point Inbox.
## Le modèle en un coup d'œil
```text title="Qui possède quoi"
Compte (votre société)
└─ Application (« Mon CRM »)
├─ Environnement sandbox ──┐
└─ Environnement production┤ chacun contient, strictement séparés :
│
├─ Clés API
├─ Destinations de webhooks
└─ Entreprises connectées (vos clients)
└─ Compte WhatsApp Business (WABA)
├─ Templates
└─ Numéros
└─ Messages et événements
```
| Terme | Signification |
|---|---|
| **Compte** | Vous : le développeur ou l'éditeur qui utilise Bref Point Inbox. C'est l'unité d'isolation : aucune donnée n'est jamais visible d'un compte à l'autre. |
| **Application** | Un projet de votre côté. Elle regroupe vos environnements et vos clés. |
| **Environnement** | `sandbox` (test, gratuit, illimité) ou `production`. Deux univers étanches. |
| **Entreprise connectée** | **Votre client**, propriétaire d'un compte WhatsApp Business. Vous en créez autant que vous avez de clients. |
| **Compte WhatsApp (WABA)** | Le compte WhatsApp Business de votre client chez Meta. Il porte les templates. |
| **Numéro** | Un numéro WhatsApp d'envoi et de réception, rattaché à un compte WhatsApp. C'est lui que vous désignez par `phoneNumberId` pour envoyer. |
| **Message** | Un message envoyé (`outbound`) ou reçu (`inbound`) par un numéro. |
| **Événement** | Une notification envoyée à vos webhooks : message reçu, délivré, lu, échoué… |
`phoneNumberId` dans l'API Bref Point Inbox est **notre** identifiant du numéro (un UUID). L'identifiant que Meta donne au numéro (chiffres seulement) n'apparaît que lors de la [connexion du compte](/docs/guides/connect-whatsapp), sous le même nom de champ dans le corps de cette requête.
## Le cycle de vie d'un message sortant
```text
queued ──► sent ──► delivered ──► read
│ │ │
└─────────┴──────────┴──► failed
```
| Statut | Signification |
|---|---|
| `queued` | Accepté par l'API, en attente d'envoi. La réponse `202` signifie exactement cela. |
| `sent` | Remis à Meta. |
| `delivered` | Meta a remis le message à l'appareil du destinataire. |
| `read` | Le destinataire a lu le message (si ses paramètres de confirmation de lecture le permettent). |
| `failed` | Refusé ou non remis ; `error.code` et `error.message` expliquent pourquoi. |
| `received` | (messages `inbound` uniquement) message reçu d'un client. |
Un statut **ne recule jamais** : si Meta nous livre les événements dans le désordre ou en double, un message `read` ne redevient pas `delivered`, et un échec n'écrase pas un message déjà lu.
## Les principes de l'API
- **Asynchrone** : `POST /messages` répond tout de suite ; l'envoi réel et ses reprises se font en arrière-plan. Suivez l'avancement par [webhooks](/docs/guides/webhooks).
- **Idempotente** : l'en-tête `Idempotency-Key` garantit qu'un envoi rejoué n'est jamais exécuté deux fois. Voir [Fiabilité](/docs/guides/reliability).
- **Isolée** : une ressource d'un autre compte ou d'un autre environnement répond `404`, comme si elle n'existait pas.
- **Observable** : chaque réponse porte un en-tête `X-Request-Id`, repris dans les erreurs et dans le [journal des appels](/docs/api/logs).
---
# Connecter un compte WhatsApp
> Faites connecter à vos clients leur compte WhatsApp Business par l'inscription intégrée de Meta, puis finalisez la connexion par l'API.
Chacun de vos clients possède son propre compte WhatsApp Business. Vous le connectez à votre application en deux temps : le client **autorise la connexion chez Meta** (dans le navigateur), puis **votre serveur finalise** par l'API.
La finalisation par l'API décrite ci-dessous est en place. Elle a été vérifiée sur des réponses simulées de Meta ; sa **validation contre la plateforme Meta réelle est en cours**. Le bouton d'inscription prêt à l'emploi (qui lance l'inscription intégrée avec l'application Meta de Bref Point) est en cours d'intégration dans la console.
## Le parcours
```text
1. Votre serveur POST /connected-businesses → crée l'entreprise de votre client
2. Le navigateur Inscription intégrée de Meta → le client autorise, Meta renvoie un code,
l'identifiant du compte WhatsApp et celui du numéro
3. Votre serveur POST …/waba-connections → échange le code, abonne les événements, lit le numéro
4. Votre serveur GET …/phone-numbers → vérifie : numéro, nom vérifié, qualité
```
### Créez l'entreprise de votre client
`externalRef` est votre propre identifiant de ce client : il doit être unique dans l'environnement et vous permet de retrouver l'entreprise plus tard.
### Le client termine l'inscription chez Meta
Dans le navigateur, votre client se connecte à Meta, choisit ou crée son compte WhatsApp Business et son numéro. À la fin, Meta fournit à votre page trois valeurs :
| Valeur | À quoi elle sert |
|---|---|
| `code` | Échangé par nos serveurs contre le jeton d'accès du client. **À usage unique et de courte durée** : transmettez-le immédiatement. |
| `wabaId` | Identifiant du compte WhatsApp Business (chiffres uniquement). |
| `phoneNumberId` | Identifiant Meta du numéro (chiffres uniquement). |
### Finalisez par l'API
Nous échangeons le code contre un jeton, abonnons l'application aux événements du compte, lisons les informations du numéro et enregistrons le tout. La réponse donne l'`id` de la connexion et celui du **numéro** (`phoneNumber.id`) : c'est cet identifiant que vous utiliserez pour envoyer des messages.
### Vérifiez
`qualityRating` est la note de qualité du numéro chez Meta (`GREEN`, `YELLOW`, `RED`) : surveillez-la, une mauvaise qualité réduit vos capacités d'envoi.
## Le mode coexistence
Avec `"mode": "coexistence"`, votre client **continue d'utiliser l'application WhatsApp Business sur son téléphone** pendant que votre logiciel utilise l'API sur le même numéro. Les messages qu'il envoie depuis son téléphone arrivent chez vous comme les autres. Sans ce mode (`cloud_api`, par défaut), le numéro n'est utilisable que par l'API.
## Plusieurs numéros pour une même entreprise
Une entreprise peut avoir plusieurs comptes WhatsApp, et un compte plusieurs numéros. Pour **ajouter un numéro**, refaites l'inscription et appelez de nouveau `POST …/waba-connections` avec le même `wabaId` et le nouveau `phoneNumberId` : le compte est reconnu, son jeton rafraîchi et le numéro ajouté, sans doublon. Reconnecter un numéro **déjà connecté** n'en ajoute pas et ne compte pas dans la limite.
En production, votre plan limite les numéros **par entreprise** (1 en Gratuit, 2 en Starter, 3 en Growth, 5 en Scale). Le sandbox n'a pas de limite.
## Ce qui est garanti
- **Le jeton Meta de votre client n'est jamais renvoyé** par l'API, même chiffré. Il est stocké chiffré, lié à votre compte et à cette connexion.
- **Un compte WhatsApp ne peut être connecté qu'une fois sur toute la plateforme.** S'il l'est déjà ailleurs, vous obtenez `409 conflict`. Cela empêche qu'un tiers détourne les messages d'un autre.
- **Reconnecter le même compte** (jeton expiré, nouvelle autorisation) est possible : le jeton est remplacé, sans doublon de numéro.
- Une entreprise ne voit que **son** environnement : un compte connecté en sandbox n'existe pas en production.
## Erreurs fréquentes
| Erreur | Cause probable |
|---|---|
| `400 validation_error` | `wabaId` ou `phoneNumberId` contient autre chose que des chiffres |
| `404 not_found` | L'entreprise n'existe pas, ou appartient à un autre environnement que celui de la clé |
| `409 conflict` | Ce compte WhatsApp est déjà connecté (à une autre entreprise ou à un autre compte) |
| `402 plan_limit_reached` | Le plan de votre compte n'autorise pas plus d'entreprises connectées en production, **ou plus de numéros pour cette entreprise**. Le refus intervient **avant** l'échange du code d'inscription : le code n'est pas consommé |
| `502 meta_error` | Meta a refusé la requête : le plus souvent, le `code` a expiré ou a déjà servi. Faites refaire l'inscription au client |
---
# Envoyer des messages
> Textes, médias, localisation et templates : formats, fenêtre de 24 heures, statuts, historique et erreurs.
`POST /messages` **accepte** un message et répond tout de suite (`202`, statut `queued`). L'envoi réel à Meta, ses reprises éventuelles et le suivi des statuts se font en arrière-plan.
## Les champs communs
| Champ | Description |
|---|---|
| `phoneNumberId` | Le numéro d'envoi : l'`id` renvoyé par la [connexion du compte](/docs/guides/connect-whatsapp). |
| `to` | Le destinataire, **en format international avec « + »** : `+237600000000`. Les espaces, tirets et parenthèses sont ignorés. |
| `type` | `text`, `template`, `image`, `video`, `audio`, `document` ou `location`. |
Un numéro local sans indicatif (`600000000`) est refusé, pour qu'il ne parte jamais dans le mauvais pays. Dans les réponses, le numéro est rendu sans le « + » (`237600000000`).
## Un template (à tout moment)
Un template **approuvé par Meta** peut être envoyé à tout moment, même sans échange préalable. C'est la seule façon de **démarrer** une conversation. Voir [Templates](/docs/guides/templates).
## Un message libre : la fenêtre de 24 heures
Un message libre n'est accepté que si le destinataire vous a **écrit depuis moins de 24 heures** sur ce numéro. Sinon :
Dans la fenêtre, tous les types de messages sont possibles.
### Texte
Le corps du texte fait de 1 à 4096 caractères.
### Image
Pour les médias (`image`, `video`, `audio`, `document`), indiquez **soit** `link` (une URL `https://` publique), **soit** `id` (un média déjà hébergé par Meta), jamais les deux. La `caption` n'existe pas pour l'audio. Le téléversement et le téléchargement de médias par l'API ne sont pas encore disponibles.
### Document
### Localisation
## Suivre un message
`error` vaut `null` tant que tout va bien ; en cas d'échec, il contient le `code` et le `message` de Meta. Pour être notifié sans interroger l'API, utilisez les [webhooks](/docs/guides/webhooks) (`message.sent`, `message.delivered`, `message.read`, `message.failed`).
## L'historique
L'historique est trié du plus récent au plus ancien et **paginé par curseur** : tant que `nextCursor` n'est pas `null`, passez-le dans `?cursor=` pour obtenir la page suivante. Filtres : `phoneNumberId`, `direction` (`inbound`, `outbound`), `status`, `limit` (1 à 100).
## Les erreurs d'envoi
| Code | HTTP | Cause |
|---|---|---|
| `invalid_message` | 422 | Numéro mal formé, texte vide, média ambigu (`id` et `link` à la fois), lien non `https` |
| `outside_service_window` | 422 | Message libre hors de la fenêtre de 24 heures |
| `template_not_approved` | 422 | Le template existe mais n'est pas (ou plus) approuvé |
| `idempotency_conflict` | 422 | Même `Idempotency-Key` pour une requête différente |
| `quota_exceeded` | 402 | Quota mensuel de votre plan atteint (production) |
| `not_found` | 404 | Numéro inconnu, ou d'un autre environnement |
Les refus de Meta pendant l'envoi (template introuvable, destinataire non joignable…) ne se voient pas dans la réponse `202` : le message passe en `failed`, avec le motif de Meta, et vous recevez `message.failed`.
Ajoutez toujours un en-tête `Idempotency-Key` à vos envois : si votre appel expire ou si votre serveur redémarre, rejouez-le tel quel, le message ne partira jamais deux fois. Voir [Fiabilité](/docs/guides/reliability).
---
# Recevoir des messages
> Recevez les messages de vos clients par webhook, répondez dans la fenêtre de 24 heures et retrouvez-les dans l'historique.
Quand un client écrit à l'un de vos numéros, Bref Point Inbox enregistre le message et envoie un événement **`message.received`** à vos destinations de webhooks. Déclarez-en une avant tout (voir [Webhooks](/docs/guides/webhooks)).
## L'événement
```json title="message.received"
{
"id": "8dc9eca4-2eff-46de-80aa-0ac9674dd1ae",
"type": "message.received",
"createdAt": "2026-11-12T06:09:17.388Z",
"data": {
"messageId": "4f21edc3-047e-4f75-819a-1e69953f40a3",
"waMessageId": "wamid.HBgM…",
"phoneNumberId": "fb10613a-8a2c-4340-87ff-192082ffaf1f",
"from": "237611111111",
"type": "text",
"content": { "text": { "body": "Bonjour, je voudrais déplacer mon rendez-vous" } },
"contactName": "Awa",
"occurredAt": "2026-11-12T06:09:17.402Z"
}
}
```
| Champ de `data` | Description |
|---|---|
| `messageId` | Identifiant du message chez Bref Point Inbox. |
| `waMessageId` | Identifiant du message chez WhatsApp. |
| `phoneNumberId` | Le numéro qui a reçu le message (`id` du numéro, comme pour l'envoi). |
| `from` | Le numéro de l'expéditeur, sans « + ». |
| `type` | `text`, `image`, `document`, `audio`, `video`, `location`, `sticker`, `button`… tel que Meta le déclare. |
| `content` | Le contenu, sous une clé du nom du type : `{ "text": { "body": … } }`, `{ "image": { "id": …, "caption": … } }`… La forme exacte est celle de Meta. |
| `contactName` | Le nom de profil WhatsApp de l'expéditeur, s'il est connu. |
Pour un média reçu, `content` contient l'identifiant du média chez Meta. Le téléchargement du fichier par l'API n'est pas encore disponible.
## Répondre
Un message reçu **ouvre la fenêtre de 24 heures** pour ce numéro et cet expéditeur : vous pouvez lui répondre par un message libre.
```bash
curl -X POST "$INBOX_API/messages" \
-H "Authorization: Bearer $INBOX_KEY" -H "Content-Type: application/json" \
-d '{ "phoneNumberId": "", "to": "+", "type": "text",
"text": { "body": "Bien sûr, quel créneau vous conviendrait ?" } }'
```
La fenêtre est calculée **par expéditeur et par numéro** : un message d'un autre client n'ouvre pas la fenêtre de celui-ci.
## Ne pas traiter deux fois
Les événements peuvent arriver **plus d'une fois** (nouvel essai après un délai d'attente, rejeu manuel, renvoi par Meta). Bref Point Inbox dédoublonne les messages reçus à l'enregistrement, mais **votre récepteur** doit aussi être tolérant : l'`id` de l'événement (et `data.messageId`) est stable, ignorez ce que vous avez déjà traité.
## Retrouver les messages reçus
Ajoutez `?direction=inbound` pour ne voir que les messages reçus.
## Tester sans téléphone
Le [bac à sable](/docs/guides/sandbox) fait « arriver » un message à la demande et déclenche votre vrai webhook :
---
# Templates
> Soumettre des templates à la revue de Meta, suivre leur statut, les synchroniser et les utiliser dans vos envois.
Un **template** est un modèle de message pré-approuvé par Meta. C'est le seul moyen d'écrire à quelqu'un qui ne vous a pas écrit dans les dernières 24 heures. Les templates appartiennent à un **compte WhatsApp** (WABA) : tous les numéros de ce compte partagent les mêmes templates.
## Le cycle de vie
```text
Vous soumettez ──► pending ──► approved (utilisable)
└──► rejected (motif dans rejectionReason)
paused / disabled (qualité insuffisante : décidé par Meta)
```
Le `status` est celui de Meta, en minuscules. **Seul `approved` est envoyable** (`usable: true`).
## Créer un template
| Règle | Détail |
|---|---|
| `name` | Minuscules, chiffres et underscores : `rappel_rdv`. |
| `language` | `fr`, `en`, `en_US`… Un même nom peut exister en plusieurs langues. |
| `category` | `MARKETING`, `UTILITY` ou `AUTHENTICATION`. Meta décide de la catégorie retenue et de la tarification qui en découle. |
| `components` | Au format Meta. Le composant `BODY` est obligatoire ; `HEADER`, `FOOTER` et `BUTTONS` sont facultatifs. Les variables s'écrivent avec des doubles accolades numérotées, et le champ `example` fournit une valeur d'exemple pour chaque variable. |
L'API valide le brouillon (nom, langue, catégorie, présence du `BODY`) **avant** d'appeler Meta et répond `400 validation_error` avec le détail si quelque chose ne va pas. Meta applique ensuite ses propres règles de contenu : un rejet arrive plus tard, en statut `rejected`.
## Suivre la revue
Vous avez deux moyens :
- **Les webhooks** : l'événement `template.status_updated` vous prévient dès que Meta statue.
- **La synchronisation**, un chemin de réparation plus qu'une boucle de scrutation :
La synchronisation aligne la base sur Meta : statuts mis à jour, et templates créés ailleurs (directement chez Meta, par exemple) ajoutés. Les changements de statut déclenchent aussi `template.status_updated`.
## Envoyer un template
Référencez-le par `name` et `language`. Les variables se passent dans `components`, dans l'ordre de leurs numéros :
```json title="Corps de POST /messages"
{
"phoneNumberId": "…",
"to": "+237600000000",
"type": "template",
"template": {
"name": "rappel_rdv",
"language": "fr",
"components": [
{ "type": "body", "parameters": [
{ "type": "text", "text": "Awa" },
{ "type": "text", "text": "14 novembre à 10 h" }
] }
]
}
}
```
Si le template existe chez Bref Point Inbox mais n'est **pas approuvé**, l'envoi est refusé immédiatement, avec le statut et le motif :
```json title="422 template_not_approved"
{ "error": { "code": "template_not_approved",
"message": "Le template « rappel_rdv » (fr) est « rejected » : INVALID_FORMAT. Seul un template approuvé peut être envoyé" } }
```
Un template **inconnu** de Bref Point Inbox (créé directement chez Meta et pas encore synchronisé) n'est pas bloqué en amont : c'est Meta qui tranche, et le message passe en `failed` si le template n'existe pas.
## Supprimer
La suppression retire le template **dans toutes ses langues**, chez Meta puis chez Bref Point Inbox.
La catégorie `AUTHENTICATION` est acceptée à la création. Les composants spécifiques aux codes à usage unique suivent le format de Meta.
---
# Webhooks
> Recevoir les événements en temps réel, vérifier leur signature, comprendre les reprises et rejouer une livraison.
Les webhooks vous notifient **dès qu'il se passe quelque chose** : message reçu, envoyé, délivré, lu, échoué, template approuvé ou rejeté. Bref Point Inbox envoie une requête `POST` JSON à l'adresse que vous déclarez.
## Déclarer une destination
Le champ `secret` (`whsec_…`) sert à **vérifier la signature** de chaque événement. Conservez-le dans vos variables d'environnement. Il se renouvelle avec `POST /webhook-endpoints/{id}/rotate-secret` ; l'ancien cesse alors de fonctionner.
L'URL doit être en **`https://`** et joignable publiquement. Les adresses `http://`, `localhost`, les réseaux privés et les noms internes sont refusés. Pour développer en local, exposez votre serveur par un tunnel HTTPS public.
Les destinations sont propres à un **environnement** : celles du sandbox ne reçoivent jamais d'événements de production.
### Vérifier la destination
Nous envoyons un événement `webhook.test` signé comme les autres et vous renvoyons la réponse de votre serveur (`ok`, code HTTP, durée, ou l'erreur de connexion).
## Les événements
| Type | Quand | Contenu de `data` |
|---|---|---|
| `message.received` | Un client vous écrit | `messageId`, `waMessageId`, `phoneNumberId`, `from`, `type`, `content`, `contactName`, `occurredAt` |
| `message.sent` | Le message est remis à Meta | `messageId`, `waMessageId`, `phoneNumberId`, `to`, `status`, `occurredAt` |
| `message.delivered` | Remis à l'appareil du destinataire | idem, plus `pricing` si Meta le fournit |
| `message.read` | Lu par le destinataire | idem |
| `message.failed` | Refusé ou non remis | `messageId`, `phoneNumberId`, `to`, `status`, `occurredAt`, `error` (`code`, `message`) |
| `template.status_updated` | Meta approuve, rejette ou met en pause un template | `templateId`, `wabaConnectionId`, `name`, `language`, `status`, `previousStatus`, `reason` |
Abonnez une destination à `*` pour tout recevoir, ou à une liste précise. `PATCH /webhook-endpoints/{id}` modifie l'abonnement.
### Le corps reçu
```json title="POST sur votre URL"
{
"id": "8dc9eca4-2eff-46de-80aa-0ac9674dd1ae",
"type": "message.delivered",
"createdAt": "2026-11-12T06:09:17.388Z",
"data": { "messageId": "7a6d4b4c-…", "status": "delivered", "occurredAt": "2026-11-12T06:09:19.002Z" }
}
```
Chaque requête porte aussi ces en-têtes :
| En-tête | Valeur |
|---|---|
| `BrefPoint-Signature` | `t=,v1=` : voir ci-dessous |
| `BrefPoint-Event-Type` | Le type de l'événement |
| `BrefPoint-Delivery-Id` | Identifiant de cette livraison |
| `Content-Type` | `application/json` |
## Vérifier la signature
**Vérifiez toujours la signature** : sans elle, n'importe qui pourrait appeler votre adresse en se faisant passer pour Bref Point Inbox.
La signature `v1` est le **HMAC-SHA256**, en hexadécimal, de la chaîne `"."`, avec votre secret `whsec_…`. `t` est l'horodatage (en secondes) de l'envoi : refusez les envois trop anciens (5 minutes par défaut) pour vous protéger du rejeu.
La signature porte sur les **octets exacts reçus**. Calculez-la **avant** tout `JSON.parse` / `json_decode` : un JSON ré-encodé (espaces, ordre des clés, accents) donne une autre signature. Avec Express, utilisez `express.raw({ type: "application/json" })` sur cette route.
Ces trois exemples sont **exécutés par les tests de Bref Point Inbox** contre la vraie signature : ce que vous copiez ici fonctionne.
## Fiabilité des livraisons
- **Répondez vite** par un code `2xx` (délai maximal : **10 secondes**), puis traitez. Une redirection (`3xx`) est un échec.
- Un échec est **réessayé jusqu'à 8 fois** au total, avec une attente qui double : environ 30 s, 1 min, 2 min, 4 min, 8 min, 16 min, puis 32 min (soit à peu près **une heure**).
- Les événements sont livrés **au moins une fois** : l'`id` est stable d'une tentative à l'autre, **dédoublonnez dessus**.
- L'**ordre n'est pas garanti** : un `message.read` peut précéder un `message.delivered`. Fiez-vous à `occurredAt`, et ne revenez jamais en arrière sur un statut.
- Après **15 livraisons définitivement échouées d'affilée**, la destination est **désactivée** (`status: "disabled"`, avec la raison). Réactivez-la avec `PATCH … { "enabled": true }` une fois votre récepteur réparé.
## Historique et rejeu
Chaque livraison et chaque tentative sont conservées (selon la rétention de votre plan).
`attemptsDetail` donne, pour chaque tentative, le code de réponse, la durée, l'erreur éventuelle et un extrait de la réponse de votre serveur : de quoi comprendre un échec sans deviner.
Votre récepteur était en panne ? Une fois réparé, **rejouez** :
Le rejeu crée une **nouvelle livraison** du même événement (même `id` d'événement) vers la même destination ; `replayOf` pointe vers l'originale. Il est refusé (`409`) si la livraison est encore en cours ou si la destination est désactivée.
---
# Tester avec le bac à sable
> Un environnement séparé, gratuit et illimité, avec un simulateur pour tester vos webhooks sans numéro WhatsApp.
Chaque application a un environnement **sandbox**, strictement séparé de la production : ses propres clés (`pk_test_…`), entreprises, numéros, webhooks et données.
| | Sandbox | Production |
|---|---|---|
| Clés | `pk_test_…` | `pk_live_…` |
| Limites de quota et de nombre d'entreprises | Aucune | Celles de votre plan |
| Facturation Bref Point Inbox | Aucune | Selon votre plan |
| Simulateur | Oui | **Refusé** (`403 sandbox_only`) |
Un compte connecté dans le sandbox est un compte de test Meta : les messages que vous **envoyez** partent réellement vers Meta (et vers les destinataires que ce compte de test autorise). Le **simulateur**, lui, ne contacte jamais Meta : il fabrique les événements localement.
## Simuler un message reçu
Idéal pour tester votre réception de bout en bout : le message est enregistré, la fenêtre de 24 heures s'ouvre pour cet expéditeur, et votre webhook `message.received` se déclenche exactement comme en production (les données portent `"simulated": true`).
## Faire avancer le statut d'un message
Après un envoi (`POST /messages`), faites évoluer le message comme le ferait Meta :
Les mêmes règles qu'en production s'appliquent : un statut **ne recule jamais**, et un `failed` ne remplace pas un message déjà `read`. Une transition sans effet répond simplement avec l'état actuel.
## Un scénario complet
1. Connectez un numéro dans le sandbox et déclarez un webhook sur `*` ([Démarrage rapide](/docs/quickstart)).
2. `POST /sandbox/simulate/inbound-message` : votre serveur reçoit `message.received`.
3. Répondez avec `POST /messages` (message libre, la fenêtre est ouverte).
4. `POST /sandbox/simulate/message-status` avec `delivered`, puis `read` : vous recevez les événements correspondants.
5. Provoquez une panne chez vous (réponse `500`) et observez les reprises dans `GET /webhook-deliveries`.
6. Quand tout est bon, créez une clé de production et reprenez les mêmes appels.
---
# Fiabilité : idempotence et reprises
> Comment l'API se comporte en cas de panne, de doublon ou de rejeu, et comment écrire une intégration robuste.
Une intégration de messagerie doit survivre aux pannes réseau, aux redémarrages et aux doublons. Voici ce que fait l'API pour vous, et ce qu'il vous reste à faire.
## Idempotence de l'envoi
Un envoi n'est pas anodin : le rejouer par erreur enverrait **deux fois** le même message à un client. L'en-tête `Idempotency-Key` l'empêche.
```bash
curl -X POST "$INBOX_API/messages" \
-H "Authorization: Bearer $INBOX_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: rdv-2026-11-14-client-42" \
-d '{ … }'
```
| Situation | Résultat |
|---|---|
| Première requête avec cette clé | Le message est accepté et enregistré. |
| Même clé, **même** requête | Aucun nouveau message : vous récupérez le message d'origine, avec l'en-tête `Idempotent-Replayed: true`. |
| Même clé, requête **différente** | `422 idempotency_conflict`. |
| Deux requêtes simultanées avec la même clé | L'une réussit, l'autre est refusée (`409`) : réessayez. |
**Choisissez une clé qui décrit l'intention métier** (`rappel-rdv-`), pas un identifiant aléatoire régénéré à chaque tentative : c'est ce qui rend le rejeu sûr.
## L'envoi est asynchrone
`202 Accepted` signifie **accepté**, pas **délivré**. Le message est en file, puis envoyé :
- **Panne passagère de Meta ou du réseau, limite de débit** : le message est réessayé automatiquement, jusqu'à 6 tentatives avec une attente croissante.
- **Refus définitif** (template introuvable, destinataire non joignable…) : pas de nouvelle tentative inutile ; le message passe tout de suite en `failed`, avec le code et le motif de Meta.
- **Débit** : les envois sont régulés **par numéro** (50 messages par seconde par défaut) : en cas de pic, ils attendent leur tour sans être perdus.
- **Redémarrage ou perte de la file** : un message resté `queued` plus de 5 minutes est repris automatiquement par une tâche de maintenance.
## Statuts : jamais en arrière
Meta peut livrer ses notifications en double ou dans le désordre. Bref Point Inbox applique une règle simple : un statut ne fait **que progresser** (`queued` → `sent` → `delivered` → `read`), et `failed` ne remplace pas un message déjà `read`. Même un `read` qui arrive avant son `delivered` est accepté, et l'étape manquante est complétée.
## Webhooks : au moins une fois
Chaque événement est livré **au moins une fois**, jamais « exactement une fois ». Votre récepteur doit donc être **idempotent** :
1. Vérifiez la signature.
2. Enregistrez l'`id` de l'événement (une table avec une contrainte d'unicité suffit).
3. S'il existe déjà, répondez `200` sans rien refaire.
4. Sinon, traitez, puis répondez `200`.
Voir [Webhooks](/docs/guides/webhooks) pour le calendrier des reprises et le rejeu.
## Ce qu'il vous reste à faire
- **Mettez un délai d'attente** sur vos appels (10 à 30 secondes) et **réessayez avec la même `Idempotency-Key`** en cas d'expiration, d'erreur réseau ou de `5xx`.
- **Ne réessayez pas** une erreur `4xx` sans corriger la requête : elle échouera pareil.
- Répondez à vos webhooks **en moins de 10 secondes** ; déléguez le travail long à une file chez vous.
- Surveillez `GET /webhook-deliveries?status=failed` et vos messages `failed`.
Si le service s'arrête exactement entre l'envoi à Meta et l'enregistrement du résultat, un nouvel essai pourrait produire un doublon. La fenêtre est très courte et ne peut pas être supprimée, car Meta n'offre pas de clé d'idempotence à l'envoi.
---
# Usage et facturation
> Qui paie quoi, comment lire votre consommation, et ce qui change entre le sandbox et la production.
Bref Point Inbox sépare **toujours** deux sortes de coûts, que vous voyez sur des lignes distinctes.
| | Qui facture | Pour quoi |
|---|---|---|
| **Coûts Meta** | Meta, **directement au propriétaire du compte WhatsApp** | Les conversations : selon la catégorie du message et le pays du destinataire |
| **Frais Bref Point Inbox** | Bref Point Inbox | L'infrastructure : un abonnement, et un frais par message au-delà du quota de votre plan |
Bref Point Inbox **ne refacture pas** les conversations. Si vous voulez que votre client paie à travers vous, c'est un choix commercial de votre côté : le propriétaire du compte WhatsApp reste redevable envers Meta.
## Lire votre consommation
Regroupez avec `groupBy` : `day` (par défaut), `category`, `country` ou `phone_number`. La période par défaut est le **mois en cours (UTC)** ; `from` et `to` la bornent.
```json title="Exemple illustratif (montants fictifs)"
{
"from": "2026-11-01T00:00:00.000Z",
"to": "2026-12-01T00:00:00.000Z",
"groupBy": "category",
"totals": {
"messages": 120,
"billableMessages": 100,
"notBillableMessages": 20,
"unpricedMessages": 0,
"metaCostEstimate": { "USD": 1.2 },
"platformFee": {}
},
"buckets": [ { "key": "utility", "messages": 100, "billableMessages": 100, "metaCostEstimate": { "USD": 1.2 } } ],
"metaCostIsEstimate": true
}
```
| Champ | Signification |
|---|---|
| `billableMessages` / `notBillableMessages` | Selon ce que **Meta annonce** pour chaque message (certains messages, comme une réponse dans la fenêtre de service, ne sont pas facturés). |
| `metaCostEstimate` | Une **estimation**, par devise, calculée avec la grille tarifaire configurée sur la plateforme. Jamais le montant de votre facture Meta. |
| `unpricedMessages` | Messages facturables pour lesquels aucun tarif n'est configuré. Ils ne sont **jamais chiffrés au hasard**. |
| `platformFee` | Les frais Bref Point Inbox (dépassement du quota inclus). Séparés du coût Meta, jamais additionnés. |
| `metaCostIsEstimate` | Toujours `true` : rappel que le coût Meta est estimé. |
Le **pays** est déduit du numéro du destinataire ; il peut être `inconnu`.
Les montants Meta que nous affichons sont des estimations utiles au pilotage, pas des factures. Le montant exact est celui de la facture que Meta adresse au propriétaire du compte.
## Votre plan
Le plan par défaut est **Gratuit**, volontairement très limité : il sert à valider une première intégration.
| | Gratuit | Starter | Growth | Scale |
|---|---|---|---|---|
| Prix par mois | 0 | 5 000 FCFA | 15 000 FCFA | 40 000 FCFA |
| Messages envoyés par mois en **production** | 100 (plafond) | 3 000 inclus | 15 000 inclus | 100 000 inclus |
| Entreprises connectées en **production** | 1 | 3 | 10 | 50 |
| Numéros WhatsApp par entreprise, en **production** | 1 | 2 | 3 | 5 |
| Au-delà du quota inclus | L'envoi s'arrête | 5 FCFA par message | 3 FCFA par message | 2 FCFA par message |
| Conservation des journaux | 3 jours | 14 jours | 30 jours | 90 jours |
Un plan **Enterprise** (volumes négociés, engagement de service) est proposé sur devis. Le détail et les prix actuels sont sur [la page des tarifs](/#tarifs) ; tant que le paiement en ligne n'est pas disponible, l'activation d'un plan payant est faite par l'équipe.
- La période de facturation est le **mois calendaire UTC**.
- **Le sandbox n'est ni limité ni facturé**, quel que soit le plan.
- Un plafond atteint donne `402 quota_exceeded` (envoi) ou `402 plan_limit_reached` (nouvelle entreprise, ou nouveau numéro au-delà de la limite par entreprise), avec un message qui indique la limite et votre consommation.
- Sur un plan payant, il n'y a pas de plafond dur : les messages au-delà du quota inclus sont facturés au frais par message de votre plan, sur la ligne `platformFee`, séparée du coût Meta.
## À propos des paiements
Le paiement en ligne des abonnements n'est pas encore disponible dans la console : les plans sont attribués par l'équipe Bref Point Inbox.
---
# Vue d'ensemble
> Conventions de l'API REST : adresse, versions, authentification, format des erreurs, pagination, identifiants et dates.
La référence complète est **générée à partir de la spécification OpenAPI réelle** de l'API : elle ne peut pas en diverger. Chaque opération est accompagnée d'un exemple d'appel (curl, Node.js, Python, PHP) et de la réponse **réelle** enregistrée depuis l'API.
La console interactive de l'API (`/api/docs`) permet d'appeler chaque route depuis le navigateur : cliquez sur « Authorize » et collez une clé **de sandbox**. La spécification brute est sur [`/openapi.json`](/openapi.json).
## Adresse et version
```text
https://VOTRE-HOTE/api/v1
```
La version majeure est dans l'adresse (`/v1`). Les changements qui ne cassent pas les intégrations (nouveaux champs, nouvelles routes, nouveaux événements) sont ajoutés à `/v1` : **ignorez les champs inconnus** dans vos réponses. Un changement incompatible donnerait une nouvelle version. Voir le [journal des changements](/docs/changelog).
## Authentification
`Authorization: Bearer `. Deux sortes de jetons : la **session** de la console (`ses_…`) et la **clé API** (`pk_test_…`, `pk_live_…`). Voir [Authentification et clés](/docs/authentication). Chaque opération indique celle qu'elle exige et la portée requise. Les routes à clé acceptent aussi la session de la console, avec un en-tête `X-Environment-Id`.
## Format des requêtes et des réponses
- Corps en **JSON** (`Content-Type: application/json`), en **UTF-8**.
- **Identifiants** : UUID, sauf les identifiants Meta (`wabaId`, `phoneNumberId` à la connexion), qui sont des chaînes de chiffres.
- **Dates** : ISO 8601, en **UTC** (`2026-11-12T06:09:17.388Z`).
- **Champs inconnus** dans une requête : rejetés (`400 validation_error`), pour détecter vite une faute de frappe.
- Les ressources d'un **autre compte ou d'un autre environnement** répondent `404`, comme si elles n'existaient pas.
## Erreurs
Toutes les erreurs ont **le même format**, quelle que soit leur origine :
```json
{
"error": {
"code": "validation_error",
"message": "Requête invalide : voir details",
"details": ["email must be an email"],
"requestId": "req_0a1b2c3d-…"
}
}
```
| Champ | Description |
|---|---|
| `code` | Identifiant stable, à utiliser dans votre code. |
| `message` | Explication lisible, en français. Ne pas analyser son texte. |
| `details` | (validation) La liste des champs en cause. |
| `requestId` | À communiquer au support. Identique à l'en-tête `X-Request-Id` de la réponse. |
Liste complète : [Codes d'erreur](/docs/errors).
## Pagination
Les listes longues sont paginées **par curseur**, du plus récent au plus ancien.
```json
{ "data": [ … ], "nextCursor": "MjAyNi0xMS0xMlQw…" }
```
Tant que `nextCursor` n'est pas `null`, passez-le dans `?cursor=` pour obtenir la page suivante. `?limit=` fixe la taille de page (1 à 100 ; 1 à 200 pour les journaux). Ne construisez jamais un curseur vous-même.
## Identifiant de requête
Chaque réponse porte un en-tête `X-Request-Id`. Vous pouvez fournir le vôtre (8 à 64 caractères : lettres, chiffres, `.`, `_`, `-`) pour corréler vos journaux avec les nôtres. Il apparaît dans le [journal des appels](/docs/api/logs).
## Idempotence
`POST /messages` accepte un en-tête `Idempotency-Key`. Voir [Fiabilité](/docs/guides/reliability).
## Appels depuis un navigateur
L'API est faite pour être appelée **depuis votre serveur**. Les appels depuis un navigateur (CORS) ne sont pas autorisés : une clé API placée dans une page web serait publique.
---
# Authentification
Créer un compte et ouvrir une session pour la console. Les routes de ce groupe utilisent la session du portail, pas une clé API.
## Créer un compte développeur (et son premier membre, propriétaire)
`POST /auth/signup`
Authentification : aucune
Corps (JSON) :
| Champ | Type | | Description |
|---|---|---|---|
| `accountName` | string | requis | Nom de votre société ou équipe : il désigne votre compte (2 à 120 caractères). |
| `email` | string | requis | Adresse e-mail du membre. Elle sert d'identifiant de connexion et doit être unique. |
| `password` | string | requis | Mot de passe : 10 caractères au minimum. |
```bash
curl -X POST "$INBOX_API/auth/signup" \
-H "Content-Type: application/json" \
-d '{
"accountName": "Acme Software",
"email": "dev@acme.com",
"password": "un-mot-de-passe-solide"
}'
```
Réponse 201 :
```json
{
"accountId": "bbfa1ecb-9483-4158-8e9a-4437e512b14b",
"memberId": "e8878e87-7817-4773-8346-fc26a2433aa6",
"sessionToken": "ses_91bb0d22558b4575a110844d944cca5d8b05e72328b",
"expiresAt": "2026-12-12T08:33:45.098Z"
}
```
## Ouvrir une session
`POST /auth/login`
Authentification : aucune
Corps (JSON) :
| Champ | Type | | Description |
|---|---|---|---|
| `email` | string | requis | Adresse e-mail du membre. Elle sert d'identifiant de connexion et doit être unique. |
| `password` | string | requis | Mot de passe : 10 caractères au minimum. |
```bash
curl -X POST "$INBOX_API/auth/login" \
-H "Content-Type: application/json" \
-d '{
"email": "dev@acme.com",
"password": "un-mot-de-passe-solide"
}'
```
Réponse 200 :
```json
{
"accountId": "bbfa1ecb-9483-4158-8e9a-4437e512b14b",
"memberId": "e8878e87-7817-4773-8346-fc26a2433aa6",
"sessionToken": "ses_9be05ed6c0764f063b187d57f0499b2eb42e7f0e4f7",
"expiresAt": "2026-12-12T08:33:45.271Z"
}
```
## Membre connecté
`GET /auth/me`
Authentification : session du portail
```bash
curl "$INBOX_API/auth/me" \
-H "Authorization: Bearer $INBOX_SESSION"
```
Réponse 200 :
```json
{
"accountId": "bbfa1ecb-9483-4158-8e9a-4437e512b14b",
"memberId": "e8878e87-7817-4773-8346-fc26a2433aa6",
"role": "owner",
"email": "dev@acme.com"
}
```
## Fermer la session
`POST /auth/logout`
Authentification : session du portail
```bash
curl -X POST "$INBOX_API/auth/logout" \
-H "Authorization: Bearer $INBOX_SESSION"
```
Réponse 204 :
```json
(aucun contenu)
```
---
# Applications et clés
Une application a deux environnements (sandbox, production). Les clés API se créent et se révoquent ici, avec la session du portail.
## Créer une application (avec ses environnements sandbox et production)
`POST /apps`
Authentification : session du portail
Corps (JSON) :
| Champ | Type | | Description |
|---|---|---|---|
| `name` | string | requis | Nom affiché dans la console (2 à 80 caractères). Unique dans votre compte pour une application. |
```bash
curl -X POST "$INBOX_API/apps" \
-H "Authorization: Bearer $INBOX_SESSION" \
-H "Content-Type: application/json" \
-d '{
"name": "Mon CRM"
}'
```
Réponse 201 :
```json
{
"id": "8b2717b6-be9e-42c7-8889-6e3c6ea4cbe3",
"name": "Mon CRM",
"createdAt": "2026-11-12T08:33:45.324Z",
"environments": [
{
"id": "7d3d2aa8-d312-48d7-894c-25b906dfa11a",
"kind": "sandbox"
},
{
"id": "badd0f25-05da-4716-8c28-f2fe1baba427",
"kind": "production"
}
]
}
```
## Lister les applications du compte
`GET /apps`
Authentification : session du portail
```bash
curl "$INBOX_API/apps" \
-H "Authorization: Bearer $INBOX_SESSION"
```
Réponse 200 :
```json
[
{
"id": "8b2717b6-be9e-42c7-8889-6e3c6ea4cbe3",
"name": "Mon CRM",
"createdAt": "2026-11-12T08:33:45.324Z",
"environments": [
{
"id": "7d3d2aa8-d312-48d7-894c-25b906dfa11a",
"kind": "sandbox"
},
{
"id": "badd0f25-05da-4716-8c28-f2fe1baba427",
"kind": "production"
}
]
}
]
```
## Générer une clé API. Le secret n’est affiché qu’une seule fois.
`POST /apps/{appId}/keys`
Authentification : session du portail
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `appId` | path | string | |
Corps (JSON) :
| Champ | Type | | Description |
|---|---|---|---|
| `name` | string | requis | Nom affiché dans la console (2 à 80 caractères). Unique dans votre compte pour une application. |
| `environment` | string | requis | Environnement de la clé : `sandbox` pour tester, `production` pour envoyer à de vrais destinataires. |
| `scopes` | string[] | requis | Droits de la clé. Une clé ne peut faire que ce qu'on lui accorde. Valeurs : messages:send, messages:read, templates:manage, webhooks:manage, connections:read, connections:manage, usage:read. |
```bash
curl -X POST "$INBOX_API/apps/APP_ID/keys" \
-H "Authorization: Bearer $INBOX_SESSION" \
-H "Content-Type: application/json" \
-d '{
"name": "Dev local",
"environment": "sandbox",
"scopes": [
"messages:send",
"messages:read",
"connections:manage",
"connections:read",
"templates:manage",
"webhooks:manage",
"usage:read"
]
}'
```
Réponse 201 :
```json
{
"id": "5ef37bc9-8aac-43ef-81b1-6bb3bdb66e78",
"name": "Dev local",
"prefix": "pk_test_bwcA3X-A",
"environment": "sandbox",
"scopes": [
"messages:send",
"messages:read",
"connections:manage",
"connections:read",
"templates:manage",
"webhooks:manage",
"usage:read"
],
"lastUsedAt": null,
"revokedAt": null,
"createdAt": "2026-11-12T08:33:45.382Z",
"secret": "pk_test_26f602c48c47ef280792fd3a506473688df70b80118"
}
```
## Lister les clés d'une application (sans secret)
`GET /apps/{appId}/keys`
Authentification : session du portail
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `appId` | path | string | |
```bash
curl "$INBOX_API/apps/APP_ID/keys" \
-H "Authorization: Bearer $INBOX_SESSION"
```
Réponse 200 :
```json
[
{
"id": "5ef37bc9-8aac-43ef-81b1-6bb3bdb66e78",
"name": "Dev local",
"prefix": "pk_test_bwcA3X-A",
"environment": "sandbox",
"scopes": [
"messages:send",
"messages:read",
"connections:manage",
"connections:read",
"templates:manage",
"webhooks:manage",
"usage:read"
],
"lastUsedAt": null,
"revokedAt": null,
"createdAt": "2026-11-12T08:33:45.382Z"
}
]
```
## Révoquer une clé API
`DELETE /apps/{appId}/keys/{keyId}`
Authentification : session du portail
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `keyId` | path | string | |
```bash
curl -X DELETE "$INBOX_API/apps/APP_ID/keys/KEY_ID" \
-H "Authorization: Bearer $INBOX_SESSION"
```
Réponse 204 :
```json
(aucun contenu)
```
---
# Connexions WhatsApp
Enregistrer l'entreprise de votre client, puis connecter son compte WhatsApp Business et ses numéros.
## Enregistrer une entreprise cliente (propriétaire futur d’un compte WhatsApp Business)
`POST /connected-businesses`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : connections:manage
Corps (JSON) :
| Champ | Type | | Description |
|---|---|---|---|
| `name` | string | requis | Nom de l'entreprise de votre client (2 à 120 caractères). |
| `externalRef` | string | optionnel | Votre propre identifiant de ce client |
```bash
curl -X POST "$INBOX_API/connected-businesses" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Cabinet Dentaire Zokem",
"externalRef": "client-42"
}'
```
Réponse 201 :
```json
{
"id": "39e2803f-79e7-4e99-8da0-1c15991f5985",
"name": "Cabinet Dentaire Zokem",
"externalRef": "client-42",
"createdAt": "2026-11-12T08:33:45.469Z"
}
```
## Lister les entreprises de l'environnement de la clé
`GET /connected-businesses`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : connections:read
```bash
curl "$INBOX_API/connected-businesses" \
-H "Authorization: Bearer $INBOX_KEY"
```
Réponse 200 :
```json
[
{
"id": "39e2803f-79e7-4e99-8da0-1c15991f5985",
"name": "Cabinet Dentaire Zokem",
"externalRef": "client-42",
"createdAt": "2026-11-12T08:33:45.469Z"
}
]
```
## Terminer l'Embedded Signup : connecter le compte et le numéro WhatsApp Business
`POST /connected-businesses/{businessId}/waba-connections`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : connections:manage
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `businessId` | path | string | |
Corps (JSON) :
| Champ | Type | | Description |
|---|---|---|---|
| `code` | string | requis | Code renvoyé par l'Embedded Signup (usage unique, bref) |
| `wabaId` | string | requis | Identifiant du compte WhatsApp Business (WABA) renvoyé par l'inscription intégrée. Chiffres uniquement. |
| `phoneNumberId` | string | requis | Identifiant du numéro WhatsApp renvoyé par l'inscription intégrée. Chiffres uniquement. |
| `mode` | string | optionnel | `cloud_api` (défaut), ou `coexistence` si le client garde l'application WhatsApp Business sur son téléphone. |
```bash
curl -X POST "$INBOX_API/connected-businesses/BUSINESS_ID/waba-connections" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "AQBx7…",
"wabaId": "102290129340398",
"phoneNumberId": "106540352242922",
"mode": "cloud_api"
}'
```
Réponse 201 :
```json
{
"id": "b76a1f98-53d6-4078-8d28-6406482daabb",
"wabaId": "102290129340398",
"status": "active",
"mode": "cloud_api",
"phoneNumber": {
"id": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"phoneNumberId": "106540352242922",
"displayNumber": "+237 6 52242922",
"verifiedName": "Cabinet Test",
"qualityRating": "GREEN"
}
}
```
## Comptes et numéros connectés d'une entreprise
`GET /connected-businesses/{businessId}/phone-numbers`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : connections:read
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `businessId` | path | string | |
```bash
curl "$INBOX_API/connected-businesses/BUSINESS_ID/phone-numbers" \
-H "Authorization: Bearer $INBOX_KEY"
```
Réponse 200 :
```json
[
{
"id": "b76a1f98-53d6-4078-8d28-6406482daabb",
"wabaId": "102290129340398",
"status": "active",
"mode": "cloud_api",
"phoneNumbers": [
{
"id": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"phoneNumberId": "106540352242922",
"displayNumber": "+237 6 52242922",
"verifiedName": "Cabinet Test",
"qualityRating": "GREEN"
}
]
}
]
```
---
# Messages
Envoyer (asynchrone), consulter le statut et lister l'historique.
## Envoyer un message (accepté immédiatement, envoyé de façon asynchrone)
`POST /messages`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : messages:send
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Rejouer la même requête avec la même clé ne renvoie jamais le message deux fois. |
Corps (JSON) :
| Champ | Type | | Description |
|---|---|---|---|
| `phoneNumberId` | string | requis | Identifiant du numéro (renvoyé par /connected-businesses/:id/phone-numbers) |
| `to` | string | requis | Destinataire, format international |
| `type` | string | requis | Type de message : text, template, image, video, audio, document ou location. |
| `text` | object | optionnel | Pour `type: text` : `{ body, previewUrl? }`. Le corps fait 1 à 4096 caractères. Exige une conversation ouverte (fenêtre de 24 h). |
| `template` | object | optionnel | Pour `type: template` : `{ name, language, components? }`. Le template doit être approuvé par Meta. |
| `image` | object | optionnel | Pour `type: image` : `{ link \| id, caption? }`. `link` est une URL https publique, `id` un média déjà hébergé par Meta. |
| `video` | object | optionnel | Pour `type: video` : `{ link \| id, caption? }`. |
| `audio` | object | optionnel | Pour `type: audio` : `{ link \| id }`. |
| `document` | object | optionnel | Pour `type: document` : `{ link \| id, caption?, filename? }`. |
| `location` | object | optionnel | Pour `type: location` : `{ latitude, longitude, name?, address? }`. |
```bash
curl -X POST "$INBOX_API/messages" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: rdv-2026-11-14-client-42" \
-d '{
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "+237600000000",
"type": "template",
"template": {
"name": "rappel_rdv",
"language": "fr",
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "Awa"
},
{
"type": "text",
"text": "14 novembre à 10 h"
}
]
}
]
}
}'
```
Réponse 202 :
```json
{
"id": "d5ad1e3e-6ab9-4f7f-8e4e-b17104a7bd00",
"direction": "outbound",
"status": "queued",
"type": "template",
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "237600000000",
"waMessageId": null,
"content": {
"type": "template",
"template": {
"name": "rappel_rdv",
"language": "fr",
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "Awa"
},
{
"type": "text",
"text": "14 novembre à 10 h"
}
]
}
]
}
},
"error": null,
"createdAt": "2026-11-12T08:33:45.855Z",
"sentAt": null,
"deliveredAt": null,
"readAt": null
}
```
## Historique des messages (paginé, plus récents d’abord)
`GET /messages`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : messages:read
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `phoneNumberId` | query | string | Identifiant du numéro d'envoi (champ `id` d'un numéro, renvoyé par /connected-businesses/{id}/phone-numbers). |
| `direction` | query | string | Filtrer par sens : `inbound` (reçus) ou `outbound` (envoyés). |
| `status` | query | string | Filtrer par statut : queued, sent, delivered, read, failed ou received. |
| `limit` | query | number | Nombre d'éléments par page, de 1 à 100. |
| `cursor` | query | string | Valeur `nextCursor` de la page précédente |
```bash
curl "$INBOX_API/messages" \
-H "Authorization: Bearer $INBOX_KEY"
```
Réponse 200 :
```json
{
"data": [
{
"id": "a9691d7a-ee51-4da9-87b2-24cce79a9313",
"direction": "outbound",
"status": "sent",
"type": "location",
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "237611111111",
"waMessageId": "wamid.HBgMXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"content": {
"type": "location",
"location": {
"name": "Cabinet Dentaire Zokem",
"address": "Akwa, Douala",
"latitude": 4.0511,
"longitude": 9.7679
}
},
"error": null,
"createdAt": "2026-11-12T08:33:46.131Z",
"sentAt": "2026-11-12T08:33:46.251Z",
"deliveredAt": null,
"readAt": null
},
{
"id": "9e09a76b-537b-4d5f-8454-4305ba8748a6",
"direction": "outbound",
"status": "sent",
"type": "document",
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "237611111111",
"waMessageId": "wamid.HBgMXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"content": {
"type": "document",
"media": {
"link": "https://exemple.com/ordonnance.pdf",
"caption": "Votre ordonnance",
"filename": "ordonnance.pdf"
}
},
"error": null,
"createdAt": "2026-11-12T08:33:46.098Z",
"sentAt": "2026-11-12T08:33:46.221Z",
"deliveredAt": null,
"readAt": null
}
],
"nextCursor": "MjAyNi0xMS0xMlQwODozMzo0Ni4wOThafGMwYmU4NDdlLWQyNDQtNGUyOS04N2M1LTAyNTE3ODk1Yzg5OQ"
}
```
## Statut d'un message
`GET /messages/{id}`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : messages:read
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `id` | path | string | |
```bash
curl "$INBOX_API/messages/ID" \
-H "Authorization: Bearer $INBOX_KEY"
```
Réponse 200 :
```json
{
"id": "d5ad1e3e-6ab9-4f7f-8e4e-b17104a7bd00",
"direction": "outbound",
"status": "sent",
"type": "template",
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "237600000000",
"waMessageId": "wamid.HBgMXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"content": {
"type": "template",
"template": {
"name": "rappel_rdv",
"language": "fr",
"components": [
{
"type": "body",
"parameters": [
{
"text": "Awa",
"type": "text"
},
{
"text": "14 novembre à 10 h",
"type": "text"
}
]
}
]
}
},
"error": null,
"createdAt": "2026-11-12T08:33:45.855Z",
"sentAt": "2026-11-12T08:33:45.891Z",
"deliveredAt": null,
"readAt": null
}
```
---
# Templates
Soumettre des templates à la revue de Meta, suivre leur statut, les synchroniser.
## Soumettre un template à la revue de Meta
`POST /waba-connections/{wabaConnectionId}/templates`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : templates:manage
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `wabaConnectionId` | path | string | |
Corps (JSON) :
| Champ | Type | | Description |
|---|---|---|---|
| `name` | string | requis | Nom du template : minuscules, chiffres et underscores (ex. rappel_rdv). |
| `language` | string | requis | Code de langue : `fr`, `en`, `en_US`… |
| `category` | string | requis | Catégorie Meta : MARKETING, UTILITY ou AUTHENTICATION. Elle détermine la tarification chez Meta. |
| `components` | string[] | requis | Composants au format Meta |
```bash
curl -X POST "$INBOX_API/waba-connections/WABA_CONNECTION_ID/templates" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "rappel_rdv",
"language": "fr",
"category": "UTILITY",
"components": [
{
"type": "BODY",
"text": "Bonjour {{1}}, votre rendez-vous est confirmé pour le {{2}}.",
"example": {
"body_text": [
[
"Awa",
"14 novembre à 10 h"
]
]
}
}
]
}'
```
Réponse 201 :
```json
{
"id": "ff532b8f-2956-4f04-851f-db09d5eefbd4",
"name": "rappel_rdv",
"language": "fr",
"category": "UTILITY",
"status": "pending",
"usable": false,
"rejectionReason": null,
"components": [
{
"type": "BODY",
"text": "Bonjour {{1}}, votre rendez-vous est confirmé pour le {{2}}.",
"example": {
"body_text": [
[
"Awa",
"14 novembre à 10 h"
]
]
}
}
],
"updatedAt": "2026-11-12T08:33:45.595Z"
}
```
## Templates du compte WhatsApp et leur statut de revue
`GET /waba-connections/{wabaConnectionId}/templates`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : templates:manage
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `wabaConnectionId` | path | string | |
```bash
curl "$INBOX_API/waba-connections/WABA_CONNECTION_ID/templates" \
-H "Authorization: Bearer $INBOX_KEY"
```
Réponse 200 :
```json
[
{
"id": "ff532b8f-2956-4f04-851f-db09d5eefbd4",
"name": "rappel_rdv",
"language": "fr",
"category": "UTILITY",
"status": "approved",
"usable": true,
"rejectionReason": null,
"components": [
{
"text": "Bonjour {{1}}, votre rendez-vous est confirmé pour le {{2}}.",
"type": "BODY"
}
],
"updatedAt": "2026-11-12T08:33:45.640Z"
}
]
```
## Synchroniser les statuts depuis Meta
`POST /waba-connections/{wabaConnectionId}/templates/sync`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : templates:manage
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `wabaConnectionId` | path | string | |
```bash
curl -X POST "$INBOX_API/waba-connections/WABA_CONNECTION_ID/templates/sync" \
-H "Authorization: Bearer $INBOX_KEY"
```
Réponse 200 :
```json
[
{
"id": "ff532b8f-2956-4f04-851f-db09d5eefbd4",
"name": "rappel_rdv",
"language": "fr",
"category": "UTILITY",
"status": "approved",
"usable": true,
"rejectionReason": null,
"components": [
{
"text": "Bonjour {{1}}, votre rendez-vous est confirmé pour le {{2}}.",
"type": "BODY"
}
],
"updatedAt": "2026-11-12T08:33:45.640Z"
}
]
```
## Supprimer un template (toutes ses langues)
`DELETE /waba-connections/{wabaConnectionId}/templates/{name}`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : templates:manage
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `wabaConnectionId` | path | string | |
| `name` | path | string | |
```bash
curl -X DELETE "$INBOX_API/waba-connections/WABA_CONNECTION_ID/templates/NAME" \
-H "Authorization: Bearer $INBOX_KEY"
```
Réponse 204 :
```json
(aucun contenu)
```
---
# Webhooks
Destinations, événements de test, historique des livraisons et rejeu.
## Déclarer une destination de webhook. Le secret de signature n’est affiché qu’une fois.
`POST /webhook-endpoints`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : webhooks:manage
Corps (JSON) :
| Champ | Type | | Description |
|---|---|---|---|
| `url` | string | requis | URL https de votre récepteur. Les adresses http, localhost et réseau privé sont refusées. |
| `events` | string[] | requis | `*` pour tout recevoir |
```bash
curl -X POST "$INBOX_API/webhook-endpoints" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.exemple.com/webhooks/inbox",
"events": [
"message.received",
"message.delivered",
"message.failed"
]
}'
```
Réponse 201 :
```json
{
"id": "bc06ae32-7564-46d5-89fb-a9869019aa8e",
"url": "https://api.exemple.com/webhooks/inbox",
"events": [
"message.received",
"message.delivered",
"message.failed"
],
"status": "active",
"consecutiveFailures": 0,
"disabledReason": null,
"createdAt": "2026-11-12T08:33:45.704Z",
"secret": "whsec_b4ac869893324714f88bfd25026b3e2e2891bb8c087"
}
```
## Lister les destinations
`GET /webhook-endpoints`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : webhooks:manage
```bash
curl "$INBOX_API/webhook-endpoints" \
-H "Authorization: Bearer $INBOX_KEY"
```
Réponse 200 :
```json
[
{
"id": "bc06ae32-7564-46d5-89fb-a9869019aa8e",
"url": "https://api.exemple.com/webhooks/inbox",
"events": [
"message.received",
"message.delivered",
"message.failed"
],
"status": "active",
"consecutiveFailures": 0,
"disabledReason": null,
"createdAt": "2026-11-12T08:33:45.704Z"
}
]
```
## Détail d’une destination
`GET /webhook-endpoints/{id}`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : webhooks:manage
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `id` | path | string | |
```bash
curl "$INBOX_API/webhook-endpoints/ID" \
-H "Authorization: Bearer $INBOX_KEY"
```
Réponse 200 :
```json
{
"id": "bc06ae32-7564-46d5-89fb-a9869019aa8e",
"url": "https://api.exemple.com/webhooks/inbox",
"events": [
"message.received",
"message.delivered",
"message.failed"
],
"status": "active",
"consecutiveFailures": 0,
"disabledReason": null,
"createdAt": "2026-11-12T08:33:45.704Z"
}
```
## Modifier l’URL, les événements, ou désactiver/réactiver
`PATCH /webhook-endpoints/{id}`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : webhooks:manage
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `id` | path | string | |
Corps (JSON) :
| Champ | Type | | Description |
|---|---|---|---|
| `url` | string | optionnel | URL https de votre récepteur. Les adresses http, localhost et réseau privé sont refusées. |
| `events` | string[] | optionnel | Événements reçus : `*` pour tout, ou une liste parmi message.received, message.sent, message.delivered, message.read, message.failed, template.status_updated. |
| `enabled` | boolean | optionnel | false = désactiver, true = réactiver (remet le compteur d’échecs à zéro) |
```bash
curl -X PATCH "$INBOX_API/webhook-endpoints/ID" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [
"*"
]
}'
```
Réponse 200 :
```json
{
"id": "bc06ae32-7564-46d5-89fb-a9869019aa8e",
"url": "https://api.exemple.com/webhooks/inbox",
"events": [
"*"
],
"status": "active",
"consecutiveFailures": 0,
"disabledReason": null,
"createdAt": "2026-11-12T08:33:45.704Z"
}
```
## Supprimer une destination (ses livraisons passées sont conservées jusqu’à la fin de la rétention)
`DELETE /webhook-endpoints/{id}`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : webhooks:manage
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `id` | path | string | |
```bash
curl -X DELETE "$INBOX_API/webhook-endpoints/ID" \
-H "Authorization: Bearer $INBOX_KEY"
```
Réponse 204 :
```json
(aucun contenu)
```
## Générer un nouveau secret de signature (l’ancien cesse de fonctionner)
`POST /webhook-endpoints/{id}/rotate-secret`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : webhooks:manage
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `id` | path | string | |
```bash
curl -X POST "$INBOX_API/webhook-endpoints/ID/rotate-secret" \
-H "Authorization: Bearer $INBOX_KEY"
```
Réponse 201 :
```json
{
"id": "bc06ae32-7564-46d5-89fb-a9869019aa8e",
"url": "https://api.exemple.com/webhooks/inbox",
"events": [
"*"
],
"status": "active",
"consecutiveFailures": 0,
"disabledReason": null,
"createdAt": "2026-11-12T08:33:45.704Z",
"secret": "whsec_c0dd380e9faa2e006a42d82e13b793a7db5a2a0c8d1"
}
```
## Vérifier la destination : envoie un événement de test signé et renvoie la réponse
`POST /webhook-endpoints/{id}/test`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : webhooks:manage
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `id` | path | string | |
```bash
curl -X POST "$INBOX_API/webhook-endpoints/ID/test" \
-H "Authorization: Bearer $INBOX_KEY"
```
Réponse 200 :
```json
{
"ok": true,
"statusCode": 200,
"durationMs": 3,
"error": null
}
```
## Historique des livraisons (paginé)
`GET /webhook-deliveries`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : webhooks:manage
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `endpointId` | query | string | Filtrer sur une destination. |
| `status` | query | string | Filtrer par statut de livraison : pending, succeeded ou failed. |
| `limit` | query | number | Nombre d'éléments par page, de 1 à 100. |
| `cursor` | query | string | Valeur `nextCursor` de la page précédente, pour obtenir la suite. |
```bash
curl "$INBOX_API/webhook-deliveries" \
-H "Authorization: Bearer $INBOX_KEY"
```
Réponse 200 :
```json
{
"data": [
{
"id": "473dfe39-7555-4942-8a5b-d2ec99026b44",
"endpointId": "bc06ae32-7564-46d5-89fb-a9869019aa8e",
"eventId": "2cbc3e2b-fba4-4246-868d-76ffe9fcb829",
"eventType": "message.sent",
"status": "succeeded",
"attempts": 1,
"lastStatusCode": 200,
"lastError": null,
"replayOf": null,
"createdAt": "2026-11-12T08:33:46.267Z",
"deliveredAt": "2026-11-12T08:33:46.324Z"
},
{
"id": "bbc7a388-fa7b-4ef3-8a5c-fbf58564274d",
"endpointId": "bc06ae32-7564-46d5-89fb-a9869019aa8e",
"eventId": "868ce546-0caa-45f3-860b-0884eb112f39",
"eventType": "message.sent",
"status": "succeeded",
"attempts": 1,
"lastStatusCode": 200,
"lastError": null,
"replayOf": null,
"createdAt": "2026-11-12T08:33:46.265Z",
"deliveredAt": "2026-11-12T08:33:46.313Z"
},
{
"id": "7a8b6205-9cf5-4ceb-8710-b0e1d029b2c1",
"endpointId": "bc06ae32-7564-46d5-89fb-a9869019aa8e",
"eventId": "8ff3994c-4406-4b8f-8f54-1ef9ee961cc0",
"eventType": "message.sent",
"status": "succeeded",
"attempts": 1,
"lastStatusCode": 200,
"lastError": null,
"replayOf": null,
"createdAt": "2026-11-12T08:33:46.264Z",
"deliveredAt": "2026-11-12T08:33:46.303Z"
}
],
"nextCursor": "MjAyNi0xMS0xMlQwODozMzo0Ni4yNjRafDU3MDEzYjE1LTM3ZmMtNDgwZC1hOTkyLWIxODhiMjNmZGE1MQ"
}
```
## Détail d’une livraison, avec chaque tentative
`GET /webhook-deliveries/{id}`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : webhooks:manage
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `id` | path | string | |
```bash
curl "$INBOX_API/webhook-deliveries/ID" \
-H "Authorization: Bearer $INBOX_KEY"
```
Réponse 200 :
```json
{
"id": "473dfe39-7555-4942-8a5b-d2ec99026b44",
"endpointId": "bc06ae32-7564-46d5-89fb-a9869019aa8e",
"eventId": "2cbc3e2b-fba4-4246-868d-76ffe9fcb829",
"eventType": "message.sent",
"status": "succeeded",
"attempts": 1,
"lastStatusCode": 200,
"lastError": null,
"replayOf": null,
"createdAt": "2026-11-12T08:33:46.267Z",
"deliveredAt": "2026-11-12T08:33:46.324Z",
"payload": {
"data": {
"to": "237611111111",
"status": "sent",
"messageId": "a9691d7a-ee51-4da9-87b2-24cce79a9313",
"occurredAt": "2026-11-12T08:33:46.251Z",
"waMessageId": "wamid.HBgMXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87"
},
"createdAt": "2026-11-12T08:33:46.247Z"
},
"attemptsDetail": [
{
"attempt": 1,
"statusCode": 200,
"error": null,
"durationMs": 0,
"responseSnippet": "ok",
"at": "2026-11-12T08:33:46.322Z"
}
]
}
```
## Rejouer l’événement vers la même destination
`POST /webhook-deliveries/{id}/replay`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : webhooks:manage
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `id` | path | string | |
```bash
curl -X POST "$INBOX_API/webhook-deliveries/ID/replay" \
-H "Authorization: Bearer $INBOX_KEY"
```
Réponse 202 :
```json
{
"id": "13980898-c6ac-4b7c-8e78-f68fd741cfb9",
"endpointId": "bc06ae32-7564-46d5-89fb-a9869019aa8e",
"eventId": "2cbc3e2b-fba4-4246-868d-76ffe9fcb829",
"eventType": "message.sent",
"status": "pending",
"attempts": 0,
"lastStatusCode": null,
"lastError": null,
"replayOf": "473dfe39-7555-4942-8a5b-d2ec99026b44",
"createdAt": "2026-11-12T08:33:46.391Z",
"deliveredAt": null
}
```
---
# Bac à sable
Simuler des messages reçus et des changements de statut, sans WhatsApp.
## Simuler un message reçu d’un client (clé sandbox uniquement). Déclenche le webhook `message.received`.
`POST /sandbox/simulate/inbound-message`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : messages:send
Corps (JSON) :
| Champ | Type | | Description |
|---|---|---|---|
| `phoneNumberId` | string | requis | Identifiant d'un numéro de l'environnement sandbox. |
| `from` | string | requis | Numéro de l'expéditeur simulé, format international avec « + ». |
| `text` | string | requis | Texte du message simulé. |
```bash
curl -X POST "$INBOX_API/sandbox/simulate/inbound-message" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"from": "+237611111111",
"text": "Bonjour, je voudrais déplacer mon rendez-vous"
}'
```
Réponse 201 :
```json
{
"messageId": "3f3baf5a-f93e-42e8-8263-ce62dc4aadd6",
"from": "237611111111",
"status": "received",
"simulated": true
}
```
## Faire avancer le statut d’un message sortant (clé sandbox uniquement). Déclenche le webhook correspondant.
`POST /sandbox/simulate/message-status`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : messages:send
Corps (JSON) :
| Champ | Type | | Description |
|---|---|---|---|
| `messageId` | string | requis | Identifiant d'un message sortant de l'environnement sandbox. |
| `status` | string | requis | Nouveau statut : sent, delivered, read ou failed. Un statut ne recule jamais, comme chez Meta. |
```bash
curl -X POST "$INBOX_API/sandbox/simulate/message-status" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"messageId": "d5ad1e3e-6ab9-4f7f-8e4e-b17104a7bd00",
"status": "delivered"
}'
```
Réponse 200 :
```json
{
"messageId": "d5ad1e3e-6ab9-4f7f-8e4e-b17104a7bd00",
"status": "delivered",
"simulated": true
}
```
---
# Usage et facturation
Consommation, plan et identité de la plateforme.
## Consommation par jour, catégorie, pays ou numéro
`GET /usage`
Authentification : clé API, ou session de la console avec X-Environment-Id · portée : usage:read
Le coût Meta est une ESTIMATION calculée avec la grille tarifaire configurée ; `unpricedMessages` compte les messages facturables sans tarif configuré. Les frais BrefPoint sont séparés et jamais ajoutés au coût Meta.
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `from` | query | string | Début (inclus), ISO 8601. Défaut : début du mois en cours (UTC) |
| `to` | query | string | Fin (exclue), ISO 8601 |
| `groupBy` | query | string | Regroupement : `day` (défaut), `category`, `country` ou `phone_number`. |
```bash
curl "$INBOX_API/usage" \
-H "Authorization: Bearer $INBOX_KEY"
```
Réponse 200 :
```json
{
"from": "2026-11-01T00:00:00.000Z",
"to": "2026-12-01T00:00:00.000Z",
"groupBy": "day",
"totals": {
"messages": 0,
"billableMessages": 0,
"notBillableMessages": 0,
"unpricedMessages": 0,
"metaCostEstimate": {},
"platformFee": {}
},
"buckets": [],
"metaCostIsEstimate": true
}
```
## Plan, consommation de la période et limites (portail)
`GET /billing/overview`
Authentification : session du portail
```bash
curl "$INBOX_API/billing/overview" \
-H "Authorization: Bearer $INBOX_SESSION"
```
Réponse 200 :
```json
{
"plan": {
"id": "654e934f-a17e-4c05-8964-b1febb33b2b5",
"code": "free",
"name": "Gratuit",
"monthlyPriceAmount": null,
"currency": null,
"includedMessages": 100,
"hardMessageLimit": 100,
"maxConnectedBusinesses": 1,
"maxNumbersPerBusiness": 1,
"overageFeePerMessage": 0,
"logRetentionDays": 3
},
"period": {
"start": "2026-11-01T00:00:00.000Z",
"end": "2026-12-01T00:00:00.000Z"
},
"messages": {
"used": 0,
"included": 100,
"hardLimit": 100
},
"connectedBusinesses": {
"used": 0,
"max": 1
}
}
```
## Identité commerciale et légale de la plateforme (publique)
`GET /platform`
Authentification : aucune
```bash
curl "$INBOX_API/platform"
```
Réponse 200 :
```json
{
"brand": {
"name": "Bref Point Inbox",
"tagline": "One API. Every conversation.",
"website": null,
"supportEmail": null
},
"legal": {
"name": "Labred Industry",
"address": null,
"taxId": null
},
"notice": "Bref Point Inbox est un produit indépendant, édité par Labred Industry. Il n'est ni édité, ni approuvé par Meta."
}
```
---
# Journaux
Journal des appels faits avec vos clés.
## Journal des appels API du compte (méthode, route, statut, durée, code d'erreur, requestId)
`GET /logs/requests`
Authentification : session du portail
Paramètres :
| Nom | Emplacement | Type | Description |
|---|---|---|---|
| `environmentId` | query | string | Filtrer sur un environnement. |
| `status` | query | string | `success` (statut < 400) ou `error` (statut ≥ 400). |
| `method` | query | string | Filtrer par méthode HTTP (GET, POST…). |
| `route` | query | string | Filtre sur le chemin (ex. /messages) |
| `limit` | query | number | Nombre d'éléments par page, de 1 à 200 (50 par défaut). |
| `cursor` | query | string | Valeur `nextCursor` de la page précédente, pour obtenir la suite. |
```bash
curl "$INBOX_API/logs/requests" \
-H "Authorization: Bearer $INBOX_SESSION"
```
Réponse 200 :
```json
{
"data": [
{
"id": "67528eac-848d-4155-8763-7e775550d557",
"requestId": "req_7484f6f8-cc27-4b8e-8bfa-987494dd32e0",
"method": "GET",
"route": "/api/v1/usage",
"status": 200,
"durationMs": 15,
"errorCode": null,
"environmentId": "7d3d2aa8-d312-48d7-894c-25b906dfa11a",
"apiKeyId": "5ef37bc9-8aac-43ef-81b1-6bb3bdb66e78",
"at": "2026-11-12T08:33:46.423Z"
},
{
"id": "e04e4028-3c28-496c-8d3e-d99572a15170",
"requestId": "req_a2f45ea7-9fa9-4132-8cab-6492beab475e",
"method": "POST",
"route": "/api/v1/webhook-deliveries/:id/replay",
"status": 202,
"durationMs": 19,
"errorCode": null,
"environmentId": "7d3d2aa8-d312-48d7-894c-25b906dfa11a",
"apiKeyId": "5ef37bc9-8aac-43ef-81b1-6bb3bdb66e78",
"at": "2026-11-12T08:33:46.395Z"
},
{
"id": "fd8ab473-5174-48e6-8924-058fb102c7e3",
"requestId": "req_3019ad2e-845a-43b9-8963-8d851c0d0652",
"method": "GET",
"route": "/api/v1/webhook-deliveries/:id",
"status": 200,
"durationMs": 16,
"errorCode": null,
"environmentId": "7d3d2aa8-d312-48d7-894c-25b906dfa11a",
"apiKeyId": "5ef37bc9-8aac-43ef-81b1-6bb3bdb66e78",
"at": "2026-11-12T08:33:46.374Z"
}
],
"nextCursor": "MjAyNi0xMS0xMlQwODozMzo0Ni4zNzRafGM3MjdmMTYxLWQ3NjYtNGMwNy1hNDc3LTc0YzBiYWI2NGQ1OQ"
}
```
---
# Système
État du service.
## État de l'API et de sa base de données
`GET /health`
Authentification : aucune
```bash
curl "$INBOX_API/health"
```
Réponse 200 :
```json
{
"status": "ok",
"database": "up"
}
```
---
# Codes d'erreur
> Tous les codes d'erreur de l'API, leur signification, et s'il est utile de réessayer.
Toute erreur a la forme `{ "error": { "code", "message", "details?", "requestId" } }`. **Basez votre code sur `code`**, jamais sur le texte de `message`. Conservez le `requestId` : c'est ce qui permet de retrouver l'appel.
## Table des codes
| Code | HTTP | Signification | Réessayer ? |
|---|---|---|---|
| `validation_error` | 400 | Un champ est invalide ou manquant ; `details` le liste. | Non, corrigez d'abord. |
| `unauthorized` | 401 | Clé ou session absente, invalide, expirée ou révoquée. | Non, vérifiez le jeton. |
| `quota_exceeded` | 402 | Le quota mensuel de votre plan est atteint (production). | Non, changez de plan ou attendez le mois suivant. |
| `plan_limit_reached` | 402 | Le plan n'autorise pas plus d'entreprises connectées, ni plus de numéros pour cette entreprise. | Non : changez de plan. |
| `forbidden` | 403 | La clé n'a pas la portée requise ; le message la nomme. | Non, créez une clé avec la bonne portée. |
| `sandbox_only` | 403 | Action réservée aux clés de bac à sable. | Non. |
| `not_found` | 404 | La ressource n'existe pas, **ou appartient à un autre compte ou environnement**. | Non. |
| `conflict` | 409 | Doublon (nom, référence externe, compte WhatsApp déjà connecté…) ou action impossible dans l'état actuel (livraison en cours, destination désactivée). | Selon le cas. |
| `invalid_message` | 422 | Message invalide : numéro mal formé, texte vide, média ambigu, lien non `https`… | Non. |
| `outside_service_window` | 422 | Message libre hors de la fenêtre de 24 heures. | Non : envoyez un template, ou attendez que le client écrive. |
| `template_not_approved` | 422 | Le template n'est pas approuvé par Meta. | Non, attendez l'approbation. |
| `idempotency_conflict` | 422 | Même `Idempotency-Key` pour une requête différente. | Non : utilisez une autre clé. |
| `meta_error` | 502 | Meta a refusé la requête ou ne répond pas ; le message reprend sa réponse. | **Oui** si c'est une panne, non si c'est un refus. |
| `internal_error` | 500 | Erreur de notre côté. | **Oui**, puis contactez le support avec le `requestId`. |
## Exemples réels
### 400 — `validation_error`
`details` indique le champ et la règle enfreinte.
### 401 — `unauthorized`
### 403 — `forbidden`
### 404 — `not_found`
Pour ne pas révéler l'existence de ressources qui ne sont pas les vôtres, un identifiant valide mais appartenant à un autre compte (ou à l'autre environnement) répond `404`, exactement comme un identifiant inexistant.
### 409 — `conflict`
### 422 — `outside_service_window`
### 422 — `invalid_message`
### 422 — `idempotency_conflict`
## Erreurs de livraison d'un message
Une fois le message **accepté** (`202`), les échecs ne se voient plus dans une réponse HTTP : le message passe en `failed`, avec un `error` qui reprend le code et le message de Meta, et un événement `message.failed` est envoyé à vos webhooks.
| Situation | Reprise automatique ? |
|---|---|
| Panne de Meta, délai dépassé, limite de débit | Oui, jusqu'à 6 tentatives avec attente croissante |
| Refus définitif (template introuvable, destinataire non joignable, contenu refusé) | Non : `failed` immédiatement |
Les codes numériques qui apparaissent dans `error.code` sont ceux de **Meta** : consultez sa documentation pour leur sens exact.
---
# Limites et quotas
> Les valeurs limites de l'API : tailles, pagination, délais, reprises, et limites du plan.
## Requêtes et champs
| Élément | Limite |
|---|---|
| Texte d'un message | 1 à 4 096 caractères |
| Numéro du destinataire | Format international avec « + », 8 à 15 chiffres |
| Média (image, vidéo, audio, document) | `link` en `https://`, ou `id` Meta ; jamais les deux |
| Nom de template | Minuscules, chiffres et `_`, jusqu'à 512 caractères |
| `Idempotency-Key` | Jusqu'à 200 caractères |
| Nom de compte, d'entreprise | 2 à 120 caractères |
| Nom d'application, de clé | 2 à 80 caractères |
| `externalRef` | Jusqu'à 120 caractères, unique par environnement |
| Mot de passe | 10 à 200 caractères |
| URL de webhook | `https://` public, jusqu'à 2 000 caractères |
## Pagination
| Liste | Taille par défaut | Maximum |
|---|---|---|
| Messages, livraisons de webhooks | 25 | 100 |
| Journal des appels | 50 | 200 |
## Envoi
| Élément | Valeur |
|---|---|
| Tentatives d'envoi en cas de panne passagère | 6, attente croissante |
| Débit par numéro | 50 messages par seconde par défaut (réglé par la plateforme) |
| Reprise d'un message resté `queued` | Après 5 minutes |
## Webhooks
| Élément | Valeur |
|---|---|
| Délai de réponse de votre serveur | 10 secondes |
| Tentatives par livraison | 8 au total, attente doublée (environ une heure) |
| Désactivation automatique d'une destination | Après 15 livraisons définitivement échouées d'affilée |
| Extrait de la réponse conservé | 1 Ko |
| Tolérance de signature conseillée | 5 minutes |
## Sessions et clés
| Élément | Valeur |
|---|---|
| Durée d'une session de console | 30 jours |
| Durée d'une clé API | Jusqu'à révocation |
## Plans
| | Gratuit (par défaut) | Starter | Growth | Scale |
|---|---|---|---|---|
| Messages envoyés par mois, en production | **100** (plafond) | 3 000 inclus | 15 000 inclus | 100 000 inclus |
| Entreprises connectées, en production | **1** | 3 | 10 | 50 |
| Numéros WhatsApp **par entreprise**, en production | **1** | 2 | 3 | 5 |
| Frais au-delà du quota inclus | — (l'envoi s'arrête) | 5 FCFA par message | 3 FCFA par message | 2 FCFA par message |
| Conservation des journaux et de l'historique des livraisons | 3 jours | 14 jours | 30 jours | 90 jours |
| Sandbox | Illimité, jamais facturé | idem | idem | idem |
Le plan **Gratuit** est volontairement très limité : il sert à valider une première intégration avec un client. Les plans payants n'ont pas de plafond dur : au-delà du quota inclus, un frais par message s'applique, sur sa propre ligne. Les prix sont présentés sur [la page des tarifs](/#tarifs). Une limite atteinte donne `402 quota_exceeded` (messages) ou `402 plan_limit_reached` (entreprises ou numéros).
Les limites de **Meta** (paliers de messages par numéro, qualité du numéro, règles de contenu) s'appliquent en plus : elles dépendent de votre compte WhatsApp et sont décidées par Meta.
---
# Pour les agents IA
> La documentation et la spécification dans des formats lisibles par les assistants de code et les agents.
Toute la documentation est conçue pour être lue par des humains **et** par des agents (assistants de code, outils d'automatisation).
| Ressource | Adresse | Contenu |
|---|---|---|
| Index | [`/llms.txt`](/llms.txt) | La liste des pages, avec un lien Markdown et une description pour chacune |
| Documentation complète | [`/llms-full.txt`](/llms-full.txt) | Toutes les pages en un seul fichier Markdown |
| Une page en Markdown | `/docs/.md` | Ajoutez `.md` à l'adresse d'une page, ou utilisez le lien « Voir en Markdown » |
| Spécification OpenAPI 3 | [`/openapi.json`](/openapi.json) | Toutes les routes, paramètres, corps et portées requises (`x-required-scopes`) |
## Conseils pour un agent
- **Partez de la spécification OpenAPI** pour générer un client ou des outils : elle indique, pour chaque opération, le mode d'authentification et la portée exigée.
- **Utilisez une clé de sandbox** (`pk_test_…`) pour tout essai automatisé : rien n'y est facturé et aucun vrai destinataire n'est contacté.
- **Donnez à l'agent le moins de portées possible** : une clé en lecture seule (`messages:read`, `usage:read`) suffit pour observer.
- Faites toujours passer un **`Idempotency-Key`** par ses envois : un agent qui réessaie ne doit jamais dupliquer un message.
- **Ne laissez jamais un agent envoyer en production sans validation humaine** : un message envoyé à un client ne se rattrape pas.
---
# Journal des changements
> L'historique des évolutions de l'API et de la documentation.
## 12 novembre 2026 — Console : connexions, messages, webhooks
- La console propose les pages **Connexions**, **Messages** et **Webhooks**, avec un sélecteur d'environnement (sandbox / production).
- Les routes à clé API acceptent **aussi la session de la console**, avec l'en-tête `X-Environment-Id` (voir [Authentification](/docs/authentication)).
## 12 novembre 2026 — Limite de numéros par entreprise
- Les plans limitent désormais le nombre de **numéros WhatsApp par entreprise connectée** (production seulement) : `402 plan_limit_reached` au-delà. Le contrôle a lieu **avant** l'échange du code d'inscription de Meta, qui n'est donc pas consommé par un refus.
- `GET /billing/overview` expose `plan.maxNumbersPerBusiness` (`null` = illimité).
## 12 novembre 2026 — API v1
Première version de l'API.
**Comptes et accès**
- Inscription, connexion, sessions révocables ; mots de passe hachés avec sel.
- Applications avec deux environnements, **sandbox** et **production**, strictement séparés.
- Clés API `pk_test_…` / `pk_live_…`, avec **portées**, révocation et date de dernière utilisation ; secret affiché une seule fois.
**Connexion WhatsApp**
- Entreprises connectées, finalisation de l'inscription intégrée de Meta, comptes et numéros, **mode coexistence**.
- Jetons Meta chiffrés, jamais renvoyés par l'API.
- Un compte WhatsApp ne peut être connecté qu'une fois sur toute la plateforme.
**Messages**
- Envoi asynchrone et **idempotent** de texte, images, vidéos, audios, documents, localisation et templates.
- Règle des 24 heures appliquée, reprises automatiques, régulation du débit par numéro.
- Statuts qui ne reculent jamais ; historique paginé.
**Templates**
- Création, liste, suppression, synchronisation ; refus immédiat d'un template non approuvé ; événement `template.status_updated`.
**Webhooks**
- Destinations par environnement, signature horodatée (HMAC-SHA256), 8 tentatives, historique de chaque tentative, rejeu, rotation du secret, désactivation automatique.
- Protection contre les destinations internes.
**Usage et facturation**
- Consommation par jour, catégorie, pays ou numéro ; coût Meta **estimé** et frais Bref Point Inbox sur des lignes séparées.
- Quotas et limites de plan en production ; le sandbox n'est ni limité ni facturé.
**Observabilité**
- Format d'erreur unique avec `requestId` ; journal des appels sans contenu ; maintenance planifiée (purge selon la rétention, reprise des envois perdus).
- **Simulateur de bac à sable** pour tester ses webhooks sans WhatsApp.
**À venir et limites connues**
- La validation contre la plateforme Meta réelle (inscription intégrée, envoi, templates) est en cours.
- Pas encore de téléversement ni de téléchargement de médias par l'API.
- Pas de SDK officiels : générez un client depuis la [spécification OpenAPI](/openapi.json).
- Le paiement en ligne des abonnements n'est pas encore disponible.