Ressources
Envoyer des messages, statuts et groupes
Envoyer/lister les messages WhatsApp, interroger l'état de livraison, publier un statut, lister les groupes, et gérer les contacts consentants.
Messages
| Méthode | Chemin | Permission | Objet |
|---|---|---|---|
| POST | /shops/{idOrName}/messages | messages.send | mettre en file un message WhatsApp → 202 |
| GET | /shops/{idOrName}/messages?status=&limit=&cursor= | messages.read | liste, les plus récents d'abord |
| GET | /messages/{id} | messages.read | état de livraison d'un message |
curl -s -X POST -H "Authorization: Bearer omk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"channel":"whatsapp","to":"+24177000000","text":"Hello!",
"mediaUrl":"https://example.com/photo.jpg","mediaType":"image",
"idempotencyKey":"order-1234-confirmation"}' \
https://app.omni-manager.com/api/v1/shops/SHOP_ID/messages
Tous les champs sauf channel et to sont optionnels ; text ou mediaUrl est requis (sinon 422 EMPTY_MESSAGE). to doit être au format E.164 (+CCxxxxxxxxx). Au plus un mediaUrl par appel — le contrat de cet endpoint est toujours une seule pièce jointe. Répéter un appel avec le même idempotencyKey renvoie le message d'origine (200, en-tête X-Idempotent-Replay: true) et n'envoie jamais deux fois.
Un appel réussi renvoie 202 avec le message mis en file et le verdict de la politique qui l'a laissé passer :
{ "id": "…", "status": "queued", "statusUrl": "/api/v1/messages/…",
"policy": {
"recipientClass": "known", // "known" | "cold" | "declared" | "group"
"warmupDay": 12,
"caps": { "perDay": 100, "perHour": 16, "coldPerDay": 10, "declaredPerDay": 50 },
"usage": { "today": 3, "lastHour": 1, "coldToday": 0, "declaredToday": 0 }
} }
Interrogez GET /messages/{id} pour l'état final : sent, failed (erreur de livraison — BOT_RESTARTED, WHATSAPP_SESSION_LOST, BOT_SERVICE_UNREACHABLE, ou WHATSAPP_NOT_CONNECTED une fois qu'un message a trop longtemps attendu une session qui n'est jamais revenue), ou rejected (RECIPIENT_NOT_ON_WHATSAPP) — un message ne reste jamais queued indéfiniment.
Statuts
| Méthode | Chemin | Permission | Objet |
|---|---|---|---|
| POST | /shops/{idOrName}/statuses | statuses.post | publier un statut WhatsApp via le numéro de la boutique → 202 |
Corps : { caption?, mediaUrl?, mediaType? } ("image" ou "video" ; caption ou mediaUrl requis, caption plafonné à ~700 caractères, une seule pièce jointe). Soumis à son propre plafond quotidien, statusesPerDay, distinct des plafonds de messages ci-dessus — 429 STATUSES_DAILY_CAP_REACHED une fois épuisé.
Groupes
| Méthode | Chemin | Permission | Objet |
|---|---|---|---|
| GET | /shops/{idOrName}/groups?canSend=&minParticipants=&q= | groups.read | les groupes WhatsApp de la boutique |
Chaque élément est { id, name, participantCount, isAdmin, announce, canSend }. canSend vaut exactement isAdmin — pas !announce || isAdmin — parce que le désabonnement ne peut pas fonctionner par membre à l'intérieur d'un groupe et la simple appartenance n'est pas une preuve de consentement à la diffusion ; administrer le groupe est le seul signal disponible, il est donc requis pour envoyer à N'IMPORTE QUEL groupe, pas seulement ceux en mode « annonces ». Filtrez avec ?canSend=true, ?minParticipants=50, ?q=sous-chaîne du nom.
Envoyer à un groupe se fait avec le même POST …/messages ci-dessus, avec un JID de groupe comme to ; recipientClass dans le verdict de la politique vaut alors "group", et les plafonds spécifiques aux groupes de la politique anti-bannissement s'appliquent à la place des plafonds 1:1.
Contacts consentants
Un tenant peut déclarer qu'un numéro n'est pas un inconnu, le faisant passer du compartiment cold au compartiment declared (son propre budget quotidien, généralement plus large — voir Messagerie et politique anti-bannissement).
| Méthode | Chemin | Permission | Objet |
|---|---|---|---|
| GET | /shops/{idOrName}/contacts/consented?activeOnly=&limit=&cursor= | contacts.read | lister les contacts déclarés (non isolés par boutique en stockage — chaque boutique du tenant voit la même liste) |
| POST | /shops/{idOrName}/contacts/consented | contacts.write | déclarer un ou plusieurs numéros |
| DELETE | /shops/{idOrName}/contacts/consented | contacts.write | révoquer un numéro |
POST est toujours un lot, même d'un seul élément : { "contacts": [{ "phone": "+241…", "note"?, "expiresAt"? }] }. Plafonné à une taille de lot et à un plafond de déclarations réellement nouvelles par jour (les deux côté serveur, non pilotés par le tenant) ; dépasser l'un ou l'autre renvoie 429 DECLARE_BATCH_TOO_LARGE / 429 DECLARE_DAILY_CAP_REACHED sans toucher à celles qui seraient passées. Une déclaration faite via cette route enregistre toujours source: "api". DELETE prend { "phone": "+241…" } et révoque (revokedAt renseigné), sans jamais supprimer définitivement — la piste d'audit reste.
Un numéro qui répond STOP / ARRÊT / DÉSABONNER / UNSUBSCRIBE se désabonne, qu'il ait été déclaré ou non — l'envoi suivant vers lui échoue avec 422 RECIPIENT_OPTED_OUT tant qu'il n'écrit pas à nouveau.