{"openapi": "3.1.0", "info": {"title": "API Diffuz", "version": "1.0.0", "summary": "Diffusion de messages WhatsApp en masse depuis votre numéro professionnel.", "description": "L'API Diffuz permet à vos outils (CRM, site e-commerce, logiciel de caisse…) de\n**diffuser des messages WhatsApp en masse**, avec la même protection\nanti-blocage que le bot Diffuz.\n\n## Deux façons d'envoyer\n\n| `sender` | Le message part… | Prérequis |\n|---|---|---|\n| `own` (par défaut) | **de votre propre numéro WhatsApp** : vos contacts reconnaissent votre nom | Numéro connecté (voir *Session WhatsApp*) |\n| `diffuz` | **du numéro du bot Diffuz** | Aucun : idéal si vous n'avez pas de numéro dédié |\n\n## Démarrage rapide\n\n1. Demandez votre **clé API** à l'équipe Diffuz : elle est délivrée une fois\n   votre compte validé.\n2. Vérifiez-la avec `GET /auth/verify`.\n3. Pour envoyer depuis votre numéro, connectez-le : `POST /session`, puis\n   `GET /session/qr` (à scanner) ou `POST /session/pairing-code` (à saisir).\n4. Lancez une diffusion avec `POST /diffusions`, puis suivez-la par webhook ou\n   avec `GET /diffusions/{id}`.\n\n```bash\ncurl https://api-diffuz.labis2ac.com/v1/diffusions \\\n  -H \"Authorization: Bearer dfz_live_••••••••\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: promo-wax-2026-10-27\" \\\n  -d '{\n    \"message\": \"Bonjour {{prenom}}, la nouvelle collection Wax arrive ce week-end !\",\n    \"recipients\": [\n      {\"phone\": \"2250700000001\", \"variables\": {\"prenom\": \"Awa\"}},\n      {\"phone\": \"2250500000002\", \"variables\": {\"prenom\": \"Koffi\"}}\n    ],\n    \"opt_in_confirmed\": true\n  }'\n```\n\n## Une diffusion n'est jamais instantanée\n\nPour protéger le numéro expéditeur, Diffuz envoie **un message à la fois, au\nrythme d'un humain** : 20 à 44 secondes entre deux messages et une pause de 50\nà 70 minutes tous les 25 messages. C'est pourquoi `POST /diffusions` répond\n`202 Accepted` : la diffusion est acceptée, l'envoi se fait en arrière-plan.\n\nAvant l'envoi, Diffuz :\n\n- **vérifie** que chaque numéro existe sur WhatsApp et écarte les autres ;\n- **rédige plusieurs formulations** de votre message (même offre, même ton),\n  pour ne pas envoyer un texte identique à toute votre base.\n\nUne seule diffusion avance à la fois par numéro expéditeur : les suivantes\nattendent leur tour. Si WhatsApp restreint le numéro (erreurs 463 / 475), la\ndiffusion se met **en pause d'elle-même** ; sa reprise est décidée par l'équipe Diffuz.\n\n## Messages personnalisés\n\n- **Variables** : écrivez `{{prenom}}`, `{{commande}}`… dans `message`, et donnez\n  leurs valeurs dans `variables` pour chaque destinataire. Avec un fichier CSV ou\n  XLSX, chaque colonne devient une variable.\n- **Message propre à un destinataire** : donnez-lui son propre `message` ; il\n  remplace le message commun et part tel quel.\n\nUne variable utilisée dans le message mais absente pour un destinataire bloque\nla diffusion (`400 missing_variable`) : aucun client ne reçoit « Bonjour {{prenom}} ».\n\n## Numéros de téléphone\n\nFormat international, chiffres uniquement, sans `+` ni espaces :\n`2250700000001` pour le +225 07 00 00 00 01.\n\n## Idempotence\n\nEnvoyez un en-tête `Idempotency-Key` (identifiant unique de votre choix) avec\n`POST /diffusions`. Si la requête est rejouée avec la même clé, Diffuz renvoie\nla diffusion déjà créée au lieu d'en lancer une seconde : vos contacts ne\nreçoivent jamais deux fois le même message.\n\n## Pagination\n\nLes listes sont paginées par curseur : passez `next_cursor` dans le paramètre\n`cursor` de la requête suivante tant que `has_more` vaut `true`.\n\n## Limites de débit\n\n120 requêtes par minute et par clé. Chaque réponse indique votre consommation\ndans les en-têtes `RateLimit-Limit`, `RateLimit-Remaining` et `RateLimit-Reset`.\nAu-delà, l'API répond `429` avec un en-tête `Retry-After`.\n\n## Erreurs\n\nToutes les erreurs ont la même forme :\n\n```json\n{\n  \"error\": {\n    \"type\": \"invalid_request\",\n    \"code\": \"recipients_empty\",\n    \"message\": \"Ajoutez au moins un destinataire.\",\n    \"param\": \"recipients\",\n    \"request_id\": \"req_8Kq2mX4tVb\"\n  }\n}\n```\n\n| Statut | Type | Signification |\n|---|---|---|\n| 400 | `invalid_request` | Requête mal formée ou champ invalide |\n| 401 | `authentication_error` | Clé absente, invalide ou révoquée |\n| 403 | `permission_error` | Compte non validé, ou numéro WhatsApp non connecté pour `sender: own` |\n| 404 | `not_found` | Ressource introuvable |\n| 409 | `conflict` | Action impossible dans l'état actuel |\n| 429 | `rate_limited` | Trop de requêtes |\n| 500 | `api_error` | Erreur de notre côté : réessayez, puis contactez le support |\n| 502 | `whatsapp_unavailable` | La passerelle WhatsApp ne répond pas : réessayez |\n\n## Consentement\n\nVous ne devez écrire qu'à des contacts qui ont accepté de recevoir vos messages.\nChaque diffusion exige `opt_in_confirmed: true`. Les envois non sollicités\nexposent le numéro expéditeur à des signalements et à un blocage par WhatsApp.\n\n## Support\n\nsupport@diffuz.tech · WhatsApp +225 05 86 41 30 30, 24h/24 et 7j/7.", "contact": {"name": "Support Diffuz", "email": "support@diffuz.tech", "url": "https://diffuz.labis2ac.com/#contact"}, "termsOfService": "https://diffuz.labis2ac.com/", "license": {"name": "Propriétaire, © Diffuz", "url": "https://diffuz.labis2ac.com/"}}, "servers": [{"url": "https://api-diffuz.labis2ac.com/v1", "description": "API Diffuz"}], "security": [{"cleApi": []}], "tags": [{"name": "Authentification", "description": "Chaque requête porte votre clé dans l'en-tête `Authorization: Bearer dfz_live_…`. Gardez-la secrète, côté serveur uniquement : ne la mettez jamais dans une application mobile ou une page web."}, {"name": "Session WhatsApp", "description": "Connexion de **votre** numéro WhatsApp à Diffuz, nécessaire pour diffuser avec `sender: own`. Comme sur WhatsApp Web : scannez un QR code ou saisissez un code dans WhatsApp › Appareils connectés › Connecter un appareil."}, {"name": "Diffusions", "description": "Une diffusion envoie un message à une liste de destinataires, un par un, au rythme anti-blocage de Diffuz."}, {"name": "Destinataires", "description": "Suivi message par message : envoyé, livré, lu ou en échec."}, {"name": "Webhooks", "description": "Diffuz prévient votre serveur à chaque étape, à l'adresse `callback_url` donnée à la création de la diffusion."}], "paths": {"/auth/verify": {"get": {"tags": ["Authentification"], "summary": "Vérifier une clé", "description": "Confirme que la clé est valide et indique si le compte peut diffuser. Idéal pour tester la connexion au moment de brancher votre outil.", "operationId": "verify_key", "responses": {"200": {"description": "Clé valide.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/KeyVerification"}, "example": {"valid": true, "key": {"name": "CRM de la boutique", "prefix": "dfz_live_Ab3x", "created_at": "2026-10-20T10:12:00Z", "last_used_at": "2026-10-27T08:59:58Z"}, "account": {"company_name": "Boutique Awa", "phone": "2250700000000", "authorized": true, "whatsapp_status": "connected"}, "webhook_secret": "whsec_4Gx9Lm2Qp7RtVw1Zk8NbHc3Yd6Fs"}}}, "headers": {"RateLimit-Limit": {"$ref": "#/components/headers/RateLimit-Limit"}, "RateLimit-Remaining": {"$ref": "#/components/headers/RateLimit-Remaining"}, "RateLimit-Reset": {"$ref": "#/components/headers/RateLimit-Reset"}}}, "401": {"$ref": "#/components/responses/401"}, "429": {"$ref": "#/components/responses/429"}}}}, "/auth/rotate": {"post": {"tags": ["Authentification"], "summary": "Renouveler la clé", "description": "Crée une nouvelle clé et programme la fin de l'actuelle **24 h plus tard**, le temps de mettre à jour vos outils. La nouvelle clé n'est affichée qu'une seule fois : enregistrez-la immédiatement.", "operationId": "rotate_key", "responses": {"201": {"description": "Nouvelle clé créée.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/KeyRotation"}, "example": {"key": "dfz_live_Q7m2Xr9KpL4vNs8TzWc1HyB6dFgJ3uE5aRt0oIkMn", "prefix": "dfz_live_Q7m2", "previous_key_expires_at": "2026-10-28T09:00:00Z"}}}, "headers": {"RateLimit-Limit": {"$ref": "#/components/headers/RateLimit-Limit"}, "RateLimit-Remaining": {"$ref": "#/components/headers/RateLimit-Remaining"}, "RateLimit-Reset": {"$ref": "#/components/headers/RateLimit-Reset"}}}, "401": {"$ref": "#/components/responses/401"}, "429": {"$ref": "#/components/responses/429"}}}}, "/session": {"get": {"tags": ["Session WhatsApp"], "summary": "État de la session", "description": "Indique si votre numéro est connecté et peut diffuser avec `sender: own`.", "operationId": "get_session", "responses": {"200": {"description": "État de la session.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Session"}, "example": {"object": "session", "phone": "2250700000000", "status": "connected", "connected_at": "2026-10-20T10:12:00Z"}}}, "headers": {"RateLimit-Limit": {"$ref": "#/components/headers/RateLimit-Limit"}, "RateLimit-Remaining": {"$ref": "#/components/headers/RateLimit-Remaining"}, "RateLimit-Reset": {"$ref": "#/components/headers/RateLimit-Reset"}}}, "401": {"$ref": "#/components/responses/401"}, "429": {"$ref": "#/components/responses/429"}, "502": {"$ref": "#/components/responses/502"}}}, "post": {"tags": ["Session WhatsApp"], "summary": "Démarrer la session", "description": "Prépare la connexion de votre numéro (ou la relance si elle a échoué). Enchaînez ensuite avec `GET /session/qr` ou `POST /session/pairing-code`, puis interrogez `GET /session` jusqu'à `status: connected`.\n\nSans effet si le numéro est déjà connecté.", "operationId": "start_session", "responses": {"200": {"description": "Session démarrée (ou déjà connectée).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Session"}, "example": {"object": "session", "phone": "2250700000000", "status": "pairing", "connected_at": null}}}, "headers": {"RateLimit-Limit": {"$ref": "#/components/headers/RateLimit-Limit"}, "RateLimit-Remaining": {"$ref": "#/components/headers/RateLimit-Remaining"}, "RateLimit-Reset": {"$ref": "#/components/headers/RateLimit-Reset"}}}, "401": {"$ref": "#/components/responses/401"}, "429": {"$ref": "#/components/responses/429"}, "502": {"$ref": "#/components/responses/502"}}}}, "/session/qr": {"get": {"tags": ["Session WhatsApp"], "summary": "Obtenir le QR code", "description": "QR code à scanner depuis WhatsApp › Appareils connectés. Il change toutes les 20 secondes environ : redemandez-le tant que la session n'est pas connectée.", "operationId": "get_session_qr", "responses": {"200": {"description": "Le QR code, en image PNG encodée en base64.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SessionQr"}, "example": {"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA…", "expires_in": 20}}}, "headers": {"RateLimit-Limit": {"$ref": "#/components/headers/RateLimit-Limit"}, "RateLimit-Remaining": {"$ref": "#/components/headers/RateLimit-Remaining"}, "RateLimit-Reset": {"$ref": "#/components/headers/RateLimit-Reset"}}}, "401": {"$ref": "#/components/responses/401"}, "409": {"$ref": "#/components/responses/409Session"}, "429": {"$ref": "#/components/responses/429"}, "502": {"$ref": "#/components/responses/502"}}}}, "/session/pairing-code": {"post": {"tags": ["Session WhatsApp"], "summary": "Obtenir un code d'association", "description": "Code à 8 caractères à saisir dans WhatsApp › Appareils connectés › Connecter un appareil › *Se connecter avec le numéro de téléphone*. Pratique quand le QR code ne peut pas être scanné (même téléphone).", "operationId": "request_pairing_code", "responses": {"200": {"description": "Le code à saisir.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PairingCode"}, "example": {"code": "K7QM-2XPA", "phone": "2250700000000", "expires_in": 160}}}, "headers": {"RateLimit-Limit": {"$ref": "#/components/headers/RateLimit-Limit"}, "RateLimit-Remaining": {"$ref": "#/components/headers/RateLimit-Remaining"}, "RateLimit-Reset": {"$ref": "#/components/headers/RateLimit-Reset"}}}, "401": {"$ref": "#/components/responses/401"}, "409": {"$ref": "#/components/responses/409Session"}, "429": {"$ref": "#/components/responses/429"}, "502": {"$ref": "#/components/responses/502"}}}}, "/diffusions": {"post": {"tags": ["Diffusions"], "summary": "Lancer une diffusion", "description": "Crée une diffusion et la place dans la file d'envoi. Les destinataires sont fournis en JSON (`recipients`) ou dans un fichier CSV / XLSX (`multipart/form-data`, une colonne `phone`).\n\nUne seule diffusion est envoyée à la fois par numéro : les suivantes attendent leur tour (statut `queued`).\n\nLa vérification des numéros et la rédaction des formulations se font juste après, en arrière-plan. Si aucun numéro n'existe sur WhatsApp, la diffusion passe à `canceled` avec `cancel_reason: no_valid_recipient`.\n\nAvec `sender: diffuz`, chaque compte peut programmer un nombre limité de destinataires par jour (`403 daily_limit_reached` au-delà).", "operationId": "create_diffusion", "parameters": [{"name": "Idempotency-Key", "in": "header", "description": "Identifiant unique de la requête : la rejouer ne crée pas de doublon.", "schema": {"type": "string", "maxLength": 255, "example": "promo-wax-2026-10-27"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DiffusionCreate"}, "example": {"message": "Nouvelle collection Wax disponible en boutique ce week-end !", "recipients": ["2250700000001", "2250500000002", "2250100000003"], "variants": 3, "reference": "promo-wax-octobre", "callback_url": "https://crm.boutique-awa.ci/webhooks/diffuz", "opt_in_confirmed": true}}, "multipart/form-data": {"schema": {"$ref": "#/components/schemas/DiffusionCreateFile"}}}}, "responses": {"202": {"description": "Diffusion acceptée : l'envoi démarre en arrière-plan.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Diffusion"}, "example": {"id": "dif_7Hx2kQ9pLm", "object": "diffusion", "status": "queued", "sender": "own", "reference": "promo-wax-octobre", "message": "Nouvelle collection Wax disponible en boutique ce week-end !", "variants": 3, "recipients": {"total": 3, "excluded": 0, "pending": 3, "sent": 0, "failed": 0}, "scheduled_at": null, "started_at": null, "next_send_at": null, "completed_at": null, "pause_reason": null, "cancel_reason": null, "callback_url": "https://crm.boutique-awa.ci/webhooks/diffuz", "created_at": "2026-10-27T08:59:58Z"}}}, "headers": {"RateLimit-Limit": {"$ref": "#/components/headers/RateLimit-Limit"}, "RateLimit-Remaining": {"$ref": "#/components/headers/RateLimit-Remaining"}, "RateLimit-Reset": {"$ref": "#/components/headers/RateLimit-Reset"}}}, "400": {"$ref": "#/components/responses/400"}, "401": {"$ref": "#/components/responses/401"}, "403": {"$ref": "#/components/responses/403"}, "429": {"$ref": "#/components/responses/429"}}}, "get": {"tags": ["Diffusions"], "summary": "Lister les diffusions", "description": "Les plus récentes d'abord.", "operationId": "list_diffusions", "parameters": [{"name": "status", "in": "query", "description": "Filtre par statut.", "schema": {"$ref": "#/components/schemas/DiffusionStatus"}}, {"name": "created_after", "in": "query", "description": "Créées après cette date.", "schema": {"type": "string", "format": "date-time"}}, {"name": "created_before", "in": "query", "description": "Créées avant cette date.", "schema": {"type": "string", "format": "date-time"}}, {"name": "limit", "in": "query", "description": "Nombre d'éléments par page.", "schema": {"type": "integer", "minimum": 1, "maximum": 100, "default": 20}}, {"name": "cursor", "in": "query", "description": "Valeur de `next_cursor` renvoyée par la page précédente.", "schema": {"type": "string"}}], "responses": {"200": {"description": "Une page de diffusions.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DiffusionList"}, "example": {"object": "list", "data": [{"id": "dif_7Hx2kQ9pLm", "object": "diffusion", "status": "sending", "sender": "own", "reference": "promo-wax-octobre", "message": "Nouvelle collection Wax disponible en boutique ce week-end !", "variants": 3, "recipients": {"total": 3, "excluded": 0, "pending": 1, "sent": 2, "failed": 0}, "scheduled_at": null, "started_at": "2026-10-27T09:00:04Z", "next_send_at": "2026-10-27T09:01:12Z", "completed_at": null, "pause_reason": null, "cancel_reason": null, "callback_url": "https://crm.boutique-awa.ci/webhooks/diffuz", "created_at": "2026-10-27T08:59:58Z"}], "has_more": true, "next_cursor": "Y3VyXzE3OTMwOTE1OTg"}}}, "headers": {"RateLimit-Limit": {"$ref": "#/components/headers/RateLimit-Limit"}, "RateLimit-Remaining": {"$ref": "#/components/headers/RateLimit-Remaining"}, "RateLimit-Reset": {"$ref": "#/components/headers/RateLimit-Reset"}}}, "400": {"$ref": "#/components/responses/400"}, "401": {"$ref": "#/components/responses/401"}, "429": {"$ref": "#/components/responses/429"}}}}, "/diffusions/{id}": {"get": {"tags": ["Diffusions"], "summary": "Consulter une diffusion", "description": "Statut, progression et prochaine heure d'envoi.", "operationId": "get_diffusion", "parameters": [{"name": "id", "in": "path", "required": true, "description": "Identifiant de la diffusion (`dif_…`).", "schema": {"type": "string", "example": "dif_7Hx2kQ9pLm"}}], "responses": {"200": {"description": "La diffusion.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Diffusion"}, "example": {"id": "dif_7Hx2kQ9pLm", "object": "diffusion", "status": "sending", "sender": "own", "reference": "promo-wax-octobre", "message": "Nouvelle collection Wax disponible en boutique ce week-end !", "variants": 3, "recipients": {"total": 3, "excluded": 0, "pending": 1, "sent": 2, "failed": 0}, "scheduled_at": null, "started_at": "2026-10-27T09:00:04Z", "next_send_at": "2026-10-27T09:01:12Z", "completed_at": null, "pause_reason": null, "cancel_reason": null, "callback_url": "https://crm.boutique-awa.ci/webhooks/diffuz", "created_at": "2026-10-27T08:59:58Z"}}}, "headers": {"RateLimit-Limit": {"$ref": "#/components/headers/RateLimit-Limit"}, "RateLimit-Remaining": {"$ref": "#/components/headers/RateLimit-Remaining"}, "RateLimit-Reset": {"$ref": "#/components/headers/RateLimit-Reset"}}}, "401": {"$ref": "#/components/responses/401"}, "404": {"$ref": "#/components/responses/404"}, "429": {"$ref": "#/components/responses/429"}}}}, "/diffusions/{id}/pause": {"post": {"tags": ["Diffusions"], "summary": "Mettre en pause", "description": "Suspend l'envoi après le message en cours. Aucun destinataire n'est perdu.\n\nSeule une diffusion `sending` partie de **votre** numéro peut être mise en pause : le numéro Diffuz étant partagé, une diffusion `sender: diffuz` peut seulement être annulée.", "operationId": "pause_diffusion", "parameters": [{"name": "id", "in": "path", "required": true, "description": "Identifiant de la diffusion (`dif_…`).", "schema": {"type": "string", "example": "dif_7Hx2kQ9pLm"}}], "responses": {"200": {"description": "La diffusion après l'action.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Diffusion"}, "example": {"id": "dif_7Hx2kQ9pLm", "object": "diffusion", "status": "paused", "sender": "own", "reference": "promo-wax-octobre", "message": "Nouvelle collection Wax disponible en boutique ce week-end !", "variants": 3, "recipients": {"total": 3, "excluded": 0, "pending": 1, "sent": 2, "failed": 0}, "scheduled_at": null, "started_at": "2026-10-27T09:00:04Z", "next_send_at": "2026-10-27T09:01:12Z", "completed_at": null, "pause_reason": null, "cancel_reason": null, "callback_url": "https://crm.boutique-awa.ci/webhooks/diffuz", "created_at": "2026-10-27T08:59:58Z"}}}, "headers": {"RateLimit-Limit": {"$ref": "#/components/headers/RateLimit-Limit"}, "RateLimit-Remaining": {"$ref": "#/components/headers/RateLimit-Remaining"}, "RateLimit-Reset": {"$ref": "#/components/headers/RateLimit-Reset"}}}, "401": {"$ref": "#/components/responses/401"}, "403": {"$ref": "#/components/responses/403"}, "404": {"$ref": "#/components/responses/404"}, "409": {"description": "Seule une diffusion en cours d'envoi, partie de votre numéro, peut être mise en pause.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": {"type": "conflict", "code": "invalid_state", "message": "Seule une diffusion en cours d'envoi, partie de votre numéro, peut être mise en pause.", "param": null, "request_id": "req_8Kq2mX4tVb"}}}}}, "429": {"$ref": "#/components/responses/429"}}}}, "/diffusions/{id}/resume": {"post": {"tags": ["Diffusions"], "summary": "Reprendre", "description": "Relance une diffusion mise en pause par vous. Seuls les destinataires pas encore servis reçoivent le message : personne ne le reçoit deux fois.\n\nUne pause due à une restriction WhatsApp (`pause_reason: whatsapp_restriction`) ne peut être levée que par l'équipe Diffuz, pour ne pas faire bloquer votre numéro.", "operationId": "resume_diffusion", "parameters": [{"name": "id", "in": "path", "required": true, "description": "Identifiant de la diffusion (`dif_…`).", "schema": {"type": "string", "example": "dif_7Hx2kQ9pLm"}}], "responses": {"200": {"description": "La diffusion après l'action.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Diffusion"}, "example": {"id": "dif_7Hx2kQ9pLm", "object": "diffusion", "status": "sending", "sender": "own", "reference": "promo-wax-octobre", "message": "Nouvelle collection Wax disponible en boutique ce week-end !", "variants": 3, "recipients": {"total": 3, "excluded": 0, "pending": 1, "sent": 2, "failed": 0}, "scheduled_at": null, "started_at": "2026-10-27T09:00:04Z", "next_send_at": "2026-10-27T09:01:12Z", "completed_at": null, "pause_reason": null, "cancel_reason": null, "callback_url": "https://crm.boutique-awa.ci/webhooks/diffuz", "created_at": "2026-10-27T08:59:58Z"}}}, "headers": {"RateLimit-Limit": {"$ref": "#/components/headers/RateLimit-Limit"}, "RateLimit-Remaining": {"$ref": "#/components/headers/RateLimit-Remaining"}, "RateLimit-Reset": {"$ref": "#/components/headers/RateLimit-Reset"}}}, "401": {"$ref": "#/components/responses/401"}, "403": {"$ref": "#/components/responses/403"}, "404": {"$ref": "#/components/responses/404"}, "409": {"description": "Cette diffusion n'est pas en pause, ou sa pause a été décidée par Diffuz.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": {"type": "conflict", "code": "invalid_state", "message": "Cette diffusion n'est pas en pause, ou sa pause a été décidée par Diffuz.", "param": null, "request_id": "req_8Kq2mX4tVb"}}}}}, "429": {"$ref": "#/components/responses/429"}}}}, "/diffusions/{id}/cancel": {"post": {"tags": ["Diffusions"], "summary": "Annuler", "description": "Arrête définitivement la diffusion. Les messages déjà partis ne sont pas rappelés ; les autres ne seront jamais envoyés.", "operationId": "cancel_diffusion", "parameters": [{"name": "id", "in": "path", "required": true, "description": "Identifiant de la diffusion (`dif_…`).", "schema": {"type": "string", "example": "dif_7Hx2kQ9pLm"}}], "responses": {"200": {"description": "La diffusion après l'action.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Diffusion"}, "example": {"id": "dif_7Hx2kQ9pLm", "object": "diffusion", "status": "canceled", "sender": "own", "reference": "promo-wax-octobre", "message": "Nouvelle collection Wax disponible en boutique ce week-end !", "variants": 3, "recipients": {"total": 3, "excluded": 0, "pending": 1, "sent": 2, "failed": 0}, "scheduled_at": null, "started_at": "2026-10-27T09:00:04Z", "next_send_at": "2026-10-27T09:01:12Z", "completed_at": null, "pause_reason": null, "cancel_reason": null, "callback_url": "https://crm.boutique-awa.ci/webhooks/diffuz", "created_at": "2026-10-27T08:59:58Z"}}}, "headers": {"RateLimit-Limit": {"$ref": "#/components/headers/RateLimit-Limit"}, "RateLimit-Remaining": {"$ref": "#/components/headers/RateLimit-Remaining"}, "RateLimit-Reset": {"$ref": "#/components/headers/RateLimit-Reset"}}}, "401": {"$ref": "#/components/responses/401"}, "403": {"$ref": "#/components/responses/403"}, "404": {"$ref": "#/components/responses/404"}, "409": {"description": "Une diffusion terminée ou déjà annulée ne peut plus être annulée.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": {"type": "conflict", "code": "invalid_state", "message": "Une diffusion terminée ou déjà annulée ne peut plus être annulée.", "param": null, "request_id": "req_8Kq2mX4tVb"}}}}}, "429": {"$ref": "#/components/responses/429"}}}}, "/diffusions/{id}/recipients": {"get": {"tags": ["Destinataires"], "summary": "Lister les destinataires", "description": "Statut de chaque destinataire, dans l'ordre d'envoi.", "operationId": "list_recipients", "parameters": [{"name": "id", "in": "path", "required": true, "description": "Identifiant de la diffusion (`dif_…`).", "schema": {"type": "string", "example": "dif_7Hx2kQ9pLm"}}, {"name": "status", "in": "query", "description": "Filtre par statut.", "schema": {"$ref": "#/components/schemas/RecipientStatus"}}, {"name": "limit", "in": "query", "description": "Nombre d'éléments par page.", "schema": {"type": "integer", "minimum": 1, "maximum": 100, "default": 20}}, {"name": "cursor", "in": "query", "description": "Valeur de `next_cursor` renvoyée par la page précédente.", "schema": {"type": "string"}}], "responses": {"200": {"description": "Une page de destinataires.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/RecipientList"}, "example": {"object": "list", "data": [{"object": "recipient", "diffusion_id": "dif_7Hx2kQ9pLm", "phone": "2250700000001", "status": "read", "variant": 2, "sent_at": "2026-10-27T09:00:04Z", "delivered_at": "2026-10-27T09:00:06Z", "read_at": "2026-10-27T09:14:51Z", "error": null}], "has_more": false, "next_cursor": null}}}, "headers": {"RateLimit-Limit": {"$ref": "#/components/headers/RateLimit-Limit"}, "RateLimit-Remaining": {"$ref": "#/components/headers/RateLimit-Remaining"}, "RateLimit-Reset": {"$ref": "#/components/headers/RateLimit-Reset"}}}, "401": {"$ref": "#/components/responses/401"}, "404": {"$ref": "#/components/responses/404"}, "429": {"$ref": "#/components/responses/429"}}}}, "/diffusions/{id}/report": {"get": {"tags": ["Diffusions"], "summary": "Télécharger le rapport", "description": "Rapport complet de la diffusion (un destinataire par ligne : statut, formulation reçue, heures d'envoi, de livraison et de lecture), en Excel ou en CSV.", "operationId": "get_diffusion_report", "parameters": [{"name": "id", "in": "path", "required": true, "description": "Identifiant de la diffusion (`dif_…`).", "schema": {"type": "string", "example": "dif_7Hx2kQ9pLm"}}, {"name": "format", "in": "query", "description": "Format du fichier.", "schema": {"type": "string", "enum": ["xlsx", "csv"], "default": "xlsx"}}], "responses": {"200": {"description": "Le fichier du rapport.", "content": {"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {"schema": {"type": "string", "contentMediaType": "application/octet-stream"}}, "text/csv": {"schema": {"type": "string"}}}}, "401": {"$ref": "#/components/responses/401"}, "404": {"$ref": "#/components/responses/404"}, "429": {"$ref": "#/components/responses/429"}}}}, "/diffusions/{id}/stats": {"get": {"tags": ["Diffusions"], "summary": "Bilan d'une diffusion", "description": "Compteurs et taux de livraison et de lecture.", "operationId": "get_diffusion_stats", "parameters": [{"name": "id", "in": "path", "required": true, "description": "Identifiant de la diffusion (`dif_…`).", "schema": {"type": "string", "example": "dif_7Hx2kQ9pLm"}}], "responses": {"200": {"description": "Le bilan.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DiffusionStats"}, "example": {"diffusion_id": "dif_7Hx2kQ9pLm", "total": 3, "excluded": 0, "sent": 3, "delivered": 3, "read": 2, "failed": 0, "variants_used": 3, "delivery_rate": 1.0, "read_rate": 0.667}}}, "headers": {"RateLimit-Limit": {"$ref": "#/components/headers/RateLimit-Limit"}, "RateLimit-Remaining": {"$ref": "#/components/headers/RateLimit-Remaining"}, "RateLimit-Reset": {"$ref": "#/components/headers/RateLimit-Reset"}}}, "401": {"$ref": "#/components/responses/401"}, "404": {"$ref": "#/components/responses/404"}, "429": {"$ref": "#/components/responses/429"}}}}}, "webhooks": {"diffusion.started": {"post": {"tags": ["Webhooks"], "summary": "Diffusion démarrée", "description": "Le premier message est sur le point de partir.\n\nDiffuz attend une réponse `2xx` en moins de 10 s. Sinon, l'événement est renvoyé jusqu'à 6 fois sur 24 h.", "operationId": "webhook_diffusion_started", "parameters": [{"name": "X-Diffuz-Signature", "in": "header", "required": true, "description": "`t=<horodatage>,v1=<signature>` : la signature est le HMAC-SHA256, en hexadécimal, de la chaîne `<horodatage>.<corps brut>` avec votre `webhook_secret` (voir `GET /auth/verify`). Vérifiez-la avant de traiter l'événement.", "schema": {"type": "string", "example": "t=1793091604,v1=5f2b9c…"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["id", "type", "created_at", "data"], "properties": {"id": {"type": "string", "description": "Identifiant de l'événement (`evt_…`).", "example": "evt_3Lp9sWq1Za"}, "type": {"type": "string", "const": "diffusion.started"}, "created_at": {"type": "string", "format": "date-time"}, "data": {"$ref": "#/components/schemas/Diffusion"}}}, "example": {"id": "evt_3Lp9sWq1Za", "type": "diffusion.started", "created_at": "2026-10-27T09:00:04Z", "data": {"id": "dif_7Hx2kQ9pLm", "object": "diffusion", "status": "sending", "sender": "own", "reference": "promo-wax-octobre", "message": "Nouvelle collection Wax disponible en boutique ce week-end !", "variants": 3, "recipients": {"total": 3, "excluded": 0, "pending": 1, "sent": 2, "failed": 0}, "scheduled_at": null, "started_at": "2026-10-27T09:00:04Z", "next_send_at": "2026-10-27T09:01:12Z", "completed_at": null, "pause_reason": null, "cancel_reason": null, "callback_url": "https://crm.boutique-awa.ci/webhooks/diffuz", "created_at": "2026-10-27T08:59:58Z"}}}}}, "responses": {"200": {"description": "Événement bien reçu."}}}}, "diffusion.paused": {"post": {"tags": ["Webhooks"], "summary": "Diffusion en pause", "description": "La diffusion s'est arrêtée : à votre demande, ou d'elle-même en cas de restriction WhatsApp ou de numéro déconnecté (voir `pause_reason`).\n\nDiffuz attend une réponse `2xx` en moins de 10 s. Sinon, l'événement est renvoyé jusqu'à 6 fois sur 24 h.", "operationId": "webhook_diffusion_paused", "parameters": [{"name": "X-Diffuz-Signature", "in": "header", "required": true, "description": "`t=<horodatage>,v1=<signature>` : la signature est le HMAC-SHA256, en hexadécimal, de la chaîne `<horodatage>.<corps brut>` avec votre `webhook_secret` (voir `GET /auth/verify`). Vérifiez-la avant de traiter l'événement.", "schema": {"type": "string", "example": "t=1793091604,v1=5f2b9c…"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["id", "type", "created_at", "data"], "properties": {"id": {"type": "string", "description": "Identifiant de l'événement (`evt_…`).", "example": "evt_3Lp9sWq1Za"}, "type": {"type": "string", "const": "diffusion.paused"}, "created_at": {"type": "string", "format": "date-time"}, "data": {"$ref": "#/components/schemas/Diffusion"}}}, "example": {"id": "evt_3Lp9sWq1Za", "type": "diffusion.paused", "created_at": "2026-10-27T09:00:04Z", "data": {"id": "dif_7Hx2kQ9pLm", "object": "diffusion", "status": "paused", "sender": "own", "reference": "promo-wax-octobre", "message": "Nouvelle collection Wax disponible en boutique ce week-end !", "variants": 3, "recipients": {"total": 3, "excluded": 0, "pending": 1, "sent": 2, "failed": 0}, "scheduled_at": null, "started_at": "2026-10-27T09:00:04Z", "next_send_at": null, "completed_at": null, "pause_reason": "whatsapp_restriction", "cancel_reason": null, "callback_url": "https://crm.boutique-awa.ci/webhooks/diffuz", "created_at": "2026-10-27T08:59:58Z"}}}}}, "responses": {"200": {"description": "Événement bien reçu."}}}}, "diffusion.completed": {"post": {"tags": ["Webhooks"], "summary": "Diffusion terminée", "description": "Tous les destinataires ont été servis.\n\nDiffuz attend une réponse `2xx` en moins de 10 s. Sinon, l'événement est renvoyé jusqu'à 6 fois sur 24 h.", "operationId": "webhook_diffusion_completed", "parameters": [{"name": "X-Diffuz-Signature", "in": "header", "required": true, "description": "`t=<horodatage>,v1=<signature>` : la signature est le HMAC-SHA256, en hexadécimal, de la chaîne `<horodatage>.<corps brut>` avec votre `webhook_secret` (voir `GET /auth/verify`). Vérifiez-la avant de traiter l'événement.", "schema": {"type": "string", "example": "t=1793091604,v1=5f2b9c…"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["id", "type", "created_at", "data"], "properties": {"id": {"type": "string", "description": "Identifiant de l'événement (`evt_…`).", "example": "evt_3Lp9sWq1Za"}, "type": {"type": "string", "const": "diffusion.completed"}, "created_at": {"type": "string", "format": "date-time"}, "data": {"$ref": "#/components/schemas/Diffusion"}}}, "example": {"id": "evt_3Lp9sWq1Za", "type": "diffusion.completed", "created_at": "2026-10-27T09:00:04Z", "data": {"id": "dif_7Hx2kQ9pLm", "object": "diffusion", "status": "completed", "sender": "own", "reference": "promo-wax-octobre", "message": "Nouvelle collection Wax disponible en boutique ce week-end !", "variants": 3, "recipients": {"total": 3, "excluded": 0, "pending": 0, "sent": 3, "failed": 0}, "scheduled_at": null, "started_at": "2026-10-27T09:00:04Z", "next_send_at": null, "completed_at": "2026-10-27T09:02:31Z", "pause_reason": null, "cancel_reason": null, "callback_url": "https://crm.boutique-awa.ci/webhooks/diffuz", "created_at": "2026-10-27T08:59:58Z"}}}}}, "responses": {"200": {"description": "Événement bien reçu."}}}}, "recipient.sent": {"post": {"tags": ["Webhooks"], "summary": "Message envoyé", "description": "WhatsApp a accepté le message.\n\nDiffuz attend une réponse `2xx` en moins de 10 s. Sinon, l'événement est renvoyé jusqu'à 6 fois sur 24 h.", "operationId": "webhook_recipient_sent", "parameters": [{"name": "X-Diffuz-Signature", "in": "header", "required": true, "description": "`t=<horodatage>,v1=<signature>` : la signature est le HMAC-SHA256, en hexadécimal, de la chaîne `<horodatage>.<corps brut>` avec votre `webhook_secret` (voir `GET /auth/verify`). Vérifiez-la avant de traiter l'événement.", "schema": {"type": "string", "example": "t=1793091604,v1=5f2b9c…"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["id", "type", "created_at", "data"], "properties": {"id": {"type": "string", "description": "Identifiant de l'événement (`evt_…`).", "example": "evt_3Lp9sWq1Za"}, "type": {"type": "string", "const": "recipient.sent"}, "created_at": {"type": "string", "format": "date-time"}, "data": {"$ref": "#/components/schemas/Recipient"}}}, "example": {"id": "evt_3Lp9sWq1Za", "type": "recipient.sent", "created_at": "2026-10-27T09:00:04Z", "data": {"object": "recipient", "diffusion_id": "dif_7Hx2kQ9pLm", "phone": "2250700000001", "status": "sent", "variant": 2, "sent_at": "2026-10-27T09:00:04Z", "delivered_at": null, "read_at": null, "error": null}}}}}, "responses": {"200": {"description": "Événement bien reçu."}}}}, "recipient.delivered": {"post": {"tags": ["Webhooks"], "summary": "Message livré", "description": "Le message est arrivé sur le téléphone du destinataire.\n\nDiffuz attend une réponse `2xx` en moins de 10 s. Sinon, l'événement est renvoyé jusqu'à 6 fois sur 24 h.", "operationId": "webhook_recipient_delivered", "parameters": [{"name": "X-Diffuz-Signature", "in": "header", "required": true, "description": "`t=<horodatage>,v1=<signature>` : la signature est le HMAC-SHA256, en hexadécimal, de la chaîne `<horodatage>.<corps brut>` avec votre `webhook_secret` (voir `GET /auth/verify`). Vérifiez-la avant de traiter l'événement.", "schema": {"type": "string", "example": "t=1793091604,v1=5f2b9c…"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["id", "type", "created_at", "data"], "properties": {"id": {"type": "string", "description": "Identifiant de l'événement (`evt_…`).", "example": "evt_3Lp9sWq1Za"}, "type": {"type": "string", "const": "recipient.delivered"}, "created_at": {"type": "string", "format": "date-time"}, "data": {"$ref": "#/components/schemas/Recipient"}}}, "example": {"id": "evt_3Lp9sWq1Za", "type": "recipient.delivered", "created_at": "2026-10-27T09:00:04Z", "data": {"object": "recipient", "diffusion_id": "dif_7Hx2kQ9pLm", "phone": "2250700000001", "status": "delivered", "variant": 2, "sent_at": "2026-10-27T09:00:04Z", "delivered_at": "2026-10-27T09:00:06Z", "read_at": null, "error": null}}}}}, "responses": {"200": {"description": "Événement bien reçu."}}}}, "recipient.read": {"post": {"tags": ["Webhooks"], "summary": "Message lu", "description": "Le destinataire a ouvert le message (si ses confirmations de lecture sont actives).\n\nDiffuz attend une réponse `2xx` en moins de 10 s. Sinon, l'événement est renvoyé jusqu'à 6 fois sur 24 h.", "operationId": "webhook_recipient_read", "parameters": [{"name": "X-Diffuz-Signature", "in": "header", "required": true, "description": "`t=<horodatage>,v1=<signature>` : la signature est le HMAC-SHA256, en hexadécimal, de la chaîne `<horodatage>.<corps brut>` avec votre `webhook_secret` (voir `GET /auth/verify`). Vérifiez-la avant de traiter l'événement.", "schema": {"type": "string", "example": "t=1793091604,v1=5f2b9c…"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["id", "type", "created_at", "data"], "properties": {"id": {"type": "string", "description": "Identifiant de l'événement (`evt_…`).", "example": "evt_3Lp9sWq1Za"}, "type": {"type": "string", "const": "recipient.read"}, "created_at": {"type": "string", "format": "date-time"}, "data": {"$ref": "#/components/schemas/Recipient"}}}, "example": {"id": "evt_3Lp9sWq1Za", "type": "recipient.read", "created_at": "2026-10-27T09:00:04Z", "data": {"object": "recipient", "diffusion_id": "dif_7Hx2kQ9pLm", "phone": "2250700000001", "status": "read", "variant": 2, "sent_at": "2026-10-27T09:00:04Z", "delivered_at": "2026-10-27T09:00:06Z", "read_at": "2026-10-27T09:14:51Z", "error": null}}}}}, "responses": {"200": {"description": "Événement bien reçu."}}}}, "recipient.failed": {"post": {"tags": ["Webhooks"], "summary": "Échec d'envoi", "description": "Le message n'a pas pu être envoyé à ce destinataire.\n\nDiffuz attend une réponse `2xx` en moins de 10 s. Sinon, l'événement est renvoyé jusqu'à 6 fois sur 24 h.", "operationId": "webhook_recipient_failed", "parameters": [{"name": "X-Diffuz-Signature", "in": "header", "required": true, "description": "`t=<horodatage>,v1=<signature>` : la signature est le HMAC-SHA256, en hexadécimal, de la chaîne `<horodatage>.<corps brut>` avec votre `webhook_secret` (voir `GET /auth/verify`). Vérifiez-la avant de traiter l'événement.", "schema": {"type": "string", "example": "t=1793091604,v1=5f2b9c…"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["id", "type", "created_at", "data"], "properties": {"id": {"type": "string", "description": "Identifiant de l'événement (`evt_…`).", "example": "evt_3Lp9sWq1Za"}, "type": {"type": "string", "const": "recipient.failed"}, "created_at": {"type": "string", "format": "date-time"}, "data": {"$ref": "#/components/schemas/Recipient"}}}, "example": {"id": "evt_3Lp9sWq1Za", "type": "recipient.failed", "created_at": "2026-10-27T09:00:04Z", "data": {"object": "recipient", "diffusion_id": "dif_7Hx2kQ9pLm", "phone": "2250700000001", "status": "failed", "variant": null, "sent_at": null, "delivered_at": null, "read_at": null, "error": {"code": "number_rejected", "message": "WhatsApp a refusé ce numéro."}}}}}}, "responses": {"200": {"description": "Événement bien reçu."}}}}}, "components": {"securitySchemes": {"cleApi": {"type": "http", "scheme": "bearer", "bearerFormat": "dfz_live_…", "description": "Clé API délivrée par l'équipe Diffuz, envoyée dans l'en-tête `Authorization: Bearer dfz_live_…`."}}, "headers": {"RateLimit-Limit": {"description": "Requêtes autorisées par minute.", "schema": {"type": "integer", "example": 120}}, "RateLimit-Remaining": {"description": "Requêtes restantes dans la minute.", "schema": {"type": "integer", "example": 117}}, "RateLimit-Reset": {"description": "Secondes avant la remise à zéro du compteur.", "schema": {"type": "integer", "example": 42}}}, "responses": {"400": {"description": "Requête invalide.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": {"type": "invalid_request", "code": "recipients_empty", "message": "Ajoutez au moins un destinataire.", "param": "recipients", "request_id": "req_8Kq2mX4tVb"}}}}}, "401": {"description": "Clé absente, invalide ou révoquée.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": {"type": "authentication_error", "code": "invalid_api_key", "message": "Clé API invalide ou révoquée.", "param": null, "request_id": "req_8Kq2mX4tVb"}}}}}, "403": {"description": "Compte non autorisé ou numéro WhatsApp déconnecté.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": {"type": "permission_error", "code": "whatsapp_disconnected", "message": "Votre numéro WhatsApp est déconnecté : reconnectez-le avant de diffuser.", "param": null, "request_id": "req_8Kq2mX4tVb"}}}}}, "404": {"description": "Ressource introuvable.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": {"type": "not_found", "code": "diffusion_not_found", "message": "Aucune diffusion ne porte cet identifiant.", "param": "id", "request_id": "req_8Kq2mX4tVb"}}}}}, "409Session": {"description": "Session dans un état incompatible.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": {"type": "conflict", "code": "session_already_connected", "message": "Votre numéro est déjà connecté : aucun QR code n'est nécessaire.", "param": null, "request_id": "req_8Kq2mX4tVb"}}}}}, "502": {"description": "Passerelle WhatsApp injoignable.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": {"type": "api_error", "code": "whatsapp_unavailable", "message": "La passerelle WhatsApp ne répond pas. Réessayez dans quelques instants.", "param": null, "request_id": "req_8Kq2mX4tVb"}}}}}, "429": {"description": "Trop de requêtes.", "headers": {"Retry-After": {"description": "Secondes à attendre avant de réessayer.", "schema": {"type": "integer", "example": 30}}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": {"type": "rate_limited", "code": "too_many_requests", "message": "Limite de 120 requêtes par minute atteinte.", "param": null, "request_id": "req_8Kq2mX4tVb"}}}}}}, "schemas": {"Phone": {"type": "string", "pattern": "^[1-9][0-9]{7,14}$", "description": "Numéro au format international, chiffres uniquement.", "example": "2250700000001"}, "DiffusionStatus": {"type": "string", "enum": ["scheduled", "queued", "sending", "paused", "completed", "canceled"], "description": "`scheduled` programmée pour plus tard · `queued` en file d'attente · `sending` en cours d'envoi · `paused` en pause · `completed` terminée · `canceled` annulée."}, "RecipientStatus": {"type": "string", "enum": ["pending", "sent", "delivered", "read", "failed", "excluded"], "description": "`pending` pas encore envoyé · `sent` accepté par WhatsApp · `delivered` arrivé sur le téléphone · `read` lu · `failed` échec · `excluded` écarté au lancement (numéro absent de WhatsApp)."}, "Sender": {"type": "string", "enum": ["own", "diffuz"], "default": "own", "description": "`own` depuis votre numéro connecté · `diffuz` depuis le numéro du bot Diffuz."}, "RecipientInput": {"description": "Un numéro seul, ou un objet pour personnaliser le message de ce destinataire.", "oneOf": [{"$ref": "#/components/schemas/Phone"}, {"type": "object", "required": ["phone"], "properties": {"phone": {"$ref": "#/components/schemas/Phone"}, "variables": {"type": "object", "additionalProperties": {"type": "string"}, "description": "Valeurs des `{{variables}}` du message commun.", "example": {"prenom": "Awa", "commande": "n°1245"}}, "message": {"type": "string", "maxLength": 4096, "description": "Message propre à ce destinataire : remplace le message commun et part tel quel (sans variantes)."}}}]}, "DiffusionCreate": {"type": "object", "required": ["message", "recipients", "opt_in_confirmed"], "properties": {"message": {"type": "string", "minLength": 1, "maxLength": 4096, "description": "Texte à diffuser, avec d'éventuelles `{{variables}}`. Emojis et retours à la ligne acceptés."}, "recipients": {"type": "array", "minItems": 1, "maxItems": 10000, "items": {"$ref": "#/components/schemas/RecipientInput"}, "description": "Destinataires. Un même numéro n'est servi qu'une fois."}, "sender": {"$ref": "#/components/schemas/Sender"}, "variants": {"type": "integer", "minimum": 1, "maximum": 5, "default": 3, "description": "Nombre de formulations à utiliser. `1` envoie votre texte tel quel à tout le monde."}, "scheduled_at": {"type": ["string", "null"], "format": "date-time", "description": "Lancement différé. Absent : dès que possible."}, "reference": {"type": ["string", "null"], "maxLength": 100, "description": "Votre propre identifiant, renvoyé tel quel."}, "callback_url": {"type": ["string", "null"], "format": "uri", "description": "URL HTTPS qui recevra les webhooks de cette diffusion."}, "opt_in_confirmed": {"type": "boolean", "const": true, "description": "Vous certifiez que ces contacts ont accepté de recevoir vos messages."}}}, "DiffusionCreateFile": {"type": "object", "required": ["message", "file", "opt_in_confirmed"], "properties": {"message": {"type": "string", "maxLength": 4096}, "file": {"type": "string", "contentMediaType": "application/octet-stream", "description": "Fichier CSV ou XLSX avec une colonne `phone` ; les autres colonnes deviennent des `{{variables}}`."}, "sender": {"$ref": "#/components/schemas/Sender"}, "variants": {"type": "integer", "minimum": 1, "maximum": 5, "default": 3}, "scheduled_at": {"type": "string", "format": "date-time"}, "reference": {"type": "string", "maxLength": 100}, "callback_url": {"type": "string", "format": "uri"}, "opt_in_confirmed": {"type": "boolean", "const": true}}}, "Session": {"type": "object", "properties": {"object": {"type": "string", "const": "session"}, "phone": {"$ref": "#/components/schemas/Phone"}, "status": {"type": "string", "enum": ["connected", "pairing", "disconnected"], "description": "`connected` prêt à diffuser · `pairing` en attente du QR code ou du code · `disconnected` à reconnecter (`POST /session`)."}, "connected_at": {"type": ["string", "null"], "format": "date-time"}}}, "SessionQr": {"type": "object", "properties": {"image": {"type": "string", "description": "Image PNG au format data URI, à afficher telle quelle dans une balise `<img>`."}, "expires_in": {"type": "integer", "description": "Durée de validité indicative, en secondes."}}}, "PairingCode": {"type": "object", "properties": {"code": {"type": "string", "description": "Code à saisir dans WhatsApp."}, "phone": {"$ref": "#/components/schemas/Phone"}, "expires_in": {"type": "integer", "description": "Durée de validité indicative, en secondes."}}}, "Diffusion": {"type": "object", "properties": {"id": {"type": "string", "example": "dif_7Hx2kQ9pLm"}, "object": {"type": "string", "const": "diffusion"}, "status": {"$ref": "#/components/schemas/DiffusionStatus"}, "sender": {"$ref": "#/components/schemas/Sender"}, "reference": {"type": ["string", "null"]}, "message": {"type": "string"}, "variants": {"type": "integer", "description": "Formulations utilisées."}, "recipients": {"type": "object", "description": "Progression.", "properties": {"total": {"type": "integer"}, "excluded": {"type": "integer", "description": "Écartés (absents de WhatsApp)."}, "pending": {"type": "integer"}, "sent": {"type": "integer"}, "failed": {"type": "integer"}}}, "scheduled_at": {"type": ["string", "null"], "format": "date-time"}, "started_at": {"type": ["string", "null"], "format": "date-time"}, "next_send_at": {"type": ["string", "null"], "format": "date-time", "description": "Heure prévue du prochain message."}, "completed_at": {"type": ["string", "null"], "format": "date-time"}, "pause_reason": {"type": ["string", "null"], "enum": ["requested", "whatsapp_restriction", "whatsapp_disconnected", "paused_by_diffuz", null], "description": "`requested` à votre demande · `whatsapp_restriction` restriction WhatsApp (463 / 475) · `whatsapp_disconnected` numéro déconnecté · `paused_by_diffuz` pause posée par l'équipe Diffuz."}, "cancel_reason": {"type": ["string", "null"], "enum": ["requested", "no_valid_recipient", null], "description": "`requested` annulée par vous · `no_valid_recipient` aucun numéro n'existe sur WhatsApp."}, "callback_url": {"type": ["string", "null"], "format": "uri"}, "created_at": {"type": "string", "format": "date-time"}}}, "DiffusionList": {"type": "object", "properties": {"object": {"type": "string", "const": "list"}, "data": {"type": "array", "items": {"$ref": "#/components/schemas/Diffusion"}}, "has_more": {"type": "boolean"}, "next_cursor": {"type": ["string", "null"]}}}, "Recipient": {"type": "object", "properties": {"object": {"type": "string", "const": "recipient"}, "diffusion_id": {"type": "string"}, "phone": {"$ref": "#/components/schemas/Phone"}, "status": {"$ref": "#/components/schemas/RecipientStatus"}, "variant": {"type": ["integer", "null"], "description": "Formulation reçue."}, "sent_at": {"type": ["string", "null"], "format": "date-time"}, "delivered_at": {"type": ["string", "null"], "format": "date-time"}, "read_at": {"type": ["string", "null"], "format": "date-time"}, "error": {"type": ["object", "null"], "properties": {"code": {"type": "string"}, "message": {"type": "string"}}}}}, "RecipientList": {"type": "object", "properties": {"object": {"type": "string", "const": "list"}, "data": {"type": "array", "items": {"$ref": "#/components/schemas/Recipient"}}, "has_more": {"type": "boolean"}, "next_cursor": {"type": ["string", "null"]}}}, "DiffusionStats": {"type": "object", "properties": {"diffusion_id": {"type": "string"}, "total": {"type": "integer"}, "excluded": {"type": "integer"}, "sent": {"type": "integer"}, "delivered": {"type": "integer"}, "read": {"type": "integer"}, "failed": {"type": "integer"}, "variants_used": {"type": "integer"}, "delivery_rate": {"type": "number", "minimum": 0, "maximum": 1, "description": "Livrés ÷ envoyés."}, "read_rate": {"type": "number", "minimum": 0, "maximum": 1, "description": "Lus ÷ livrés."}}}, "KeyVerification": {"type": "object", "properties": {"valid": {"type": "boolean"}, "key": {"type": "object", "properties": {"name": {"type": "string"}, "prefix": {"type": "string"}, "created_at": {"type": "string", "format": "date-time"}, "last_used_at": {"type": ["string", "null"], "format": "date-time"}}}, "account": {"type": "object", "properties": {"company_name": {"type": "string"}, "phone": {"$ref": "#/components/schemas/Phone"}, "authorized": {"type": "boolean", "description": "Compte validé par l'équipe Diffuz."}, "whatsapp_status": {"type": "string", "enum": ["connected", "disconnected", "pairing"]}}}, "webhook_secret": {"type": "string", "description": "Secret de cette clé pour vérifier la signature `X-Diffuz-Signature` des webhooks."}}}, "KeyRotation": {"type": "object", "properties": {"key": {"type": "string", "description": "Nouvelle clé, affichée une seule fois."}, "prefix": {"type": "string"}, "previous_key_expires_at": {"type": "string", "format": "date-time"}}}, "Error": {"type": "object", "required": ["error"], "properties": {"error": {"type": "object", "required": ["type", "code", "message", "request_id"], "properties": {"type": {"type": "string", "enum": ["invalid_request", "authentication_error", "permission_error", "not_found", "conflict", "unprocessable", "rate_limited", "api_error"]}, "code": {"type": "string", "description": "Code précis, stable dans le temps."}, "message": {"type": "string", "description": "Explication lisible, en français."}, "param": {"type": ["string", "null"], "description": "Champ en cause."}, "request_id": {"type": "string", "description": "À communiquer au support en cas de besoin."}}}}}}}}