openapi: 3.1.0
info:
  title: API Rangee
  version: 1.0.0
  summary: Lisez, cherchez et modifiez vos lignes, déclenchez vos automatisations et recevez les changements de vos bases.
  description: |
    L'API Rangee donne accès par programme à ce que vous faites dans l'application : les lignes de vos
    tables, le schéma de vos bases, vos automatisations et les changements en temps réel.

    ## Authentification

    Créez un jeton dans **Réglages › API**. Il n'est montré qu'une fois : copiez-le et gardez-le comme un mot
    de passe. Envoyez-le dans l'en-tête `Authorization` de chaque appel :

    ```
    Authorization: Bearer pat01JD….3f9a…
    ```

    Un jeton agit avec votre rôle : il ne voit et ne modifie que ce que vous voyez et modifiez dans
    l'application, limité aux portées et aux bases choisies à sa création.

    | Portée | Donne |
    | --- | --- |
    | `rows:read` | Lire et chercher les lignes, leur historique et leurs commentaires |
    | `rows:write` | Créer, modifier, supprimer des lignes ; commenter ; téléverser des fichiers |
    | `schema:read` | Lire les bases, tables, champs et vues |
    | `schema:write` | Créer et modifier tables, champs et vues |
    | `automations:read` | Lister les automatisations, leurs variables et leurs exécutions |
    | `automations:run` | Déclencher une automatisation |
    | `webhooks:manage` | Gérer les webhooks |

    ## Conventions

    - Les valeurs d'une ligne sont clées par identifiant de champ (`fld…`) : renommer un champ ne casse
      pas votre intégration. Ajoutez `fieldKey=name` pour les recevoir par nom. En écriture, l'identifiant
      et le nom sont acceptés.
    - Les listes ont la forme `{ "data": [...], "nextCursor": "…" }`. Passez `nextCursor` en `cursor`
      pour la page suivante ; `null` signifie la dernière page.
    - Les écritures se font par lots de 1 à 100 lignes. Un lot passe en entier ou pas du tout.
    - Ajoutez `Idempotency-Key` à une écriture pour pouvoir la rejouer sans risque pendant 24 heures.
    - Les dates sont en ISO 8601 UTC ; les dates sans heure en `YYYY-MM-DD`.
    - Chaque réponse porte un en-tête `Request-Id` à citer au support.

    ## Limites

    50 requêtes par seconde par base et 200 par jeton (en-têtes `RateLimit-*`, puis 429 avec
    `Retry-After`). 1 500 appels par mois en Free, sans plafond en Pro et Business.
  contact:
    name: Support Rangee
    url: https://rangee.io/contact
servers:
  - url: https://api.rangee.io/v1
    description: Production
security:
  - jeton: []
tags:
  - name: Accès
    description: Le jeton utilisé et son quota.
  - name: Lignes
    description: |
      Lire, chercher, créer, modifier, upserter et supprimer les lignes d'une table.

      Format des valeurs par type de champ :

      | Type | Lecture | Écriture |
      | --- | --- | --- |
      | Texte, e-mail, URL, téléphone | `string` | `string` |
      | Nombre, devise | `number` | `number` |
      | Pourcentage | `number` (`0.25` pour 25 %) | idem |
      | Note | `integer` | idem |
      | Durée | `integer` en secondes | idem |
      | Case à cocher | `true`, absente si décochée | `boolean` |
      | Date | `"YYYY-MM-DD"` | idem |
      | Date et heure | ISO 8601 UTC | ISO 8601 avec fuseau |
      | Sélection unique | `{ "id", "name" }` | identifiant ou nom de l'option |
      | Sélection multiple | liste de `{ "id", "name" }` | liste d'identifiants ou de noms |
      | Collaborateur(s) | `{ "id", "email", "name" }` ou liste | identifiant ou e-mail d'un membre |
      | Pièces jointes | liste d'objets `Attachment` | liste de `{ "id" }` de fichiers téléversés dans la base |
      | Lien vers des lignes | liste d'identifiants `row…` | idem |
      | Code-barres | `{ "text", "type" }` | idem ou `string` |
      | Formule, lookup, rollup, compteur, agrégation, numéro automatique, dates et auteurs de création et de modification, bouton, texte IA | type du résultat | lecture seule |

      Avec `"coerce": true`, les textes sont convertis en nombres, dates et cases cochées, et les options
      de sélection inconnues sont créées.
  - name: Schéma
    description: Bases, tables, champs et vues.
  - name: Automatisations
    description: |
      Déclenchez une automatisation dont le déclencheur est « Appel API » (avec ses variables) ou
      « Bouton » (sur une ligne), puis suivez son exécution.
  - name: Webhooks
    description: |
      Recevez les changements d'une base sur votre adresse, signés et rejoués en cas d'échec.

      Chaque livraison porte l'en-tête `Rangee-Signature: t=<horodatage>,v1=<signature>`. Vérifiez-la en
      calculant le HMAC-SHA256, en hexadécimal, de `<horodatage>.<corps brut>` avec le secret du webhook,
      et refusez un horodatage de plus de 5 minutes.
  - name: Pièces jointes
    description: Téléversez un fichier, puis placez-le dans une valeur de pièces jointes.
  - name: Commentaires
    description: Les commentaires d'une ligne, avec mentions.

paths:
  /me:
    get:
      tags: [Accès]
      operationId: getMe
      summary: Le jeton utilisé
      description: La personne, le jeton, ses portées, son périmètre et le quota d'appels du mois.
      responses:
        '200':
          description: Le jeton.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Me' }
        '401': { $ref: '#/components/responses/Unauthenticated' }

  /bases:
    get:
      tags: [Schéma]
      operationId: listBases
      summary: Lister les bases
      description: 'Les bases accessibles au jeton. Portée : `schema:read`.'
      x-scope: schema:read
      parameters:
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/cursor'
      responses:
        '200':
          description: Les bases.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/BaseSummary' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /bases/{baseId}:
    parameters:
      - $ref: '#/components/parameters/baseId'
    get:
      tags: [Schéma]
      operationId: getBase
      summary: Lire une base
      description: 'La base, ses tables, leurs champs et leurs vues. Portée : `schema:read`.'
      x-scope: schema:read
      responses:
        '200':
          description: La base.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Base' }
        '404': { $ref: '#/components/responses/NotFound' }

  /bases/{baseId}/tables:
    parameters:
      - $ref: '#/components/parameters/baseId'
    post:
      tags: [Schéma]
      operationId: createTable
      summary: Créer une table
      description: 'Crée une table avec ses champs ; le premier champ est le champ principal. Portée : `schema:write`.'
      x-scope: schema:write
      parameters:
        - $ref: '#/components/parameters/idempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, fields]
              properties:
                name: { type: string, maxLength: 100 }
                fields:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items: { $ref: '#/components/schemas/FieldInput' }
      responses:
        '201':
          description: La table créée.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Table' }
        '422': { $ref: '#/components/responses/Invalid' }

  /tables/{tableId}:
    parameters:
      - $ref: '#/components/parameters/tableId'
    patch:
      tags: [Schéma]
      operationId: updateTable
      summary: Modifier une table
      description: 'Renomme la table ou change son champ principal. Portée : `schema:write`.'
      x-scope: schema:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, maxLength: 100 }
                primaryFieldId: { type: string }
      responses:
        '200':
          description: La table.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Table' }
        '422': { $ref: '#/components/responses/Invalid' }
    delete:
      tags: [Schéma]
      operationId: deleteTable
      summary: Mettre une table à la corbeille
      description: |
        La table reste restaurable 30 jours depuis la corbeille de la base. La dernière table d'une base
        ne se supprime pas (409 `LAST_TABLE`). Portée : `schema:write`.
      x-scope: schema:write
      responses:
        '200': { $ref: '#/components/responses/Deleted' }
        '409': { $ref: '#/components/responses/Conflict' }

  /tables/{tableId}/fields:
    parameters:
      - $ref: '#/components/parameters/tableId'
    post:
      tags: [Schéma]
      operationId: createField
      summary: Créer un champ
      description: 'Portée : `schema:write`.'
      x-scope: schema:write
      parameters:
        - $ref: '#/components/parameters/idempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FieldInput' }
      responses:
        '201':
          description: Le champ créé.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Field' }
        '422': { $ref: '#/components/responses/Invalid' }

  /fields/{fieldId}:
    parameters:
      - $ref: '#/components/parameters/fieldId'
    patch:
      tags: [Schéma]
      operationId: updateField
      summary: Modifier un champ
      description: |
        Renomme le champ, change sa description, ses options ou son type. Changer le type convertit les
        valeurs comme dans l'application. Portée : `schema:write`.
      x-scope: schema:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, maxLength: 100 }
                description: { type: string, maxLength: 20000 }
                type: { $ref: '#/components/schemas/FieldType' }
                options: { $ref: '#/components/schemas/FieldOptions' }
      responses:
        '200':
          description: Le champ.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Field' }
        '422': { $ref: '#/components/responses/Invalid' }
    delete:
      tags: [Schéma]
      operationId: deleteField
      summary: Mettre un champ à la corbeille
      description: |
        Un champ lu par d'autres champs (formule, lookup, rollup, compteur) répond 409 `FIELD_IN_USE` avec
        ces champs dans `details.usedBy`, sauf avec `force=true` (refusé si la base protège ses champs
        utilisés). Le champ principal ne se supprime pas (409 `PRIMARY_FIELD`). Portée : `schema:write`.
      x-scope: schema:write
      parameters:
        - $ref: '#/components/parameters/force'
      responses:
        '200': { $ref: '#/components/responses/Deleted' }
        '409': { $ref: '#/components/responses/Conflict' }

  /views/{viewId}:
    parameters:
      - name: viewId
        in: path
        required: true
        schema: { type: string, examples: [viw01JD8Q2…] }
    get:
      tags: [Schéma]
      operationId: getView
      summary: Lire une vue
      description: 'La vue, avec ses filtres, ses tris et ses champs visibles. Portée : `schema:read`.'
      x-scope: schema:read
      responses:
        '200':
          description: La vue.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/View' }
        '404': { $ref: '#/components/responses/NotFound' }

  /tables/{tableId}/rows:
    parameters:
      - $ref: '#/components/parameters/tableId'
    get:
      tags: [Lignes]
      operationId: listRows
      summary: Lister les lignes
      description: |
        Les lignes d'une table, dans l'ordre de la vue si `viewId` est donné, sinon par date de création.
        Pour filtrer, utilisez [Chercher des lignes](#tag/Lignes/operation/queryRows). Portée : `rows:read`.
      x-scope: rows:read
      parameters:
        - name: viewId
          in: query
          description: Applique les filtres et tris de cette vue.
          schema: { type: string }
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/fieldKey'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/cursor'
      responses:
        '200': { $ref: '#/components/responses/RowPage' }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      tags: [Lignes]
      operationId: createRows
      summary: Créer des lignes
      description: 'Crée 1 à 100 lignes. Portée : `rows:write`.'
      x-scope: rows:write
      parameters:
        - $ref: '#/components/parameters/idempotencyKey'
        - $ref: '#/components/parameters/fieldKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rows]
              properties:
                rows:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    type: object
                    required: [values]
                    properties:
                      values: { $ref: '#/components/schemas/Values' }
                coerce: { $ref: '#/components/schemas/Coerce' }
            example:
              rows:
                - values: { Nom: Atelier Martin, Statut: Signé, Montant: 1250 }
      responses:
        '201':
          description: Les lignes créées, dans l'ordre envoyé.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RowList' }
        '422': { $ref: '#/components/responses/Invalid' }
    patch:
      tags: [Lignes]
      operationId: updateRows
      summary: Modifier des lignes
      description: 'Modifie 1 à 100 lignes ; seuls les champs envoyés changent. Portée : `rows:write`.'
      x-scope: rows:write
      parameters:
        - $ref: '#/components/parameters/idempotencyKey'
        - $ref: '#/components/parameters/fieldKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rows]
              properties:
                rows:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    type: object
                    required: [id, values]
                    properties:
                      id: { type: string }
                      values: { $ref: '#/components/schemas/Values' }
                coerce: { $ref: '#/components/schemas/Coerce' }
      responses:
        '200':
          description: Les lignes modifiées.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RowList' }
        '422': { $ref: '#/components/responses/Invalid' }
    delete:
      tags: [Lignes]
      operationId: deleteRows
      summary: Supprimer des lignes
      description: 'Met 1 à 100 lignes à la corbeille de la base, restaurables 30 jours. Portée : `rows:write`.'
      x-scope: rows:write
      parameters:
        - name: ids
          in: query
          required: true
          description: Identifiants séparés par des virgules.
          schema: { type: string, examples: ['row01JD…,row01JE…'] }
        - $ref: '#/components/parameters/idempotencyKey'
      responses:
        '200':
          description: Les lignes supprimées.
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: array
                    items: { type: string }

  /tables/{tableId}/rows/query:
    parameters:
      - $ref: '#/components/parameters/tableId'
    post:
      tags: [Lignes]
      operationId: queryRows
      summary: Chercher des lignes
      description: |
        Filtre, trie et pagine les lignes. `filter`, `search`, `viewId` et `formula` se combinent par « et ».
        Portée : `rows:read`.
      x-scope: rows:read
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Query' }
            example:
              filter:
                and:
                  - { field: Statut, operator: isAnyOf, value: [Signé, En cours] }
                  - { field: Montant, operator: '>=', value: 1000 }
              sort:
                - { field: Montant, direction: desc }
              fields: [Nom, Montant]
              limit: 50
      responses:
        '200': { $ref: '#/components/responses/RowPage' }
        '422': { $ref: '#/components/responses/Invalid' }

  /tables/{tableId}/rows/upsert:
    parameters:
      - $ref: '#/components/parameters/tableId'
    post:
      tags: [Lignes]
      operationId: upsertRows
      summary: Créer ou modifier des lignes
      description: |
        Pour chaque ligne envoyée, cherche une ligne existante dont les champs `matchOn` ont les mêmes
        valeurs : la modifie si elle existe, en crée une sinon. Plusieurs correspondances répondent 409
        `UPSERT_AMBIGUOUS`. Portée : `rows:write`.
      x-scope: rows:write
      parameters:
        - $ref: '#/components/parameters/idempotencyKey'
        - $ref: '#/components/parameters/fieldKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [matchOn, rows]
              properties:
                matchOn:
                  type: array
                  minItems: 1
                  maxItems: 3
                  description: Champs texte, e-mail, nombre, URL, téléphone ou numéro automatique.
                  items: { type: string }
                rows:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    type: object
                    required: [values]
                    properties:
                      values: { $ref: '#/components/schemas/Values' }
                coerce: { $ref: '#/components/schemas/Coerce' }
            example:
              matchOn: [E-mail]
              rows:
                - values: { E-mail: claire@atelier-martin.fr, Nom: Claire Martin }
      responses:
        '200':
          description: Les lignes, et lesquelles ont été créées ou modifiées.
          content:
            application/json:
              schema:
                type: object
                properties:
                  rows:
                    type: array
                    items: { $ref: '#/components/schemas/Row' }
                  created:
                    type: array
                    items: { type: string }
                  updated:
                    type: array
                    items: { type: string }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/Invalid' }

  /tables/{tableId}/rows/{rowId}:
    parameters:
      - $ref: '#/components/parameters/tableId'
      - $ref: '#/components/parameters/rowId'
    get:
      tags: [Lignes]
      operationId: getRow
      summary: Lire une ligne
      description: 'Portée : `rows:read`.'
      x-scope: rows:read
      parameters:
        - $ref: '#/components/parameters/fieldKey'
      responses:
        '200':
          description: La ligne.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Row' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Lignes]
      operationId: updateRow
      summary: Modifier une ligne
      description: 'Seuls les champs envoyés changent. Portée : `rows:write`.'
      x-scope: rows:write
      parameters:
        - $ref: '#/components/parameters/idempotencyKey'
        - $ref: '#/components/parameters/fieldKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RowInput' }
      responses:
        '200':
          description: La ligne.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Row' }
        '422': { $ref: '#/components/responses/Invalid' }
    put:
      tags: [Lignes]
      operationId: replaceRow
      summary: Remplacer une ligne
      description: 'Les champs modifiables absents du corps sont vidés. Portée : `rows:write`.'
      x-scope: rows:write
      parameters:
        - $ref: '#/components/parameters/idempotencyKey'
        - $ref: '#/components/parameters/fieldKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RowInput' }
      responses:
        '200':
          description: La ligne.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Row' }
        '422': { $ref: '#/components/responses/Invalid' }
    delete:
      tags: [Lignes]
      operationId: deleteRow
      summary: Supprimer une ligne
      description: 'Met la ligne à la corbeille, restaurable 30 jours. Portée : `rows:write`.'
      x-scope: rows:write
      parameters:
        - $ref: '#/components/parameters/idempotencyKey'
      responses:
        '200': { $ref: '#/components/responses/Deleted' }
        '404': { $ref: '#/components/responses/NotFound' }

  /tables/{tableId}/rows/{rowId}/history:
    parameters:
      - $ref: '#/components/parameters/tableId'
      - $ref: '#/components/parameters/rowId'
    get:
      tags: [Lignes]
      operationId: getRowHistory
      summary: Historique d'une ligne
      description: |
        Les changements de la ligne, du plus récent au plus ancien, dans la limite de rétention du plan.
        Portée : `rows:read`.
      x-scope: rows:read
      parameters:
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/cursor'
      responses:
        '200':
          description: Les changements.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Change' }

  /tables/{tableId}/rows/{rowId}/comments:
    parameters:
      - $ref: '#/components/parameters/tableId'
      - $ref: '#/components/parameters/rowId'
    get:
      tags: [Commentaires]
      operationId: listComments
      summary: Lister les commentaires
      description: 'Du plus ancien au plus récent. Portée : `rows:read`.'
      x-scope: rows:read
      parameters:
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/cursor'
      responses:
        '200':
          description: Les commentaires.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Comment' }
    post:
      tags: [Commentaires]
      operationId: createComment
      summary: Ajouter un commentaire
      description: 'Mentionnez un membre avec `@[usr…]` : il est prévenu. Portée : `rows:write`.'
      x-scope: rows:write
      parameters:
        - $ref: '#/components/parameters/idempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string, minLength: 1, maxLength: 10000 }
      responses:
        '201':
          description: Le commentaire.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Comment' }

  /tables/{tableId}/comments/{commentId}:
    parameters:
      - $ref: '#/components/parameters/tableId'
      - name: commentId
        in: path
        required: true
        schema: { type: string }
    patch:
      tags: [Commentaires]
      operationId: updateComment
      summary: Modifier un commentaire
      description: 'Seul son auteur le modifie. Portée : `rows:write`.'
      x-scope: rows:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string, minLength: 1, maxLength: 10000 }
      responses:
        '200':
          description: Le commentaire.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Comment' }
        '403': { $ref: '#/components/responses/Forbidden' }
    delete:
      tags: [Commentaires]
      operationId: deleteComment
      summary: Supprimer un commentaire
      description: 'Son auteur le supprime ; un Admin du workspace supprime tout commentaire. Portée : `rows:write`.'
      x-scope: rows:write
      responses:
        '200': { $ref: '#/components/responses/Deleted' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /bases/{baseId}/attachments:
    parameters:
      - $ref: '#/components/parameters/baseId'
    post:
      tags: [Pièces jointes]
      operationId: uploadAttachment
      summary: Téléverser un fichier
      description: |
        Envoie le fichier tel quel dans le corps (50 Mo au plus), avec son type dans `Content-Type` et son
        nom dans `filename`. Placez ensuite `{ "id": "att…" }` dans une valeur de pièces jointes d'une
        table de la même base.

        ```
        curl -X POST "https://api.rangee.io/v1/bases/app…/attachments?filename=devis.pdf" \
          -H "Authorization: Bearer $RANGEE_TOKEN" -H "Content-Type: application/pdf" \
          --data-binary @devis.pdf
        ```

        Portée : `rows:write`.
      x-scope: rows:write
      parameters:
        - name: filename
          in: query
          required: true
          schema: { type: string, maxLength: 255 }
        - name: type
          in: query
          description: Type du fichier, si `Content-Type` ne le donne pas.
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema: { type: string, format: binary }
      responses:
        '201':
          description: Le fichier.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Attachment' }
        '413': { $ref: '#/components/responses/TooLarge' }

  /bases/{baseId}/automations:
    parameters:
      - $ref: '#/components/parameters/baseId'
    get:
      tags: [Automatisations]
      operationId: listAutomations
      summary: Lister les automatisations
      description: 'Les automatisations de la base, avec leur déclencheur et leurs variables. Portée : `automations:read`.'
      x-scope: automations:read
      parameters:
        - name: trigger
          in: query
          description: Ne garde que les automatisations à ce déclencheur.
          schema: { type: string, enum: [api, button, webhook, schedule, row] }
      responses:
        '200':
          description: Les automatisations.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Automation' }

  /automations/{automationId}:
    parameters:
      - $ref: '#/components/parameters/automationId'
    get:
      tags: [Automatisations]
      operationId: getAutomation
      summary: Lire une automatisation
      description: 'Son déclencheur, ses variables et son état. Portée : `automations:read`.'
      x-scope: automations:read
      responses:
        '200':
          description: L'automatisation.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Automation' }
        '404': { $ref: '#/components/responses/NotFound' }

  /automations/{automationId}/runs:
    parameters:
      - $ref: '#/components/parameters/automationId'
    post:
      tags: [Automatisations]
      operationId: runAutomation
      summary: Déclencher une automatisation
      description: |
        Pour un déclencheur « Appel API », envoyez `inputs` ; pour un déclencheur « Bouton », envoyez
        `rowId`. Les variables sont vérifiées avant l'exécution : une erreur répond 422 `INVALID_INPUTS`
        et ne compte pas dans votre quota.

        Sans `wait`, la réponse est 202 et l'exécution est en file. Avec `wait`, la réponse attend la fin
        de l'exécution jusqu'à ce nombre de secondes : 200 si elle a fini, 202 sinon. Au-delà du quota
        d'exécutions du plan, l'exécution est créée avec l'état `refused` (réponse 200).
        Portée : `automations:run`.
      x-scope: automations:run
      parameters:
        - $ref: '#/components/parameters/idempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                inputs:
                  type: object
                  description: Les variables déclarées par l'automatisation, par nom.
                  additionalProperties: true
                rowId:
                  type: string
                  description: La ligne, pour un déclencheur « Bouton ».
                wait:
                  type: integer
                  minimum: 0
                  maximum: 30
                  default: 0
            example:
              inputs: { email: claire@atelier-martin.fr, montant: 1250, devisId: row01JD… }
              wait: 10
      responses:
        '200':
          description: L'exécution, terminée.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Run' }
        '202':
          description: L'exécution, en file ou en cours.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Run' }
        '422':
          description: Variables invalides, ou automatisation désactivée.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example:
                error:
                  code: INVALID_INPUTS
                  message: 2 variables sont invalides
                  details:
                    inputs:
                      - { name: email, problem: required }
                      - { name: montant, problem: type, expected: number }
        '429': { $ref: '#/components/responses/RateLimited' }
    get:
      tags: [Automatisations]
      operationId: listRuns
      summary: Lister les exécutions
      description: 'Les exécutions des 30 derniers jours, de la plus récente à la plus ancienne. Portée : `automations:read`.'
      x-scope: automations:read
      parameters:
        - name: status
          in: query
          schema: { $ref: '#/components/schemas/RunStatus' }
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/cursor'
      responses:
        '200':
          description: Les exécutions.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Run' }

  /runs/{runId}:
    parameters:
      - name: runId
        in: path
        required: true
        schema: { type: string, examples: [run01JD…] }
    get:
      tags: [Automatisations]
      operationId: getRun
      summary: Suivre une exécution
      description: 'Portée : `automations:read` ou `automations:run`.'
      x-scope: automations:read
      responses:
        '200':
          description: L'exécution.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Run' }
        '404': { $ref: '#/components/responses/NotFound' }

  /bases/{baseId}/webhooks:
    parameters:
      - $ref: '#/components/parameters/baseId'
    get:
      tags: [Webhooks]
      operationId: listWebhooks
      summary: Lister les webhooks
      description: 'Portée : `webhooks:manage`.'
      x-scope: webhooks:manage
      responses:
        '200':
          description: Les webhooks.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Webhook' }
    post:
      tags: [Webhooks]
      operationId: createWebhook
      summary: Créer un webhook
      description: |
        Le secret de signature n'est renvoyé qu'à la création. 20 webhooks par base au plus ; l'adresse
        doit être en HTTPS et publique. Portée : `webhooks:manage`.
      x-scope: webhooks:manage
      parameters:
        - $ref: '#/components/parameters/idempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookInput' }
            example:
              url: https://exemple.fr/rangee
              events: [row.created, row.updated]
              tableIds: [tbl01JD…]
      responses:
        '201':
          description: Le webhook et son secret.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Webhook'
                  - type: object
                    properties:
                      secret:
                        type: string
                        description: Montré une seule fois.
        '422': { $ref: '#/components/responses/Invalid' }

  /webhooks/{webhookId}:
    parameters:
      - $ref: '#/components/parameters/webhookId'
    get:
      tags: [Webhooks]
      operationId: getWebhook
      summary: Lire un webhook
      x-scope: webhooks:manage
      responses:
        '200':
          description: Le webhook.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Webhook' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Webhooks]
      operationId: updateWebhook
      summary: Modifier un webhook
      description: 'Changez son adresse, ses événements, ou réactivez-le après une désactivation. Portée : `webhooks:manage`.'
      x-scope: webhooks:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookInput'
                - type: object
                  properties:
                    enabled: { type: boolean }
      responses:
        '200':
          description: Le webhook.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Webhook' }
    delete:
      tags: [Webhooks]
      operationId: deleteWebhook
      summary: Supprimer un webhook
      x-scope: webhooks:manage
      responses:
        '200': { $ref: '#/components/responses/Deleted' }

  /webhooks/{webhookId}/deliveries:
    parameters:
      - $ref: '#/components/parameters/webhookId'
    get:
      tags: [Webhooks]
      operationId: listDeliveries
      summary: Livraisons récentes
      description: 'Les livraisons des 7 derniers jours, réussies ou non. Portée : `webhooks:manage`.'
      x-scope: webhooks:manage
      parameters:
        - name: failed
          in: query
          schema: { type: boolean }
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/cursor'
      responses:
        '200':
          description: Les livraisons.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Delivery' }

  /webhooks/{webhookId}/test:
    parameters:
      - $ref: '#/components/parameters/webhookId'
    post:
      tags: [Webhooks]
      operationId: testWebhook
      summary: Envoyer un test
      description: 'Envoie un événement `ping` signé et renvoie la livraison. Portée : `webhooks:manage`.'
      x-scope: webhooks:manage
      responses:
        '200':
          description: La livraison.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Delivery' }

webhooks:
  rowEvent:
    post:
      tags: [Webhooks]
      operationId: receiveEvent
      summary: Événement reçu
      description: |
        Envoyé à l'adresse du webhook pour chaque changement. Répondez 2xx en moins de 10 secondes ;
        sinon la livraison est rejouée pendant 24 heures. Un même événement peut arriver deux fois :
        dédoublonnez par `id`.
      parameters:
        - name: Rangee-Signature
          in: header
          required: true
          schema: { type: string, examples: ['t=1762162211,v1=5f2b…'] }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Event' }
      responses:
        '200':
          description: Événement reçu.

components:
  securitySchemes:
    jeton:
      type: http
      scheme: bearer
      description: Un jeton personnel créé dans Réglages › API.

  parameters:
    baseId:
      name: baseId
      in: path
      required: true
      schema: { type: string, examples: [app01JD8Q2…] }
    tableId:
      name: tableId
      in: path
      required: true
      schema: { type: string, examples: [tbl01JD8Q2…] }
    fieldId:
      name: fieldId
      in: path
      required: true
      schema: { type: string, examples: [fld01JD8Q2…] }
    rowId:
      name: rowId
      in: path
      required: true
      schema: { type: string, examples: [row01JD8Q2…] }
    automationId:
      name: automationId
      in: path
      required: true
      schema: { type: string, examples: [aut01JD8Q2…] }
    webhookId:
      name: webhookId
      in: path
      required: true
      schema: { type: string, examples: [whk01JD8Q2…] }
    limit:
      name: limit
      in: query
      description: Éléments par page.
      schema: { type: integer, minimum: 1, maximum: 1000, default: 100 }
    cursor:
      name: cursor
      in: query
      description: Le `nextCursor` de la page précédente.
      schema: { type: string }
    fields:
      name: fields
      in: query
      description: Ne renvoie que ces champs (identifiants ou noms, séparés par des virgules).
      schema: { type: string }
    fieldKey:
      name: fieldKey
      in: query
      description: Clés des valeurs dans la réponse.
      schema: { type: string, enum: [id, name], default: id }
    force:
      name: force
      in: query
      description: Supprime même si l'élément est utilisé ailleurs.
      schema: { type: boolean, default: false }
    idempotencyKey:
      name: Idempotency-Key
      in: header
      description: Rejouée avec le même corps dans les 24 heures, la requête renvoie la même réponse sans réécrire.
      schema: { type: string, minLength: 1, maxLength: 255 }

  responses:
    RowPage:
      description: Une page de lignes.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Page'
              - properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Row' }
    Deleted:
      description: Supprimé.
      content:
        application/json:
          schema:
            type: object
            properties:
              id: { type: string }
              deleted: { type: boolean, const: true }
    Unauthenticated:
      description: Jeton absent, invalide, révoqué ou expiré (`UNAUTHENTICATED`).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Portée manquante (`MISSING_SCOPE`) ou rôle insuffisant (`FORBIDDEN`).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Absent, ou hors du périmètre du jeton (`NOT_FOUND`).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Conflict:
      description: '`IDEMPOTENCY_CONFLICT`, `UPSERT_AMBIGUOUS`, `FIELD_IN_USE`, `NAME_TAKEN`, `LAST_TABLE` ou `PRIMARY_FIELD`.'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    TooLarge:
      description: Corps trop gros (`PAYLOAD_TOO_LARGE`).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Invalid:
      description: |
        Requête refusée, avec le chemin de l'erreur dans `details.path` : `INVALID_REQUEST`,
        `INVALID_VALUE`, `UNKNOWN_FIELD`, `READ_ONLY_FIELD`, `INVALID_FILTER`, `INVALID_FORMULA`,
        `QUERY_TOO_EXPENSIVE` ou `ROW_LIMIT`.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error:
              code: INVALID_VALUE
              message: « Montant » attend un nombre
              details: { path: 'rows[2].values.Montant' }
    RateLimited:
      description: Trop de requêtes (`RATE_LIMITED`, avec `Retry-After`) ou quota du mois atteint (`MONTHLY_LIMIT`).
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - BAD_REQUEST
                - UNAUTHENTICATED
                - MISSING_SCOPE
                - FORBIDDEN
                - NOT_FOUND
                - IDEMPOTENCY_CONFLICT
                - UPSERT_AMBIGUOUS
                - FIELD_IN_USE
                - NAME_TAKEN
                - LAST_TABLE
                - PRIMARY_FIELD
                - PAYLOAD_TOO_LARGE
                - INVALID_REQUEST
                - INVALID_VALUE
                - UNKNOWN_FIELD
                - READ_ONLY_FIELD
                - INVALID_FILTER
                - INVALID_FORMULA
                - QUERY_TOO_EXPENSIVE
                - INVALID_INPUTS
                - AUTOMATION_DISABLED
                - ROW_LIMIT
                - RATE_LIMITED
                - MONTHLY_LIMIT
                - INTERNAL
            message:
              type: string
              description: Lisible par une personne, dans la langue du compte.
            details:
              type: object
              additionalProperties: true

    Page:
      type: object
      required: [data, nextCursor]
      properties:
        data: { type: array, items: {} }
        nextCursor:
          type: [string, 'null']
          description: À passer en `cursor` pour la page suivante ; `null` sur la dernière.

    Me:
      type: object
      properties:
        user:
          $ref: '#/components/schemas/Member'
        token:
          type: object
          properties:
            id: { type: string, examples: [pat01JD…] }
            name: { type: string }
            scopes:
              type: array
              items: { $ref: '#/components/schemas/Scope' }
            workspaceId: { type: string }
            baseIds:
              type: [array, 'null']
              description: '`null` : toutes les bases accessibles du workspace.'
              items: { type: string }
            expiresAt: { type: [string, 'null'], format: date-time }
        usage:
          type: object
          properties:
            callsThisMonth: { type: integer }
            limit: { type: [integer, 'null'], description: '`null` : sans plafond.' }
            resetsAt: { type: string, format: date-time }

    Scope:
      type: string
      enum: [rows:read, rows:write, schema:read, schema:write, automations:read, automations:run, webhooks:manage]

    Member:
      type: object
      properties:
        id: { type: string, examples: [usr01JD…] }
        email: { type: string, format: email }
        name: { type: string }

    BaseSummary:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        workspaceId: { type: string }
        role: { type: string, enum: [admin, user, reader] }

    Base:
      allOf:
        - $ref: '#/components/schemas/BaseSummary'
        - type: object
          properties:
            tables:
              type: array
              items: { $ref: '#/components/schemas/Table' }

    Table:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        primaryFieldId: { type: string }
        fields:
          type: array
          items: { $ref: '#/components/schemas/Field' }
        views:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              type: { type: string, enum: [grid, form, kanban, calendar, gallery, gantt, timeline, list] }

    FieldType:
      type: string
      enum:
        - singleLineText
        - multilineText
        - richText
        - email
        - url
        - phoneNumber
        - number
        - currency
        - percent
        - rating
        - duration
        - checkbox
        - date
        - dateTime
        - singleSelect
        - multipleSelects
        - singleCollaborator
        - multipleCollaborators
        - gauge
        - barcode
        - aiText
        - autoNumber
        - multipleAttachments
        - multipleRowLinks
        - count
        - formula
        - lookup
        - rollup
        - createdTime
        - lastModifiedTime
        - createdBy
        - lastModifiedBy
        - rowId
        - aggregation
        - button

    FieldOptions:
      type: object
      description: |
        Dépend du type : `choices` pour les sélections, `precision` et `symbol` pour les nombres et devises,
        `max` pour une note, `linkedTableId` pour un lien, `formula` pour une formule, etc. Même forme en
        lecture et en écriture.
      additionalProperties: true
      examples:
        - choices:
            - { id: sel01JD…, name: Signé, color: green }
            - { id: sel01JE…, name: En cours, color: yellow }

    FieldInput:
      type: object
      required: [name, type]
      properties:
        name: { type: string, maxLength: 100 }
        type: { $ref: '#/components/schemas/FieldType' }
        description: { type: string, maxLength: 20000 }
        options: { $ref: '#/components/schemas/FieldOptions' }

    Field:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        type: { $ref: '#/components/schemas/FieldType' }
        description: { type: [string, 'null'] }
        readOnly:
          type: boolean
          description: Champ calculé, refusé en écriture.
        options: { $ref: '#/components/schemas/FieldOptions' }

    View:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        tableId: { type: string }
        type: { type: string }
        filter: { $ref: '#/components/schemas/Filter' }
        sort:
          type: array
          items: { $ref: '#/components/schemas/Sort' }
        visibleFieldIds:
          type: array
          items: { type: string }

    Values:
      type: object
      description: Les valeurs de la ligne, par identifiant de champ (ou par nom en écriture et avec `fieldKey=name`).
      additionalProperties: true
      examples:
        - { fld01JDnom: Atelier Martin, fld01JDstatut: Signé, fld01JDmontant: 1250 }

    Coerce:
      type: boolean
      default: false
      description: Convertit les textes au type du champ et crée les options de sélection inconnues.

    RowInput:
      type: object
      required: [values]
      properties:
        values: { $ref: '#/components/schemas/Values' }
        coerce: { $ref: '#/components/schemas/Coerce' }

    Row:
      type: object
      required: [id, createdAt, updatedAt, values]
      properties:
        id: { type: string, examples: [row01JD8Q2…] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        values:
          allOf:
            - $ref: '#/components/schemas/Values'
          description: Les valeurs non vides ; un champ vide est absent.

    RowList:
      type: object
      properties:
        rows:
          type: array
          items: { $ref: '#/components/schemas/Row' }

    Condition:
      type: object
      required: [field, operator]
      properties:
        field:
          type: string
          description: Identifiant ou nom du champ.
        operator:
          type: string
          enum: [isEmpty, isNotEmpty, is, isNot, contains, doesNotContain, isAnyOf, isNoneOf, hasAnyOf, hasAllOf, linksTo, '=', '!=', '<', '<=', '>', '>=']
        value:
          description: Absente pour `isEmpty` et `isNotEmpty` ; une liste pour `isAnyOf`, `isNoneOf`, `hasAnyOf`, `hasAllOf`.

    Filter:
      description: Un groupe `and` ou `or` de conditions et de groupes, sur 5 niveaux au plus.
      oneOf:
        - type: object
          required: [and]
          properties:
            and:
              type: array
              maxItems: 100
              items:
                anyOf:
                  - $ref: '#/components/schemas/Condition'
                  - $ref: '#/components/schemas/Filter'
        - type: object
          required: [or]
          properties:
            or:
              type: array
              maxItems: 100
              items:
                anyOf:
                  - $ref: '#/components/schemas/Condition'
                  - $ref: '#/components/schemas/Filter'

    Sort:
      type: object
      required: [field]
      properties:
        field: { type: string }
        direction: { type: string, enum: [asc, desc], default: asc }

    Query:
      type: object
      properties:
        filter: { $ref: '#/components/schemas/Filter' }
        search:
          type: string
          description: Texte cherché dans les champs texte, sans tenir compte de la casse ni des accents.
        viewId:
          type: string
          description: Part des filtres et tris de cette vue.
        formula:
          type: string
          description: Une formule Rangee qui doit valoir vrai, par exemple `AND({Montant} > 1000, {Statut} = "Signé")`.
        sort:
          type: array
          maxItems: 10
          items: { $ref: '#/components/schemas/Sort' }
        fields:
          type: array
          items: { type: string }
        fieldKey: { type: string, enum: [id, name], default: id }
        limit: { type: integer, minimum: 1, maximum: 1000, default: 100 }
        cursor: { type: string }

    Change:
      type: object
      properties:
        at: { type: string, format: date-time }
        actor:
          type: object
          properties:
            type: { type: string, enum: [user, automation] }
            id: { type: string }
            name: { type: string }
            email: { type: string, description: Pour une personne. }
        kind: { type: string, enum: [create, update, delete, restore] }
        changes:
          type: array
          items:
            type: object
            properties:
              fieldId: { type: string }
              before: {}
              after: {}

    Comment:
      type: object
      properties:
        id: { type: string, examples: [com01JD…] }
        rowId: { type: string }
        author: { $ref: '#/components/schemas/Member' }
        text: { type: string }
        mentions:
          type: array
          items: { $ref: '#/components/schemas/Member' }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: [string, 'null'], format: date-time }

    Attachment:
      type: object
      properties:
        id: { type: string, examples: [att01JD…] }
        filename: { type: string }
        size: { type: integer, description: En octets. }
        type: { type: string, examples: [application/pdf] }
        url:
          type: string
          format: uri
          description: Adresse signée et temporaire du fichier.

    Input:
      type: object
      description: Une variable demandée par une automatisation à déclencheur « Appel API ».
      required: [name, type, required]
      properties:
        name:
          type: string
          pattern: '^[A-Za-z_][A-Za-z0-9_]{0,63}$'
        label: { type: string }
        description: { type: [string, 'null'] }
        type:
          type: string
          enum: [text, number, boolean, date, dateTime, email, rowId, rowIds, json]
        tableId:
          type: [string, 'null']
          description: Pour `rowId` et `rowIds`, la table de la ligne.
        required: { type: boolean }
        default: {}

    Automation:
      type: object
      properties:
        id: { type: string, examples: [aut01JD…] }
        baseId: { type: string }
        name: { type: string }
        description: { type: [string, 'null'] }
        enabled:
          type: boolean
          description: Publiée et active ; seule une automatisation active se déclenche.
        trigger:
          type: object
          properties:
            type: { type: string, enum: [api, button, webhook, schedule, rowCreated, rowUpdated, rowMatches, rowEntersView, formSubmitted] }
            tableId:
              type: [string, 'null']
              description: Pour « Bouton », la table de la ligne à envoyer.
        inputs:
          type: array
          description: Les variables, pour un déclencheur « Appel API » ou « Webhook reçu ».
          items: { $ref: '#/components/schemas/Input' }
        output:
          type: array
          description: Les noms des valeurs renvoyées par l'étape « Renvoyer des valeurs ».
          items: { type: string }

    RunStatus:
      type: string
      enum: [queued, running, waiting, succeeded, failed, refused]

    Run:
      type: object
      properties:
        id: { type: string, examples: [run01JD…] }
        automationId: { type: string }
        number: { type: integer, description: Numéro de l'exécution dans l'automatisation. }
        status: { $ref: '#/components/schemas/RunStatus' }
        inputs:
          type: object
          additionalProperties: true
        output:
          type: object
          description: Les valeurs de l'étape « Renvoyer des valeurs » ; `{}` sans elle.
          additionalProperties: true
        error:
          type: [object, 'null']
          properties:
            stepId: { type: string }
            stepName: { type: string }
            message: { type: string }
        createdAt: { type: string, format: date-time }
        startedAt: { type: [string, 'null'], format: date-time }
        finishedAt: { type: [string, 'null'], format: date-time }

    WebhookEventType:
      type: string
      enum: [row.created, row.updated, row.deleted, field.created, field.updated, field.deleted, table.created, table.updated, table.deleted]

    WebhookInput:
      type: object
      required: [url, events]
      properties:
        url: { type: string, format: uri, pattern: '^https://' }
        events:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/WebhookEventType' }
        tableIds:
          type: array
          description: Limite aux changements de ces tables ; toutes par défaut.
          items: { type: string }
        fieldIds:
          type: array
          description: Limite `row.updated` aux changements de ces champs.
          items: { type: string }
        includeValues:
          type: boolean
          default: true
          description: '`false` : seuls l''identifiant de la ligne et, pour `row.updated`, les identifiants des champs changés (`changedFieldIds`) sont envoyés.'

    Webhook:
      type: object
      properties:
        id: { type: string, examples: [whk01JD…] }
        baseId: { type: string }
        url: { type: string, format: uri }
        events:
          type: array
          items: { $ref: '#/components/schemas/WebhookEventType' }
        tableIds:
          type: array
          items: { type: string }
        fieldIds:
          type: array
          items: { type: string }
        includeValues: { type: boolean }
        enabled: { type: boolean }
        disabledReason:
          type: [string, 'null']
          description: Par exemple, 24 heures de livraisons en échec.
        lastDeliveryAt: { type: [string, 'null'], format: date-time }
        createdAt: { type: string, format: date-time }

    Event:
      type: object
      required: [id, type, createdAt, baseId]
      properties:
        id:
          type: string
          description: Identifiant de l'événement, pour dédoublonner.
          examples: [evt01JD…]
        type:
          oneOf:
            - $ref: '#/components/schemas/WebhookEventType'
            - type: string
              const: ping
        createdAt: { type: string, format: date-time }
        baseId: { type: string }
        tableId: { type: [string, 'null'] }
        actor: { $ref: '#/components/schemas/Change/properties/actor' }
        data:
          type: object
          properties:
            row:
              $ref: '#/components/schemas/Row'
            previous:
              type: object
              description: Pour `row.updated`, les anciennes valeurs des seuls champs changés.
              properties:
                values: { $ref: '#/components/schemas/Values' }
            field:
              $ref: '#/components/schemas/Field'
            table:
              $ref: '#/components/schemas/Table'

    Delivery:
      type: object
      properties:
        id: { type: string }
        eventId: { type: string }
        eventType: { type: string }
        attempt: { type: integer }
        status: { type: string, enum: [succeeded, failed, pending] }
        responseStatus: { type: [integer, 'null'] }
        durationMs: { type: [integer, 'null'] }
        error: { type: [string, 'null'] }
        at: { type: string, format: date-time }
        nextAttemptAt: { type: [string, 'null'], format: date-time }
