{"openapi":"3.0.3","info":{"title":"ReachFlow API publique","version":"1.0.0","description":"API REST v1 — messages transactionnels WhatsApp, OTP, providers. Authentification : en-tête `X-API-Key`.\n\n## ⚠️ Protection anti-bannissement (montée en puissance)\n\nWhatsApp bannit les numéros qui envoient trop, trop vite, surtout les numéros récents. Pour protéger vos numéros, **chaque envoi via l'API respecte par défaut la montée en puissance (warm-up)** de l'instance, exactement comme les campagnes : un plafond quotidien progressif (sur 14 jours) et un plafond de nouveaux contacts par jour.\n\nQuand un plafond est atteint, le message est mis en file puis **passe en `failed` avec `failureCode = warmup_daily_limit`** (il n'est pas envoyé). Vous le constatez via le webhook `message.failed` ou `GET /messages/{id}`.\n\nDeux protections sont **toujours actives et NON désactivables** :\n- **Plancher numéros récents** : un numéro encore en début de warm-up reste plafonné même si vous désactivez la protection.\n- **Disjoncteur de risque** : si le score de risque d'un numéro devient critique (blocages détectés), les envois sont suspendus (`failureCode = risk_circuit_open`) jusqu'au rétablissement.\n\n## Désactiver la montée en puissance (à vos risques)\n\nPour un numéro mature à fort volume transactionnel (OTP, notifications consenties), vous pouvez **lever le plafonnement warm-up par numéro** depuis le tableau de bord : **Instances → (votre numéro) → Politique d'envoi API → désactiver la montée en puissance**. La désactivation exige une confirmation explicite d'acceptation du risque et est tracée (qui / quand).\n\n> **Avertissement** : désactiver la montée en puissance peut entraîner le **bannissement définitif du numéro par WhatsApp** en cas d'abus ou de volume trop élevé. Vous restez responsable du consentement de vos destinataires."},"servers":[{"url":"/api/v1","description":"Instance ReachFlow"}],"tags":[{"name":"Providers","description":"Numéros WhatsApp (channel_providers) connectés."},{"name":"Messages","description":"Envoi transactionnel et statut."},{"name":"OTP","description":"Codes à usage unique par WhatsApp."}],"paths":{"/providers":{"get":{"tags":["Providers"],"operationId":"listProviders","summary":"Lister tous les providers","description":"Retourne la liste des numéros WhatsApp de votre organisation accessibles via l’API.","x-reachflow-id":"list-all-providers","x-reachflow-scope":"providers:read","security":[{"ApiKey":[]}],"responses":{"200":{"description":"Liste des providers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicProviderList"},"example":{"providers":[{"id":"a3f1b2c4-1234-4abc-9def-000011112222","name":"Support client","phoneNumber":"22997000000","status":"connected","warmupDay":14,"riskScore":12}]}}}}}}},"/providers/{id}":{"get":{"tags":["Providers"],"operationId":"getProvider","summary":"Afficher un provider","description":"Détail d’un provider avec statistiques journalières.","x-reachflow-id":"get-provider","x-reachflow-scope":"providers:read","security":[{"ApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Identifiant du provider."}],"responses":{"200":{"description":"Provider","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicProviderDetail"}}}}}}},"/messages/send":{"post":{"tags":["Messages"],"operationId":"sendMessage","summary":"Envoyer un message texte","description":"Met en file un message texte vers un destinataire. Le provider doit être connecté. L'envoi respecte par défaut la montée en puissance du numéro (voir « Protection anti-bannissement ») : en cas de plafond atteint, le message passe en `failed` avec `failureCode = warmup_daily_limit`. Consultez le statut via `GET /messages/{id}` ou le webhook `message.failed`.","x-reachflow-id":"send-message","x-reachflow-scope":"messages:send","security":[{"ApiKey":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Clé d’idempotence optionnelle."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSendMessage"},"example":{"providerId":"a3f1b2c4-1234-4abc-9def-000011112222","to":"22996123456","message":"Bonjour depuis ReachFlow !"}}}},"responses":{"202":{"description":"Message accepté en file","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSendAccepted"}}}}}}},"/messages/send-media":{"post":{"tags":["Messages"],"operationId":"sendMedia","summary":"Envoyer un média","description":"Envoie une image, vidéo, audio ou document via URL publique. `mediaUrl` doit être en **HTTPS**, accessible publiquement (les adresses internes/privées sont refusées), de taille ≤ 16 Mo et d'un type cohérent avec `mediaType` — sinon `400`. Soumis à la montée en puissance du numéro (voir « Protection anti-bannissement »).","x-reachflow-id":"send-media","x-reachflow-scope":"messages:send","security":[{"ApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSendMedia"},"example":{"providerId":"a3f1b2c4-1234-4abc-9def-000011112222","to":"22996123456","mediaUrl":"https://example.com/photo.jpg","mediaType":"image","caption":"Votre reçu"}}}},"responses":{"202":{"description":"Message accepté","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSendAccepted"}}}}}}},"/messages/send-bulk":{"post":{"tags":["Messages"],"operationId":"sendBulk","summary":"Envoi en lot","description":"Envoie le même modèle à plusieurs destinataires. Les refus partiels sont listés dans rejections. Chaque message reste soumis à la montée en puissance du numéro : au-delà du plafond quotidien, les messages excédentaires passent en `failed` (`warmup_daily_limit`) — voir « Protection anti-bannissement ».","x-reachflow-id":"send-bulk","x-reachflow-scope":"messages:send","security":[{"ApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSendBulk"}}}},"responses":{"200":{"description":"Résultat du lot","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSendBulkResponse"}}}}}}},"/messages/{id}":{"get":{"tags":["Messages"],"operationId":"getMessageStatus","summary":"Statut d’un message","description":"Consulte le cycle de vie d’un message API précédemment accepté.","x-reachflow-id":"get-message-status","x-reachflow-scope":"messages:read","security":[{"ApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Identifiant message (messageId)."}],"responses":{"200":{"description":"Statut","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicMessageStatus"}}}}}}},"/otp/send":{"post":{"tags":["OTP"],"operationId":"sendOtp","summary":"Envoyer un OTP","description":"Génère un code et l’envoie au numéro indiqué. Par défaut le message utilise le nom du tenant (`{{brand}}`) ; `brandName` permet de le surcharger (marque de votre app). L'envoi OTP respecte la montée en puissance du numéro ; pour de gros volumes OTP sur un numéro mature, désactivez-la par numéro depuis le dashboard (à vos risques — voir « Protection anti-bannissement »).","x-reachflow-id":"send-otp","x-reachflow-scope":"otp:send","security":[{"ApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicOtpSend"},"example":{"providerId":"a3f1b2c4-1234-4abc-9def-000011112222","phoneNumber":"22996123456","brandName":"Mon App","codeLength":6,"expiresIn":300}}}},"responses":{"200":{"description":"OTP créé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicOtpSendResponse"}}}}}}},"/otp/verify":{"post":{"tags":["OTP"],"operationId":"verifyOtp","summary":"Vérifier un OTP","description":"Valide le code saisi par l’utilisateur final.","x-reachflow-id":"verify-otp","x-reachflow-scope":"otp:verify","security":[{"ApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicOtpVerify"},"example":{"otpId":"c4d5e6f7-8901-42ab-cdef-333344445555","code":"482910"}}}},"responses":{"200":{"description":"Résultat de vérification","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicOtpVerifyResponse"},"example":{"valid":true}}}}}}}},"components":{"securitySchemes":{"ApiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Clé API sandbox (rfl_sandbox_) ou live (rfl_live_) — type imposé par l’instance."}},"schemas":{"PublicSendMessage":{"type":"object","required":["providerId","to","message"],"properties":{"providerId":{"type":"string","format":"uuid","description":"Provider expéditeur."},"to":{"type":"string","description":"Numéro destinataire (8–20 car.)."},"message":{"type":"string","description":"Corps du message (1–4096 car.)."},"variables":{"type":"object","additionalProperties":{"type":"string"},"description":"Variables pour modèle {{clé}}."},"scheduleAt":{"type":"string","format":"date-time","description":"Envoi différé optionnel."},"saveContact":{"type":"boolean","default":false,"description":"Si true, enregistre le destinataire dans la base contacts (find-or-create). Par défaut : envoi direct sans création ni quota contacts."}}},"PublicSendMedia":{"type":"object","required":["providerId","to","mediaUrl","mediaType"],"properties":{"providerId":{"type":"string","format":"uuid"},"to":{"type":"string"},"mediaUrl":{"type":"string","format":"uri","description":"URL HTTPS publique du média (hôtes internes/privés refusés ; ≤ 16 Mo ; type cohérent avec mediaType)."},"mediaType":{"enum":["image","document","audio","video"]},"caption":{"type":"string"},"saveContact":{"type":"boolean","default":false}}},"PublicSendBulk":{"type":"object","required":["providerId","messageTemplate","recipients"],"properties":{"providerId":{"type":"string","format":"uuid"},"messageTemplate":{"type":"string"},"recipients":{"type":"array","items":{"type":"object","required":["to"],"properties":{"to":{"type":"string"},"variables":{"type":"object","additionalProperties":{"type":"string"}}}}},"scheduleAt":{"type":"string","format":"date-time"},"saveContact":{"type":"boolean","default":false}}},"PublicSendAccepted":{"type":"object","properties":{"messageId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued"]},"queuedAt":{"type":"string","format":"date-time"}}},"PublicSendBulkResponse":{"type":"object","properties":{"bulkId":{"type":"string","format":"uuid"},"accepted":{"type":"integer"},"rejected":{"type":"integer"},"rejections":{"type":"array","items":{"type":"object"}},"messageIds":{"type":"array","items":{"type":"string","format":"uuid"}}}},"PublicMessageStatus":{"type":"object","properties":{"messageId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","processing","sent","delivered","failed","cancelled"]},"to":{"type":"string"},"providerId":{"type":"string","format":"uuid"},"queuedAt":{"type":"string","format":"date-time"},"sentAt":{"type":"string","format":"date-time","nullable":true},"deliveredAt":{"type":"string","format":"date-time","nullable":true},"failedAt":{"type":"string","format":"date-time","nullable":true},"failureCode":{"type":"string","nullable":true,"enum":["warmup_daily_limit","risk_circuit_open","provider_disconnected","provider_banned","send_not_allowed","instance_busy","delivery_timeout","delivery_failed","send_error","provider_not_found"],"description":"Cause d’échec : `warmup_daily_limit` (plafond montée en puissance — désactivable par numéro depuis le dashboard, à vos risques) · `risk_circuit_open` (numéro suspendu, score de risque critique — non désactivable) · `provider_disconnected` / `provider_banned` (numéro non connecté ou banni) · `send_not_allowed` (numéro non autorisé en environnement restreint) · `instance_busy` (instance saturée, réessayez) · `delivery_timeout` (non confirmé par WhatsApp dans le délai) · `delivery_failed` (échec de livraison signalé par WhatsApp)."},"failureReason":{"type":"string","nullable":true}}},"PublicProviderList":{"type":"object","properties":{"providers":{"type":"array","items":{"$ref":"#/components/schemas/PublicProviderSummary"}}}},"PublicProviderSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"phoneNumber":{"type":"string","nullable":true},"status":{"type":"string"},"warmupDay":{"type":"integer"},"riskScore":{"type":"integer"}}},"PublicProviderDetail":{"allOf":[{"$ref":"#/components/schemas/PublicProviderSummary"},{"type":"object","properties":{"dailyStats":{"type":"object"}}}]},"PublicOtpSend":{"type":"object","required":["providerId","phoneNumber"],"properties":{"providerId":{"type":"string","format":"uuid"},"phoneNumber":{"type":"string"},"codeLength":{"type":"integer","default":6},"expiresIn":{"type":"integer","default":300},"brandName":{"type":"string","maxLength":64,"description":"Nom affiché dans le message (marque de votre application). Si omis : nom de l’organisation ReachFlow du tenant."},"template":{"type":"string","description":"Modèle personnalisé. Placeholders : {{brand}}, {{code}}. Défaut : « Votre code de vérification {{brand}} : {{code}} »."},"saveContact":{"type":"boolean","default":false}}},"PublicOtpSendResponse":{"type":"object","properties":{"otpId":{"type":"string","format":"uuid"},"messageId":{"type":"string","format":"uuid","description":"Message WhatsApp en file — le code n’est pas renvoyé par l’API (voir statut sent/failed)."},"expiresAt":{"type":"string","format":"date-time"}}},"PublicOtpVerify":{"type":"object","required":["otpId","code"],"properties":{"otpId":{"type":"string","format":"uuid"},"code":{"type":"string"}}},"PublicOtpVerifyResponse":{"type":"object","properties":{"valid":{"type":"boolean"},"reason":{"type":"string"},"attemptsLeft":{"type":"integer"}}}}}}