openapi: 3.1.0
info:
  title: Opsender API
  version: 1.7.0
  description: |
    Les événements `message.ack` exposent le code fournisseur `ack`, un
    `deliveryStatus` normalisé (`failed`, `pending`, `sent`, `delivered`,
    `read`, `played`, `unknown`) et `deliveryTerminal`.
    API WhatsApp multi-instance d'Opsender pour envoyer des messages, suivre leur livraison,
    consulter les conversations et automatiser les événements.

    Toutes les opérations protégées utilisent `X-API-Key`. Conservez cette clé côté serveur.
    Pour les envois, fournissez `Idempotency-Key` afin d'éviter les doublons lors des reprises réseau.
    Consultez `/capabilities` avant d'activer une fonction WhatsApp avancée.
servers:
  - url: https://sender.opportail.com/v1
    description: Production
  - url: http://127.0.0.1:3000/v1
    description: Local
security:
  - apiKey: []
tags:
  - name: System
    description: Santé, contrat OpenAPI et capacités disponibles.
  - name: Instances
    description: Numéros WhatsApp, état du runtime et cycle de connexion.
  - name: Messages
    description: Envoi, suivi et actions sur les messages.
  - name: Conversations
    description: Cache conversationnel et synchronisation contrôlée de l'historique.
  - name: Chats
    description: Lecture directe des conversations disponibles chez le fournisseur.
  - name: Groups
    description: Groupes, participants, administrateurs et invitations.
  - name: Channels
    description: Chaines WhatsApp experimentales, publications et abonnements.
  - name: Contacts
    description: Contacts CRM isolés par compte client.
  - name: Webhooks
    description: Abonnements, secrets de signature et historique des livraisons.
paths:
  /health:
    get:
      tags: [System]
      summary: Vérifier la disponibilité HTTP
      security: []
      responses:
        "200":
          description: Service disponible
  /openapi.yaml:
    get:
      tags: [System]
      summary: Télécharger cette spécification
      security: []
      responses:
        "200": { description: Spécification OpenAPI }
  /capabilities:
    get:
      tags: [System]
      summary: Lister les capacités de l'API et leur disponibilité
      responses:
        "200":
          description: Matrice des capacités
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CapabilitiesResponse" }
  /instances:
    get:
      tags: [Instances]
      summary: Lister les numéros WhatsApp du client
      responses:
        "200":
          description: Instances accessibles
          content:
            application/json:
              schema:
                type: object
                required: [ok, count, instances]
                properties:
                  ok: { type: boolean, const: true }
                  count: { type: integer }
                  instances:
                    type: array
                    items: { $ref: "#/components/schemas/Instance" }
    post:
      tags: [Instances]
      summary: Creer une instance WhatsApp
      description: Cree un runtime dans la limite du plan. Le statut ou le QR peut ensuite etre consulte sur l'instance.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [label]
              properties:
                label: { type: string, minLength: 1, maxLength: 80 }
                phoneE164: { type: string, pattern: '^\\+[1-9][0-9]{7,14}$' }
                makeDefault: { type: boolean, default: false }
      responses:
        "201": { description: Instance creee }
        "400": { $ref: "#/components/responses/Problem" }
        "402": { $ref: "#/components/responses/Problem" }
        "409": { $ref: "#/components/responses/Problem" }
        "429": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
    get:
      tags: [Instances]
      summary: Obtenir le statut d'une instance
      responses:
        "200":
          description: Instance trouvée
          content:
            application/json:
              schema:
                type: object
                required: [ok, instance]
                properties:
                  ok: { type: boolean, const: true }
                  instance: { $ref: "#/components/schemas/Instance" }
        "404": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/capabilities:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
    get:
      tags: [Instances]
      summary: Lister les capacités d'une instance
      responses:
        "200":
          description: Capacités de l'instance
          content:
            application/json:
                schema: { $ref: "#/components/schemas/CapabilitiesResponse" }
        "404": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/events:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
    get:
      tags: [Instances]
      summary: Lister les evenements recents d'une instance
      parameters:
        - { name: limit, in: query, schema: { type: integer, default: 10, maximum: 50 } }
      responses:
        "200": { description: Evenements de connexion sans token, QR ni contenu de message }
        "404": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/connect:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
    post:
      tags: [Instances]
      summary: Connecter ou reveiller une instance
      description: Demarre le runtime sans supprimer la session WhatsApp existante. La connexion est asynchrone et peut necessiter un nouveau QR.
      responses:
        "202": { description: Demarrage accepte; consulter ensuite le statut de l'instance }
        "404": { $ref: "#/components/responses/Problem" }
        "409": { $ref: "#/components/responses/Problem" }
        "429": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/restart:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
    post:
      tags: [Instances]
      summary: Redemarrer le runtime d'une instance
      description: Ferme puis recree le client WhatsApp en conservant son authentification locale.
      responses:
        "200": { description: Redemarrage execute }
        "404": { $ref: "#/components/responses/Problem" }
        "409": { $ref: "#/components/responses/Problem" }
        "429": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/disconnect:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
    post:
      tags: [Instances]
      summary: Deconnecter temporairement une instance
      description: Met le runtime en veille sans supprimer l'authentification; l'action connect permet de le reveiller.
      responses:
        "200": { description: Instance mise en veille ou deja inactive }
        "404": { $ref: "#/components/responses/Problem" }
        "429": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/capabilities/probe:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
    post:
      tags: [Instances]
      summary: Sonder la compatibilite de lecture de l'historique WhatsApp
      description: Teste l'inventaire et au maximum un message sans retourner les identifiants ni le contenu. Une seule sonde est autorisee par periode de refroidissement.
      responses:
        "200": { description: Resultat compatible, incompatible ou non concluant }
        "404": { $ref: "#/components/responses/Problem" }
        "409": { $ref: "#/components/responses/Problem" }
        "429": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/messages:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
    get:
      tags: [Messages]
      summary: Lister les messages d'une instance
      parameters:
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 200 } }
        - { name: status, in: query, schema: { type: string, enum: [queued, sending, sent, failed] } }
      responses:
        "200": { description: Historique filtré par instance }
        "404": { $ref: "#/components/responses/Problem" }
    post:
      tags: [Messages]
      summary: Mettre en file un message texte ou média
      description: Accepte text, image, video, audio, document, voice, sticker, location et poll vers un contact ou un groupe.
      parameters:
        - { name: Idempotency-Key, in: header, required: false, description: Cle unique de 8 a 128 caracteres conservee pendant 24 heures, schema: { type: string, minLength: 8, maxLength: 128 } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SendMessage" }
            examples:
              text:
                value:
                  to: "+243990000000"
                  type: text
                  text: { body: Bonjour depuis Opsender }
              groupImage:
                value:
                  groupId: 120363000000000000@g.us
                  type: image
                  image:
                    filename: offre.png
                    mimeType: image/png
                    base64: iVBORw0KGgoAAA...
                    caption: Nouvelle offre
      responses:
        "202": { description: Message accepté dans la file }
        "400": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/Problem" }
        "409": { $ref: "#/components/responses/Problem" }
        "422": { $ref: "#/components/responses/Problem" }
  /webhooks:
    get:
      tags: [Webhooks]
      summary: Lister les webhooks du client
      description: Retourne les abonnements sans jamais exposer leurs secrets de signature.
      responses:
        "200": { description: Liste des webhooks du client }
        "401": { $ref: "#/components/responses/Problem" }
    post:
      tags: [Webhooks]
      summary: Creer un webhook
      description: Le secret de signature est retourne une seule fois lors de la creation.
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [url], properties: { url: { type: string, format: uri }, events: { type: array, items: { type: string } }, policy: { type: object } } }
      responses:
        "200": { description: Webhook cree avec son secret initial }
        "400": { $ref: "#/components/responses/Problem" }
        "429": { $ref: "#/components/responses/Problem" }
  /webhooks/{webhookId}:
    parameters:
      - { name: webhookId, in: path, required: true, schema: { type: string } }
    patch:
      tags: [Webhooks]
      summary: Modifier un webhook
      responses:
        "200": { description: Webhook modifie sans exposer son secret }
        "400": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/Problem" }
    delete:
      tags: [Webhooks]
      summary: Supprimer un webhook
      responses:
        "200": { description: Webhook supprime }
        "404": { $ref: "#/components/responses/Problem" }
  /webhooks/{webhookId}/active:
    parameters:
      - { name: webhookId, in: path, required: true, schema: { type: string } }
    post:
      tags: [Webhooks]
      summary: Activer ou desactiver un webhook
      responses:
        "200": { description: Etat du webhook modifie }
        "404": { $ref: "#/components/responses/Problem" }
  /webhooks/{webhookId}/rotate-secret:
    parameters:
      - { name: webhookId, in: path, required: true, schema: { type: string } }
    post:
      tags: [Webhooks]
      summary: Remplacer le secret de signature
      description: Le nouveau secret est retourne une seule fois et invalide immediatement l'ancien.
      responses:
        "200": { description: Secret remplace }
        "404": { $ref: "#/components/responses/Problem" }
  /webhooks/{webhookId}/test:
    parameters:
      - { name: webhookId, in: path, required: true, schema: { type: string } }
    post:
      tags: [Webhooks]
      summary: Mettre en file une livraison webhook de test
      responses:
        "202": { description: Livraison de test mise en file }
        "404": { $ref: "#/components/responses/Problem" }
  /webhook-deliveries:
    get:
      tags: [Webhooks]
      summary: Lister l'historique des livraisons webhook
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [queued, sending, delivered, failed] } }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 200 } }
      responses:
        "200": { description: Historique filtre sans secret ni contenu sensible }
        "401": { $ref: "#/components/responses/Problem" }
  /webhook-deliveries/{deliveryId}/replay:
    parameters:
      - { name: deliveryId, in: path, required: true, schema: { type: string } }
    post:
      tags: [Webhooks]
      summary: Rejouer une livraison webhook
      description: Exige confirmDeliveryId identique au parametre du chemin et conserve la livraison originale pour l'audit.
      responses:
        "202": { description: Nouvelle livraison mise en file }
        "400": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/conversations/sync:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
    post:
      tags: [Conversations]
      summary: Demarrer une synchronisation asynchrone de l'historique WhatsApp
      description: Accepte une tache persistante qui importe au maximum 20 conversations et 100 messages par conversation.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                chatIds: { type: array, maxItems: 20, items: { type: string } }
                chatLimit: { type: integer, default: 10, maximum: 20 }
                messagesPerChat: { type: integer, default: 50, maximum: 100 }
      responses:
        "202": { description: Synchronisation acceptee; retourne un job et son statusUrl }
        "400": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/Problem" }
        "409": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
    get:
      tags: [Conversations]
      summary: Lister les synchronisations recentes de cette instance
      parameters:
        - { name: limit, in: query, schema: { type: integer, default: 20, maximum: 100 } }
      responses:
        "200": { description: Liste des synchronisations }
        "404": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/conversations:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
    get:
      tags: [Conversations]
      summary: Lister les conversations conservees dans le cache Opsender
      parameters:
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 100 } }
        - { name: cursor, in: query, schema: { type: string } }
        - { name: q, in: query, description: Recherche dans le nom, le numéro et le contenu des messages, schema: { type: string, maxLength: 200 } }
        - { name: unreadOnly, in: query, description: Limiter la réponse aux conversations ayant des messages non lus, schema: { type: boolean, default: false } }
      responses:
        "200": { description: Conversations agregees avec derniere activite et compteurs }
        "400": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/conversations/{chatId}/messages:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: chatId, in: path, required: true, schema: { type: string } }
    get:
      tags: [Conversations]
      summary: Lire les messages conserves d'une conversation
      parameters:
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 100 } }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        "200": { description: Page de messages provenant du cache Opsender }
        "400": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/conversations/{chatId}/read:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: chatId, in: path, required: true, schema: { type: string } }
    post:
      tags: [Conversations]
      summary: Remettre à zéro le compteur local de messages non lus
      description: Marque la conversation comme lue dans le cache Opsender. Cette opération ne dépend pas de la disponibilité de l'historique distant WhatsApp.
      responses:
        "200": { description: État de lecture local enregistré }
        "400": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/conversations/sync/{jobId}:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: jobId, in: path, required: true, schema: { type: string } }
    get:
      tags: [Conversations]
      summary: Consulter la progression d'une synchronisation
      responses:
        "200": { description: Etat, progression et resultat de la synchronisation }
        "404": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/messages/{messageId}:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: messageId, in: path, required: true, schema: { type: integer } }
    get:
      tags: [Messages]
      summary: Obtenir un message d'une instance
      responses:
        "200": { description: Message trouvé }
        "404": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/messages/search:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
    get:
      tags: [Messages]
      summary: Rechercher dans le cache conversationnel entrant
      parameters:
        - { name: q, in: query, schema: { type: string, minLength: 2 } }
        - { name: chatId, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 100 } }
        - { name: cursor, in: query, description: Curseur opaque retourne par la page precedente, schema: { type: string } }
      responses:
        "200": { description: Messages trouvés }
        "400": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/messages/{messageId}/react:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: messageId, in: path, required: true, schema: { type: string } }
    post:
      tags: [Messages]
      summary: Ajouter ou retirer une réaction
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [emoji], properties: { emoji: { type: string } } }
      responses: { "200": { description: Réaction appliquée }, "404": { $ref: "#/components/responses/Problem" } }
  /instances/{instanceId}/messages/{messageId}/media:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: messageId, in: path, required: true, schema: { type: string } }
    get:
      tags: [Messages]
      summary: Télécharger le média d'un message en Base64
      responses: { "200": { description: Média WhatsApp }, "404": { $ref: "#/components/responses/Problem" }, "410": { $ref: "#/components/responses/Problem" } }
  /instances/{instanceId}/messages/{messageId}/delivery:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: messageId, in: path, required: true, schema: { type: string } }
    get:
      tags: [Messages]
      summary: Obtenir les informations de livraison et lecture
      responses: { "200": { description: Informations normalisées }, "404": { $ref: "#/components/responses/Problem" } }
  /instances/{instanceId}/messages/{messageId}/reply:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: messageId, in: path, required: true, schema: { type: string } }
    post:
      tags: [Messages]
      summary: Répondre à un message
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/TextAction" } } } }
      responses: { "200": { description: Réponse envoyée }, "404": { $ref: "#/components/responses/Problem" } }
  /instances/{instanceId}/messages/{messageId}/forward:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: messageId, in: path, required: true, schema: { type: string } }
    post:
      tags: [Messages]
      summary: Transférer un message
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [to], properties: { to: { type: string } } } } } }
      responses: { "200": { description: Message transféré }, "404": { $ref: "#/components/responses/Problem" } }
  /instances/{instanceId}/messages/{messageId}/edit:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: messageId, in: path, required: true, schema: { type: string } }
    post:
      tags: [Messages]
      summary: Modifier un message envoyé
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/TextAction" } } } }
      responses: { "200": { description: Message modifié }, "409": { $ref: "#/components/responses/Problem" } }
  /instances/{instanceId}/messages/{messageId}/delete:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: messageId, in: path, required: true, schema: { type: string } }
    post:
      tags: [Messages]
      summary: Supprimer un message
      requestBody: { content: { application/json: { schema: { type: object, properties: { everyone: { type: boolean }, clearMedia: { type: boolean, default: true } } } } } }
      responses: { "200": { description: Message supprimé }, "404": { $ref: "#/components/responses/Problem" } }
  /instances/{instanceId}/chats/{chatId}/read:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: chatId, in: path, required: true, schema: { type: string } }
    post:
      tags: [Chats]
      summary: Marquer une conversation comme lue
      responses: { "200": { description: Conversation marquée comme lue }, "404": { $ref: "#/components/responses/Problem" } }
  /instances/{instanceId}/channels:
    parameters: [{ $ref: "#/components/parameters/InstanceId" }]
    get:
      tags: [Channels]
      summary: Lister les chaines visibles par l'instance
      responses: { "200": { description: Chaines WhatsApp normalisees }, "409": { $ref: "#/components/responses/Problem" }, "503": { $ref: "#/components/responses/Problem" } }
    post:
      tags: [Channels]
      summary: Creer une chaine WhatsApp
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [title], properties: { title: { type: string, maxLength: 100 }, description: { type: string, maxLength: 2048 }, picture: { type: object } } } } } }
      responses: { "201": { description: Chaine creee }, "409": { $ref: "#/components/responses/Problem" } }
  /instances/{instanceId}/channels/{channelId}:
    parameters: [{ $ref: "#/components/parameters/InstanceId" }, { name: channelId, in: path, required: true, schema: { type: string } }]
    get:
      tags: [Channels]
      summary: Obtenir une chaine
      responses: { "200": { description: Chaine normalisee }, "404": { $ref: "#/components/responses/Problem" } }
    patch:
      tags: [Channels]
      summary: Modifier une chaine
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { title: { type: string }, description: { type: string }, reactions: { type: integer, enum: [0, 1, 2] } } } } } }
      responses: { "200": { description: Chaine modifiee }, "403": { $ref: "#/components/responses/Problem" } }
    delete:
      tags: [Channels]
      summary: Supprimer definitivement une chaine
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [confirmChannelId], properties: { confirmChannelId: { type: string } } } } } }
      responses: { "200": { description: Chaine supprimee }, "400": { $ref: "#/components/responses/Problem" } }
  /instances/{instanceId}/channels/{channelId}/subscribe:
    parameters: [{ $ref: "#/components/parameters/InstanceId" }, { name: channelId, in: path, required: true, schema: { type: string } }]
    post:
      tags: [Channels]
      summary: S'abonner a une chaine
      responses: { "200": { description: Abonnement confirme }, "409": { $ref: "#/components/responses/Problem" } }
  /instances/{instanceId}/channels/{channelId}/unsubscribe:
    parameters: [{ $ref: "#/components/parameters/InstanceId" }, { name: channelId, in: path, required: true, schema: { type: string } }]
    post:
      tags: [Channels]
      summary: Se desabonner d'une chaine
      requestBody: { content: { application/json: { schema: { type: object, properties: { deleteLocalModels: { type: boolean } } } } } }
      responses: { "200": { description: Desabonnement confirme }, "409": { $ref: "#/components/responses/Problem" } }
  /instances/{instanceId}/channels/{channelId}/messages:
    parameters: [{ $ref: "#/components/parameters/InstanceId" }, { name: channelId, in: path, required: true, schema: { type: string } }]
    post:
      tags: [Channels]
      summary: Publier un message texte ou media dans une chaine administree
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { text: { type: string }, attachment: { type: object } } } } } }
      responses: { "201": { description: Publication acceptee par WhatsApp }, "403": { $ref: "#/components/responses/Problem" } }
  /instances/{instanceId}/groups:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
    get:
      tags: [Groups]
      summary: Lister les groupes visibles par l'instance
      responses:
        "200": { description: Groupes WhatsApp }
        "404": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/groups/{groupId}:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: groupId, in: path, required: true, schema: { type: string } }
    get:
      tags: [Groups]
      summary: Obtenir les informations d'un groupe
      responses:
        "200": { description: Groupe WhatsApp }
        "404": { $ref: "#/components/responses/Problem" }
    patch:
      tags: [Groups]
      summary: Modifier le nom, la description ou les permissions du groupe
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/GroupUpdate" }
      responses:
        "200": { description: Groupe modifié }
        "400": { $ref: "#/components/responses/Problem" }
        "403": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/groups/{groupId}/participants:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: groupId, in: path, required: true, schema: { type: string } }
    get:
      tags: [Groups]
      summary: Lister les participants et leurs rôles
      responses:
        "200": { description: Participants normalisés }
    post:
      tags: [Groups]
      summary: Ajouter des participants
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ParticipantsInput" }
      responses:
        "200": { description: Résultat par participant }
        "403": { $ref: "#/components/responses/Problem" }
    delete:
      tags: [Groups]
      summary: Retirer des participants
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ParticipantsInput" }
      responses:
        "200": { description: Participants retirés }
        "403": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/groups/{groupId}/admins:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: groupId, in: path, required: true, schema: { type: string } }
    post:
      tags: [Groups]
      summary: Promouvoir des administrateurs
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ParticipantsInput" }
      responses:
        "200": { description: Participants promus }
    delete:
      tags: [Groups]
      summary: Rétrograder des administrateurs
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ParticipantsInput" }
      responses:
        "200": { description: Administrateurs rétrogradés }
  /instances/{instanceId}/groups/{groupId}/invite:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: groupId, in: path, required: true, schema: { type: string } }
    get:
      tags: [Groups]
      summary: Obtenir le lien d'invitation
      responses:
        "200": { description: Code et URL d'invitation }
  /instances/{instanceId}/groups/{groupId}/invite/reset:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: groupId, in: path, required: true, schema: { type: string } }
    post:
      tags: [Groups]
      summary: Révoquer et recréer le lien d'invitation
      responses:
        "200": { description: Nouveau code et URL d'invitation }
  /instances/{instanceId}/groups/{groupId}/leave:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: groupId, in: path, required: true, schema: { type: string } }
    post:
      tags: [Groups]
      summary: Quitter définitivement le groupe
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [confirmGroupId]
              properties:
                confirmGroupId: { type: string }
      responses:
        "200": { description: Groupe quitté }
        "400": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/chats:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
    get:
      tags: [Chats]
      summary: Lister les conversations WhatsApp récentes
      parameters:
        - { name: limit, in: query, schema: { type: integer, default: 50, minimum: 1, maximum: 100 } }
      responses:
        "200": { description: Conversations normalisées }
        "404": { $ref: "#/components/responses/Problem" }
  /instances/{instanceId}/chats/{chatId}/messages:
    parameters:
      - $ref: "#/components/parameters/InstanceId"
      - { name: chatId, in: path, required: true, schema: { type: string } }
    get:
      tags: [Chats]
      summary: Lire les messages récents d'une conversation
      parameters:
        - { name: limit, in: query, schema: { type: integer, default: 50, minimum: 1, maximum: 100 } }
      responses:
        "200": { description: Messages WhatsApp normalisés, sans objet interne du fournisseur }
        "404": { $ref: "#/components/responses/Problem" }
  /contacts:
    get:
      tags: [Contacts]
      summary: Lister les contacts CRM du client
      parameters:
        - { name: q, in: query, schema: { type: string } }
        - { name: optIn, in: query, schema: { type: boolean } }
        - { name: limit, in: query, schema: { type: integer, maximum: 500 } }
      responses:
        "200": { description: Contacts et statistiques }
    post:
      tags: [Contacts]
      summary: Créer ou fusionner un contact selon son numéro
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContactInput" }
      responses:
        "201": { description: Contact créé }
        "200": { description: Contact existant fusionné }
        "400": { $ref: "#/components/responses/Problem" }
  /contacts/{contactId}:
    parameters:
      - { name: contactId, in: path, required: true, schema: { type: string } }
    get:
      tags: [Contacts]
      summary: Obtenir un contact
      responses:
        "200": { description: Contact trouvé }
        "404": { $ref: "#/components/responses/Problem" }
    patch:
      tags: [Contacts]
      summary: Modifier un contact
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContactInput" }
      responses:
        "200": { description: Contact modifié }
        "404": { $ref: "#/components/responses/Problem" }
    delete:
      tags: [Contacts]
      summary: Supprimer un contact
      responses:
        "200": { description: Contact supprimé }
        "404": { $ref: "#/components/responses/Problem" }
components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
  parameters:
    InstanceId:
      name: instanceId
      in: path
      required: true
      description: Label stable de l'instance attribuée au client.
      schema: { type: string, minLength: 1 }
  responses:
    Problem:
      description: Erreur API structurée
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ProblemResponse" }
  schemas:
    ParticipantsInput:
      type: object
      required: [participants]
      properties:
        participants:
          type: array
          minItems: 1
          maxItems: 50
          items: { type: string, description: Numéro E.164 ou identifiant WhatsApp. }
        autoSendInvite: { type: boolean, default: true }
        comment: { type: string, maxLength: 500 }
    GroupUpdate:
      type: object
      properties:
        subject: { type: string, minLength: 1, maxLength: 100 }
        description: { type: string, maxLength: 2048 }
        messagesAdminsOnly: { type: boolean }
        infoAdminsOnly: { type: boolean }
    ContactInput:
      type: object
      properties:
        name: { type: string }
        phoneE164: { type: string, pattern: '^\\+[1-9][0-9]{7,14}$' }
        email: { type: string, format: email }
        tags: { type: array, items: { type: string } }
        optIn: { type: boolean }
        notes: { type: string }
    Instance:
      type: object
      required: [id, label, isDefault, runtime, capabilitiesUrl]
      properties:
        id: { type: string }
        label: { type: string }
        phoneE164: { type: [string, "null"] }
        isDefault: { type: boolean }
        createdAt: { type: string }
        updatedAt: { type: string }
        runtime: { type: object, additionalProperties: true }
        capabilitiesUrl: { type: string }
    CapabilitiesResponse:
      type: object
      required: [ok, provider, apiVersion, capabilities]
      properties:
        ok: { type: boolean, const: true }
        instanceId: { type: string }
        provider: { type: string }
        apiVersion: { type: string }
        capabilities:
          type: object
          additionalProperties:
            type: string
            enum: [supported, planned, unavailable]
    MediaContent:
      type: object
      required: [base64]
      properties:
        base64: { type: string, contentEncoding: base64 }
        mimeType: { type: string }
        filename: { type: string }
        caption: { type: string }
    SendMessage:
      type: object
      required: [type]
      properties:
        type: { type: string, enum: [text, image, video, audio, document, voice, sticker, location, contact, poll, buttons] }
        to: { type: string }
        groupId: { type: string }
        text:
          oneOf:
            - type: string
            - type: object
              required: [body]
              properties:
                body: { type: string }
        image: { $ref: "#/components/schemas/MediaContent" }
        video: { $ref: "#/components/schemas/MediaContent" }
        audio: { $ref: "#/components/schemas/MediaContent" }
        document: { $ref: "#/components/schemas/MediaContent" }
        voice: { $ref: "#/components/schemas/MediaContent" }
        sticker: { $ref: "#/components/schemas/MediaContent" }
        location:
          type: object
          required: [latitude, longitude]
          properties:
            latitude: { type: number, minimum: -90, maximum: 90 }
            longitude: { type: number, minimum: -180, maximum: 180 }
            name: { type: string }
            address: { type: string }
            url: { type: string }
        poll:
          type: object
          required: [question, options]
          properties:
            question: { type: string }
            options: { type: array, minItems: 2, maxItems: 12, items: { type: string } }
            allowMultipleAnswers: { type: boolean, default: false }
        buttons:
          type: object
          description: Interaction experimentale. WhatsApp limite le rendu a trois boutons. Sans accuse serveur dans le delai borne, fallbackToText permet un repli texte numerote. Pendant 24 heures, la premiere reponse correspondant au numero ou au libelle d'un choix produit le webhook message.button_response.
          required: [body, items]
          properties:
            title: { type: string, maxLength: 60 }
            body: { type: string, maxLength: 1024 }
            footer: { type: string, maxLength: 60 }
            fallbackToText: { type: boolean, default: true }
            items:
              type: array
              minItems: 1
              maxItems: 3
              items:
                type: object
                required: [id, body]
                properties:
                  id: { type: string, pattern: "^[A-Za-z0-9_.:-]{1,64}$" }
                  body: { type: string, maxLength: 20 }
        contacts: { type: array, maxItems: 20, items: { type: string } }
        mentions: { type: array, maxItems: 50, items: { type: string } }
    TextAction:
      type: object
      required: [text]
      properties:
        text: { type: string }
    ProblemResponse:
      type: object
      required: [ok, error]
      properties:
        ok: { type: boolean, const: false }
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
            details: { type: object, additionalProperties: true }
