# API SeveUp v1.18.0

> Documentation de l'API publique SeveUp, générée depuis le contrat OpenAPI.

- Page développeurs : https://www.seve-up.app/fr/developpeurs
- Contrat OpenAPI (YAML) : https://www.seve-up.app/openapi/public-v1.yaml
- Journal des changements (JSON) : https://www.seve-up.app/openapi/changelog-v1.json
- Journal des changements (flux Atom) : https://www.seve-up.app/openapi/changelog-v1.xml
- Adresse : https://api.seve-up.app

API publique de SeveUp App, réservée aux plans PRO, BUSINESS, ENTREPRISE et CUSTOM (TRIAL : lecture seule).

**Authentification.** Une clé d'organisation `sk_…`, créée dans la console d'organisation (section « Clés d'API »),
envoyée en `Authorization: Bearer sk_…`. Chaque clé porte des scopes ; une route sans le scope requis répond `403`
avec `code: SCOPE_MISSING`. Aucun secret n'est accepté dans une URL.

**Erreurs.** Toujours `application/problem+json` (RFC 9457). Le champ `code` est stable : le programme s'appuie
dessus, jamais sur `title` ou `detail`. Le `type` s'en déduit (`SCOPE_MISSING` donne
`https://seve-up.com/problems/scope-missing`). Une requête invalide (`VALIDATION`) détaille chaque champ fautif dans
`errors`. Un identifiant inconnu, mal formé ou d'une autre organisation répond `404` `NOT_FOUND`.

**Pagination.** Par curseur opaque : `?limit=` (1 à 200, 50 par défaut) et `?cursor=` (valeur de `next_cursor`
de la page précédente). Fin de liste : `has_more: false` et `next_cursor: null`.

**Plans.** L'API est ouverte aux plans PRO, BUSINESS, ENTREPRISE et CUSTOM. Le plan d'essai (TRIAL) l'ouvre en lecture
seule : toute écriture répond `403` `PLAN_READ_ONLY`. Les autres plans répondent `403` `PLAN_REQUIRED`. Un plan
payant arrivé à échéance reste lisible ; ses écritures répondent `PLAN_READ_ONLY`.

**Débit.** Chaque réponse porte `RateLimit-Policy` et `RateLimit` (draft-ietf-httpapi-ratelimit-headers), par clé et
par fenêtre d'une minute : 60 requêtes en essai, 600 en PRO, 1 200 en BUSINESS, 3 000 en ENTREPRISE et CUSTOM.
Au-delà : `429` `RATE_LIMITED` avec `Retry-After` en secondes.

**Quotas.** Par organisation et par mois calendaire (UTC) : appels, versions déposées, volume déposé, contrôles
IDS. Un quota atteint répond `429` `QUOTA_EXCEEDED`, avec `Retry-After` jusqu'au 1er du mois suivant. Un quota se
consomme avant le lancement du traitement : une requête refusée ne compte pas. Si la vérification est
momentanément impossible, l'appel répond `503` `BUSY` plutôt que de passer sans être compté.

**Capacité.** Les opérations marquées `x-heavy` (dépôt, conversion, contrôle IDS, export, synthèses) partagent un
nombre limité de places ; une organisation a au plus un nombre fixé de traitements actifs (3 en PRO, 5 en
BUSINESS, 10 en ENTREPRISE et CUSTOM). Place indisponible : `503` `BUSY` avec `Retry-After`. Trop de traitements en
cours : `429` `QUOTA_EXCEEDED` avec `Retry-After`.

**Idempotence.** Tout `POST` accepte `Idempotency-Key` (1 à 200 caractères, gardée 24 h par organisation). Rejouer
la même clé avec le même corps renvoie la réponse d'origine et `Idempotent-Replayed: true` ; avec un autre corps,
`409` `IDEMPOTENCY_KEY_REUSED`.

**Traitements longs.** Conversion d'une maquette, contrôle IDS et export répondent `202` avec une ressource
`Job` ; suivre `GET /api/v1/jobs/{job_id}` ou s'abonner aux webhooks.

**Types.** Les nombres et booléens sont de vrais types JSON. Les dates sont en RFC 3339 (UTC).

## Démarrer

De la clé au résultat d'un contrôle IDS, en sept appels. Les exemples utilisent `curl` et `jq` ; toute requête porte `Authorization: Bearer` suivi de la clé.

### 1. Créer une clé

Dans SeveUp App, console d'organisation, section « Clés d'API » (réservée aux administrateurs) : « Clé de l'API publique », cochez les scopes utiles (`projects:read`, `models:read`, `models:write`, `ids:read`, `ids:write`). Le secret `sk_…` n'est montré qu'une fois. La même section affiche l'usage de l'API : appels, quotas du mois et erreurs.

```bash
export SEVEUP_API=https://api.seve-up.app
export SEVEUP_KEY=sk_…   # clé d'organisation
```

### 2. Lister les projets

Réponse paginée par curseur : `data`, `has_more`, `next_cursor`.

```bash
curl -s "$SEVEUP_API/api/v1/projects" -H "Authorization: Bearer $SEVEUP_KEY" | jq '.data[] | {id, name}'
```

### 3. Ouvrir un dépôt

Le fichier ne passe pas par l'API : la réponse donne une adresse d'envoi TUS et ses en-têtes, valables 2 h. `Idempotency-Key` permet de rejouer l'appel sans créer un second dépôt.

```bash
FICHIER=maquette.ifc
TAILLE=$(wc -c < "$FICHIER")
curl -s -X POST "$SEVEUP_API/api/v1/uploads" \
  -H "Authorization: Bearer $SEVEUP_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: depot-$(date +%s)" \
  -d "{\"file_name\": \"$FICHIER\", \"size_bytes\": $TAILLE, \"purpose\": \"model\"}" > depot.json
DEPOT=$(jq -r .id depot.json)
```

### 4. Envoyer le fichier

Protocole TUS 1.0 : création, puis envoi des octets. Pour un gros fichier, préférez un client TUS (tus-js-client, tuspy…) qui reprend après une coupure, avec des morceaux de 6 Mo. Puis `complete` vérifie que le fichier est arrivé en entier.

```bash
URL=$(jq -r .upload_url depot.json)
AUTH=$(jq -r '.upload_headers.Authorization' depot.json)
META=$(jq -r '.upload_headers["Upload-Metadata"]' depot.json)
LOC=$(curl -s -i -X POST "$URL" -H "Authorization: $AUTH" -H "Tus-Resumable: 1.0.0" \
  -H "Upload-Metadata: $META" -H "Upload-Length: $TAILLE" \
  | awk 'tolower($1)=="location:" {print $2}' | tr -d '\r')
curl -s -X PATCH "$LOC" -H "Authorization: $AUTH" -H "Tus-Resumable: 1.0.0" \
  -H "Upload-Offset: 0" -H "Content-Type: application/offset+octet-stream" --data-binary @"$FICHIER"
curl -s -X POST "$SEVEUP_API/api/v1/uploads/$DEPOT/complete" \
  -H "Authorization: Bearer $SEVEUP_KEY" -H "Content-Type: application/json" -d '{}'
```

### 5. Créer la maquette

La conversion démarre aussitôt : réponse `202` avec un job, et son adresse dans `Location`. Pour une nouvelle version d'une maquette existante : `POST /api/v1/models/{model_id}/revisions`.

```bash
PROJET=…   # id d'un projet de l'étape 2
curl -s -X POST "$SEVEUP_API/api/v1/projects/$PROJET/models" \
  -H "Authorization: Bearer $SEVEUP_KEY" -H "Content-Type: application/json" \
  -d "{\"name\": \"Ma maquette\", \"upload_id\": \"$DEPOT\"}" > job.json
REVISION=$(jq -r .target.id job.json)
```

### 6. Suivre le traitement

Statuts : `QUEUED`, `PROCESSING`, puis `COMPLETE` ou `ERROR` (avec `error.code`). Plutôt que d'interroger, un webhook `revision.processing.completed` peut vous prévenir.

```bash
JOB=$(jq -r .id job.json)
until s=$(curl -s "$SEVEUP_API/api/v1/jobs/$JOB" -H "Authorization: Bearer $SEVEUP_KEY" | jq -r .status); \
  [ "$s" = COMPLETE ] || [ "$s" = ERROR ]; do sleep 20; done; echo "$s"
```

### 7. Contrôler avec un IDS et lire le résultat

Choisissez un jeu d'exigences existant, ou déposez un `.ids` (dépôt `purpose: ids`, puis `POST /api/v1/requirement-sets`). Le contrôle est un job comme les autres. Dans le résultat, une spécification compte dans le score si `applicable` est vrai.

```bash
JEU=$(curl -s "$SEVEUP_API/api/v1/requirement-sets" -H "Authorization: Bearer $SEVEUP_KEY" | jq -r '.data[0].id')
curl -s -X POST "$SEVEUP_API/api/v1/ids-checks" \
  -H "Authorization: Bearer $SEVEUP_KEY" -H "Content-Type: application/json" \
  -d "{\"revision_id\": \"$REVISION\", \"requirement_set_id\": \"$JEU\"}"
# … suivre le job comme à l'étape 6, puis :
curl -s "$SEVEUP_API/api/v1/revisions/$REVISION/ids-results" -H "Authorization: Bearer $SEVEUP_KEY" \
  | jq '.data[] | {score_percent, specification_count_failed}'
```

## Organisation

Organisation à laquelle la clé est rattachée.

### GET /api/v1/organization

Lire l'organisation de la clé (`getOrganization`)

Organisation à laquelle la clé est rattachée : son identifiant et son nom, rien d'autre. Une clé ne voit que
cette organisation ; une ressource d'une autre organisation répond `404`. Un agent s'en sert pour répondre
qu'une demande vise une autre organisation, hors de portée de la clé, plutôt que de la chercher. Le nom peut
être `null` s'il n'a jamais été renseigné.

Scopes : `projects:read`

**Réponses**

- `200` L'organisation de la clé. (Organization)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "2f6d1c8a-4b3e-4f7a-9c2d-8e5b1a0f3d47",
  "name": "SNCF Réseau — LNPCA"
}
```

## Projets

Projets de l'organisation visibles par la clé.

### GET /api/v1/projects

Lister les projets (`listProjects`)

Scopes : `projects:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `limit` | query | integer | Taille de page. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |
| `q` | query | string | Filtre sur le nom (contient, sans casse). |

**Réponses**

- `200` Une page de projets. (Page & object)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "data": [
    {
      "id": "5b0f3c3e-1d7a-4d0c-9a51-3f2b8f1e7a10",
      "name": "LNPCA — Section 1",
      "description": "Ligne nouvelle Provence Côte d'Azur",
      "space_id": "0b8a2c47-5a0e-4a7e-8f0d-0c6d2b7e9c31",
      "model_count": 197,
      "requirement_set_count": 4,
      "start_date": "2026-01-05T00:00:00Z",
      "end_date": null,
      "created_at": "2026-01-05T09:12:44Z",
      "updated_at": "2026-10-20T16:02:10Z"
    }
  ],
  "next_cursor": "eyJpZCI6IjViMGYzYzNlIn0",
  "has_more": true
}
```

### POST /api/v1/projects

Créer un projet (`createProject`)

Projet créé au nom du créateur de la clé, dans l'espace `space_id` (à la racine de l'organisation sinon). Un
nom déjà pris dans le même espace (sans tenir compte de la casse) répond `409`.

Scopes : `projects:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Clé d'idempotence, gardée 24 h par organisation. |

**Corps de la requête** (`ProjectCreate`)

- `name` (string, obligatoire)
- `description` (string | null)
- `space_id` (string (uuid) | null) : Espace de l'organisation (`space_id` d'un projet) ; racine si absent.
- `start_date` (string (date-time) | null)
- `end_date` (string (date-time) | null) : Postérieure ou égale à `start_date`.

**Réponses**

- `201` Projet créé. (Project)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "6c1a4d4f-2e8b-4e1d-8b62-4a3c9f2e8b21",
  "name": "LNPCA — Section 2",
  "description": "Ligne nouvelle Provence Côte d'Azur",
  "space_id": "0b8a2c47-5a0e-4a7e-8f0d-0c6d2b7e9c31",
  "space_name": "SNCF",
  "space_path": "~/SNCF",
  "model_count": 0,
  "requirement_set_count": 0,
  "start_date": "2027-01-04T00:00:00Z",
  "end_date": null,
  "created_at": "2026-11-04T09:00:00Z",
  "updated_at": "2026-11-04T09:00:00Z"
}
```

### GET /api/v1/projects/{project_id}

Lire un projet (`getProject`)

Scopes : `projects:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `project_id` (obligatoire) | path | string (uuid) | Identifiant du projet (`id` rendu par `GET /api/v1/projects`). |

**Réponses**

- `200` Le projet. (Project)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "5b0f3c3e-1d7a-4d0c-9a51-3f2b8f1e7a10",
  "name": "LNPCA — Section 1",
  "description": "Ligne nouvelle Provence Côte d'Azur",
  "space_id": "0b8a2c47-5a0e-4a7e-8f0d-0c6d2b7e9c31",
  "model_count": 197,
  "requirement_set_count": 4,
  "start_date": "2026-01-05T00:00:00Z",
  "end_date": null,
  "created_at": "2026-01-05T09:12:44Z",
  "updated_at": "2026-10-20T16:02:10Z"
}
```

### PATCH /api/v1/projects/{project_id}

Modifier un projet (`updateProject`)

Seuls les champs présents changent ; `null` vide un champ facultatif (`description`, dates, `space_id` qui range
le projet à la racine). Renommer vers un nom déjà pris dans l'espace répond `409`, comme un projet synchronisé
depuis Trimble Connect, qui se modifie dans Trimble.

Scopes : `projects:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `project_id` (obligatoire) | path | string (uuid) | Identifiant du projet (`id` rendu par `GET /api/v1/projects`). |

**Corps de la requête** (`ProjectUpdate`)

- `name` (string)
- `description` (string | null)
- `space_id` (string (uuid) | null) : `null` range le projet à la racine de l'organisation.
- `start_date` (string (date-time) | null)
- `end_date` (string (date-time) | null)

**Réponses**

- `200` Le projet modifié. (Project)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "5b0f3c3e-1d7a-4d0c-9a51-3f2b8f1e7a10",
  "name": "LNPCA — Section 1 (APD)",
  "description": "Ligne nouvelle Provence Côte d'Azur",
  "space_id": "0b8a2c47-5a0e-4a7e-8f0d-0c6d2b7e9c31",
  "model_count": 197,
  "requirement_set_count": 4,
  "start_date": "2026-01-05T00:00:00Z",
  "end_date": "2027-06-30T00:00:00Z",
  "created_at": "2026-01-05T09:12:44Z",
  "updated_at": "2026-11-04T09:00:00Z"
}
```

### DELETE /api/v1/projects/{project_id}

Archiver un projet (`archiveProject`)

Comme « Archiver » dans l'App : le projet sort des listes de l'API et de l'App, et se restaure depuis la section
« Archivés » de la liste des projets. Ses maquettes et jeux d'exigences ne sont ni archivés ni supprimés. Un projet
déjà archivé répond `404`.

Scopes : `projects:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `project_id` (obligatoire) | path | string (uuid) | Identifiant du projet (`id` rendu par `GET /api/v1/projects`). |

**Réponses**

- `204` Projet archivé.
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

## Espaces

Espaces de l'organisation (regroupements de projets), et ce qui y est rangé.

### GET /api/v1/spaces

Lister les espaces (`listSpaces`)

Espaces de l'organisation, archivés compris (`archived`). `space_id` d'un projet désigne l'un d'eux.

Scopes : `projects:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `limit` | query | integer | Taille de page. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |
| `q` | query | string | Filtre sur le nom (contient, sans casse). |
| `parent_id` | query | string (uuid) | Sous-espaces directs de cet espace ; espace inconnu ou d'une autre organisation, 404. |

**Réponses**

- `200` Une page d'espaces. (Page & object)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "data": [
    {
      "id": "bd52f342-c3ec-43f8-a3d0-90a7a7c0c269",
      "name": "LNPCA_SYNTHESE",
      "parent_id": null,
      "path": "~/LNPCA_SYNTHESE",
      "archived": false,
      "project_count": 3,
      "created_at": "2026-09-10T13:57:20Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

## Tableaux de bord

Tableaux de bord (modèles de rapports de l'App) d'un espace, et modèles proposés pour en créer.

### GET /api/v1/spaces/{space_id}/dashboards

Lister les tableaux de bord d'un espace (`listSpaceDashboards`)

Tableaux de bord rangés dans l'espace (ceux de la bibliothèque de l'espace dans l'App). Ceux de portée
organisation, visibles dans tous les espaces, n'y sont pas répétés.

Scopes : `projects:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `space_id` (obligatoire) | path | string (uuid) | Identifiant de l'espace (`id` rendu par `GET /api/v1/spaces`, `space_id` d'un projet). |
| `limit` | query | integer | Taille de page. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |

**Réponses**

- `200` Une page de tableaux de bord. (Page & object)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "data": [
    {
      "id": "7e2f9a10-4b3c-4d5e-8f60-1a2b3c4d5e6f",
      "name": "Contrôle IDS LNPCA",
      "description": "Résultats des exigences IDS de vos maquettes : scores, règles, objets en échec.",
      "format": "HTML",
      "space_id": "bd52f342-c3ec-43f8-a3d0-90a7a7c0c269",
      "template_id": "00000000-0000-0000-0000-00000000a000",
      "created_at": "2026-11-12T09:00:00Z",
      "render_url": "https://www.seve-up.app/fr/library/dashboards/7e2f9a10-4b3c-4d5e-8f60-1a2b3c4d5e6f/view"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### POST /api/v1/spaces/{space_id}/dashboards

Créer un tableau de bord dans un espace depuis un modèle (`createSpaceDashboard`)

Copie le modèle `template_id` dans l'espace, sous le nom `name`, au nom du créateur de la clé : comme « Dupliquer
dans ce projet » de l'App (mêmes droits, même contrôle du fichier). Le modèle est un modèle du catalogue
(`GET /api/v1/dashboard-templates`, par exemple « Conformité BIM »
`00000000-0000-0000-0000-00000000a000`) ou un tableau de bord de l'organisation. La copie ne suit pas les mises à
jour ultérieures du modèle. Créer un tableau de bord propre à l'organisation exige un plan qui l'inclut
(`403 PLAN_REQUIRED` sinon). Modèle inconnu, d'une autre organisation ou plus proposé : `404
TEMPLATE_NOT_FOUND` ; modèle réservé non débloqué : `403 TEMPLATE_RESERVED` ; espace archivé : `409`. Deux
tableaux de bord peuvent porter le même nom.

Scopes : `dashboards:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `space_id` (obligatoire) | path | string (uuid) | Identifiant de l'espace (`id` rendu par `GET /api/v1/spaces`, `space_id` d'un projet). |
| `Idempotency-Key` | header | string | Clé d'idempotence, gardée 24 h par organisation. |

**Corps de la requête** (`DashboardCreate`)

- `name` (string, obligatoire)
- `template_id` (string (uuid), obligatoire) : Modèle du catalogue (`GET /api/v1/dashboard-templates`) ou tableau de bord de l'organisation.

**Réponses**

- `201` Tableau de bord créé. (Dashboard)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "7e2f9a10-4b3c-4d5e-8f60-1a2b3c4d5e6f",
  "name": "Contrôle IDS LNPCA",
  "description": "Résultats des exigences IDS de vos maquettes : scores, règles, objets en échec.",
  "format": "HTML",
  "space_id": "bd52f342-c3ec-43f8-a3d0-90a7a7c0c269",
  "template_id": "00000000-0000-0000-0000-00000000a000",
  "created_at": "2026-11-12T09:00:00Z",
  "render_url": "https://www.seve-up.app/fr/library/dashboards/7e2f9a10-4b3c-4d5e-8f60-1a2b3c4d5e6f/view"
}
```

### GET /api/v1/dashboard-templates

Lister les modèles de tableaux de bord proposés (`listDashboardTemplates`)

Modèles du catalogue SeveUp proposés à l'organisation, à passer en `template_id` de
`POST /api/v1/spaces/{space_id}/dashboards`. `reserved` : modèle visible mais réservé, à faire débloquer par
SeveUp avant usage.

Scopes : `projects:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `limit` | query | integer | Taille de page. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |

**Réponses**

- `200` Une page de modèles. (Page & object)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "data": [
    {
      "id": "00000000-0000-0000-0000-00000000a000",
      "name": "Conformité BIM",
      "description": "Résultats des exigences IDS de vos maquettes : scores, règles, objets en échec.",
      "format": "HTML",
      "reserved": false,
      "published_at": "2026-10-08T09:09:07Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

## Maquettes

Maquettes IFC et leurs révisions.

### GET /api/v1/projects/{project_id}/models

Lister les maquettes d'un projet (`listProjectModels`)

Scopes : `models:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `project_id` (obligatoire) | path | string (uuid) | Identifiant du projet (`id` rendu par `GET /api/v1/projects`). |
| `limit` | query | integer | Taille de page. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |

**Réponses**

- `200` Une page de maquettes. (Page & object)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "data": [
    {
      "id": "9d2e7f10-6c4b-4f2a-8e11-2a7c5b3d9e01",
      "name": "LNPCA-S1-OA-PONT-0042",
      "format": "IFC",
      "revision_count": 3,
      "latest_revision_id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b",
      "created_at": "2026-03-02T10:00:00Z",
      "updated_at": "2026-10-18T08:41:00Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### POST /api/v1/projects/{project_id}/models

Créer une maquette depuis un fichier déposé (`createModel`)

Crée la maquette et sa première révision à partir d'un fichier déposé (`POST /api/v1/uploads` puis
`complete`). La conversion démarre aussitôt : la réponse est le job de conversion.

Scopes : `models:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `project_id` (obligatoire) | path | string (uuid) | Identifiant du projet (`id` rendu par `GET /api/v1/projects`). |
| `Idempotency-Key` | header | string | Clé d'idempotence, gardée 24 h par organisation. |

**Corps de la requête** (`ModelCreate`)

- `name` (string, obligatoire)
- `upload_id` (string (uuid), obligatoire) : Dépôt clos (`purpose: model`).

**Réponses**

- `202` Traitement accepté ; suivre le job. (Job)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "1f2e3d4c-5b6a-4978-8a1b-2c3d4e5f6a7b",
  "type": "conversion",
  "status": "QUEUED",
  "percent_complete": 0,
  "eta_seconds": null,
  "target": {
    "type": "revision",
    "id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b"
  },
  "result": null,
  "error": null,
  "created_at": "2026-10-21T07:58:00Z",
  "started_at": null,
  "completed_at": null
}
```

### GET /api/v1/models

Rechercher les maquettes de l'organisation (`searchModels`)

Cherche une maquette par son nom dans toute l'organisation, sans parcourir les projets un à un. `q` filtre sur
le nom (contient, sans casse) ; sans `q`, la liste rend toutes les maquettes de l'organisation, y compris celles
rangées dans aucun projet. Chaque maquette porte les projets qui la contiennent (`projects`, trié par nom) : une
maquette peut appartenir à plusieurs projets, et deux projets de même nom se distinguent par `space_path`.

Scopes : `models:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `limit` | query | integer | Taille de page. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |
| `q` | query | string | Filtre sur le nom de la maquette (contient, sans casse). Absent, toutes les maquettes. |
| `project_id` | query | string (uuid) | Maquettes de ce projet seulement ; projet inconnu ou d'une autre organisation, 404. |

**Réponses**

- `200` Une page de maquettes, avec leurs projets. (Page & object)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "data": [
    {
      "id": "9d2e7f10-6c4b-4f2a-8e11-2a7c5b3d9e01",
      "name": "LNPCA-S1-OA-PONT-0042",
      "format": "IFC",
      "revision_count": 3,
      "latest_revision_id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b",
      "created_at": "2026-03-02T10:00:00Z",
      "updated_at": "2026-10-18T08:41:00Z",
      "projects": [
        {
          "id": "5b0f3c3e-1d7a-4d0c-9a51-3f2b8f1e7a10",
          "name": "LNPCA — Section 1",
          "space_path": "~/SNCF"
        }
      ]
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### GET /api/v1/models/{model_id}

Lire une maquette (`getModel`)

Scopes : `models:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `model_id` (obligatoire) | path | string (uuid) | Identifiant de la maquette (`id` rendu par la liste des maquettes du projet). |

**Réponses**

- `200` La maquette. (Model)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "9d2e7f10-6c4b-4f2a-8e11-2a7c5b3d9e01",
  "name": "LNPCA-S1-OA-PONT-0042",
  "format": "IFC",
  "revision_count": 3,
  "latest_revision_id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b",
  "created_at": "2026-03-02T10:00:00Z",
  "updated_at": "2026-10-18T08:41:00Z"
}
```

### PATCH /api/v1/models/{model_id}

Renommer une maquette (`updateModel`)

Seul `name` se modifie. Une maquette synchronisée depuis Trimble Connect se renomme dans Trimble (`409`).

Scopes : `models:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `model_id` (obligatoire) | path | string (uuid) | Identifiant de la maquette (`id` rendu par la liste des maquettes du projet). |

**Corps de la requête** (`ModelUpdate`)

- `name` (string, obligatoire)

**Réponses**

- `200` La maquette renommée. (Model)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "7d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6",
  "name": "LNPCA_OA-MN-130_STR",
  "format": "IFC",
  "revision_count": 3,
  "latest_revision_id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b",
  "created_at": "2026-03-02T10:00:00Z",
  "updated_at": "2026-11-04T09:00:00Z"
}
```

### DELETE /api/v1/models/{model_id}

Archiver une maquette (`archiveModel`)

Comme « Archiver » dans l'App : la maquette et ses révisions sortent des listes et se restaurent depuis la section
« Archivés » de la bibliothèque. Ses liens aux projets sont retirés et ne reviennent pas à la restauration. Refusé
(`409`) tant qu'une de ses révisions est en traitement. Une maquette déjà archivée répond `404`.

Scopes : `models:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `model_id` (obligatoire) | path | string (uuid) | Identifiant de la maquette (`id` rendu par la liste des maquettes du projet). |

**Réponses**

- `204` Maquette archivée.
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

### GET /api/v1/models/{model_id}/naming-check

Contrôler le nom de fichier d'une maquette (`checkModelName`)

Le nom du fichier de la dernière révision, contrôlé par la convention de nommage de la maquette (même résultat
que `POST /api/v1/naming-conventions/{naming_convention_id}/check`). `compliant` est nul quand rien n'a été
contrôlé : maquette sans convention (`naming_convention_id` nul), convention archivée, ou sans révision.

Scopes : `models:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `model_id` (obligatoire) | path | string (uuid) | Identifiant de la maquette (`id` rendu par la liste des maquettes du projet). |

**Réponses**

- `200` Le contrôle du nom. (ModelNameCheck)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "model_id": "7c2e9f10-3b4a-4d5e-8f6a-1b2c3d4e5f60",
  "naming_convention_id": "4d5e6f70-8192-4a3b-9c4d-5e6f708192a3",
  "revision_id": "1f2e3d4c-5b6a-4978-8695-a4b3c2d1e0f9",
  "file_name": "OA12-XYZ.ifc",
  "compliant": false,
  "problems": [
    {
      "code": "not_in_list",
      "part": "Discipline",
      "expected": "STR, ARC"
    }
  ],
  "values": {}
}
```

### GET /api/v1/models/{model_id}/revisions

Lister les révisions d'une maquette (`listModelRevisions`)

Révisions de la plus récente à la plus ancienne.

Scopes : `models:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `model_id` (obligatoire) | path | string (uuid) | Identifiant de la maquette (`id` rendu par la liste des maquettes du projet). |
| `limit` | query | integer | Taille de page. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |

**Réponses**

- `200` Une page de révisions. (Page & object)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "data": [
    {
      "id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b",
      "model_id": "9d2e7f10-6c4b-4f2a-8e11-2a7c5b3d9e01",
      "version": 3,
      "file_name": "LNPCA-S1-OA-PONT-0042_C03.ifc",
      "size_bytes": 184467200,
      "ifc_schema": "IFC4X3_ADD2",
      "authoring_software": "Revit 2025",
      "status": "COMPLETE",
      "processing": {
        "conversion": {
          "status": "COMPLETE",
          "percent_complete": 100,
          "eta_seconds": null,
          "error_message": null
        },
        "dataset": {
          "status": "COMPLETE",
          "percent_complete": 100,
          "eta_seconds": null,
          "error_message": null
        },
        "scene": {
          "status": "COMPLETE",
          "percent_complete": 100,
          "eta_seconds": null,
          "error_message": null
        }
      },
      "object_count": 44213,
      "created_at": "2026-10-18T08:41:00Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### POST /api/v1/models/{model_id}/revisions

Ajouter une révision depuis un fichier déposé (`createRevision`)

Nouvelle version de la maquette. La conversion démarre aussitôt ; la réponse est le job.

Scopes : `models:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `model_id` (obligatoire) | path | string (uuid) | Identifiant de la maquette (`id` rendu par la liste des maquettes du projet). |
| `Idempotency-Key` | header | string | Clé d'idempotence, gardée 24 h par organisation. |

**Corps de la requête** (`RevisionCreate`)

- `upload_id` (string (uuid), obligatoire)
- `milestone` (string)

**Réponses**

- `202` Traitement accepté ; suivre le job. (Job)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "1f2e3d4c-5b6a-4978-8a1b-2c3d4e5f6a7b",
  "type": "conversion",
  "status": "QUEUED",
  "percent_complete": 0,
  "eta_seconds": null,
  "target": {
    "type": "revision",
    "id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b"
  },
  "result": null,
  "error": null,
  "created_at": "2026-10-21T07:58:00Z",
  "started_at": null,
  "completed_at": null
}
```

### GET /api/v1/revisions/{revision_id}

Lire une révision et son état de traitement (`getRevision`)

Scopes : `models:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `revision_id` (obligatoire) | path | string (uuid) | Identifiant de la révision (`latest_revision_id` d'une maquette, ou `id` d'une révision). |

**Réponses**

- `200` La révision. (Revision)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b",
  "model_id": "9d2e7f10-6c4b-4f2a-8e11-2a7c5b3d9e01",
  "version": 3,
  "file_name": "LNPCA-S1-OA-PONT-0042_C03.ifc",
  "size_bytes": 184467200,
  "ifc_schema": "IFC4X3_ADD2",
  "authoring_software": "Revit 2025",
  "status": "PROCESSING",
  "processing": {
    "conversion": {
      "status": "COMPLETE",
      "percent_complete": 100,
      "eta_seconds": null,
      "error_message": null
    },
    "dataset": {
      "status": "PROCESSING",
      "percent_complete": 42,
      "eta_seconds": 180,
      "error_message": null
    },
    "scene": {
      "status": "QUEUED",
      "percent_complete": 0,
      "eta_seconds": null,
      "error_message": null
    }
  },
  "object_count": null,
  "created_at": "2026-10-18T08:41:00Z"
}
```

### PATCH /api/v1/revisions/{revision_id}

Changer le jalon d'une révision (`updateRevision`)

Seul `milestone` se modifie (`null` le retire). Une révision n'a pas d'autre nom : elle se désigne par sa maquette et
son numéro de version.

Scopes : `models:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `revision_id` (obligatoire) | path | string (uuid) | Identifiant de la révision (`latest_revision_id` d'une maquette, ou `id` d'une révision). |

**Corps de la requête** (`RevisionUpdate`)

- `milestone` (string | null, obligatoire) : Jalon (APS, APD, PRO, EXE…) ; `null` le retire.

**Réponses**

- `200` La révision modifiée. (Revision)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b",
  "model_id": "9d2e7f10-6c4b-4f2a-8e11-2a7c5b3d9e01",
  "version": 3,
  "milestone": "APD",
  "file_name": "LNPCA-S1-OA-PONT-0042_C03.ifc",
  "size_bytes": 184467200,
  "ifc_schema": "IFC4X3_ADD2",
  "authoring_software": "Revit 2025",
  "status": "COMPLETE",
  "processing": {
    "conversion": {
      "status": "COMPLETE",
      "percent_complete": 100,
      "eta_seconds": null,
      "error_message": null
    },
    "dataset": {
      "status": "COMPLETE",
      "percent_complete": 100,
      "eta_seconds": null,
      "error_message": null
    },
    "scene": {
      "status": "COMPLETE",
      "percent_complete": 100,
      "eta_seconds": null,
      "error_message": null
    }
  },
  "object_count": 44213,
  "created_at": "2026-10-18T08:41:00Z"
}
```

### GET /api/v1/revisions/{revision_id}/objects

Chercher des objets d'une révision (classe, propriété, valeur) (`queryRevisionObjects`)

Lecture ciblée du jeu de données, au plus 50 objets par page, par exemple « les IfcWall dont
`Pset_WallCommon.FireRating` est absente ou vide ». Une propriété se lit sur l'objet, sinon sur son type (l'objet
l'emporte). La classe se compare sans tenir compte de la casse et sans sous-classes ; sans classe, seuls les objets
de la maquette sont lus (ni types ni définitions). Les valeurs sortent en texte. `total` n'est calculé que pour la
première page (nul si le compte est trop long). `count_values=true` compte en plus les valeurs de la propriété ou
de l'attribut sur toute la sélection (`value_counts`, les 50 plus fréquentes, et `distinct_value_count`), sur la
première page seulement. Jeu de données pas terminé ou plus disponible : `409`.

Scopes : `models:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `revision_id` (obligatoire) | path | string (uuid) | Identifiant de la révision (`latest_revision_id` d'une maquette, ou `id` d'une révision). |
| `ifc_class` | query | string | Classe IFC, par exemple `IfcWall`. |
| `property_set` | query | string | Jeu de propriétés ; va avec `property`. |
| `property` | query | string | Nom de la propriété ; va avec `property_set`. |
| `attribute` | query | string | Attribut IFC lu à la place d'une propriété. |
| `condition` | query | string | Filtre sur la propriété ou l'attribut : `any` (aucun filtre, valeur rendue), `missing` (absente), `empty` (présente sans valeur), `missing_or_empty`, `present` (renseignée), `equals`, `not_equals` (renseignée et différente), `contains` (sans tenir compte de la casse). |
| `value` | query | string | Valeur comparée, obligatoire pour `equals`, `not_equals` et `contains`. |
| `limit` | query | integer | Taille de page. |
| `count_values` | query | boolean | Compter les valeurs sur toute la sélection, avec `property` ou `attribute`. Nul si le comptage est trop long ; la page d'objets reste servie. Passer `limit=1` pour ne lire que les comptes. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |

**Réponses**

- `200` Une page d'objets. (ObjectPage)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "revision_id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b",
  "data": [
    {
      "object_id": 470981,
      "global_id": "1EveRUJ9f8ABmwj$jhNe5l",
      "name": "Mur M12",
      "ifc_class": "IfcWall",
      "state": "MISSING",
      "value": null,
      "value_source": null
    }
  ],
  "next_cursor": null,
  "has_more": false,
  "total": 1
}
```

### GET /api/v1/naming-conventions

Lister les conventions de nommage (`listNamingConventions`)

Conventions de nommage des fichiers de l'organisation (archivées exclues), des plus anciennes aux plus récentes.
`project_id` limite aux conventions rattachées à un projet. La liste donne le nombre de parties du nom ; leurs
règles se lisent avec `GET /api/v1/naming-conventions/{naming_convention_id}`.

Scopes : `models:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `limit` | query | integer | Taille de page. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |
| `project_id` | query | string (uuid) | Projet auquel la convention est rattachée. |

**Réponses**

- `200` Une page de conventions. (Page & object)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "data": [
    {
      "id": "4d5e6f70-8192-4a3b-9c4d-5e6f708192a3",
      "name": "LNPCA — maquettes",
      "delimiter": "-",
      "part_count": 5,
      "project_ids": [
        "5b0f3c3e-1d7a-4d0c-9a51-3f2b8f1e7a10"
      ],
      "created_at": "2026-02-10T08:00:00Z",
      "updated_at": "2026-09-14T15:30:00Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### GET /api/v1/naming-conventions/{naming_convention_id}

Lire une convention de nommage (`getNamingConvention`)

La convention et ses parties dans l'ordre du nom : type (`text`, `number`, `version` calculée), caractère
obligatoire, bornes d'un nombre, longueur maximale, motif (expression régulière) et valeurs permises. Pour une
partie à liste, `allowed_values[].value` est ce qui doit figurer dans le nom.

Scopes : `models:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `naming_convention_id` (obligatoire) | path | string (uuid) | Identifiant de la convention (`id` rendu par `GET /api/v1/naming-conventions`). |

**Réponses**

- `200` La convention. (NamingConvention)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "4d5e6f70-8192-4a3b-9c4d-5e6f708192a3",
  "name": "LNPCA — maquettes",
  "delimiter": "-",
  "part_count": 2,
  "project_ids": [
    "5b0f3c3e-1d7a-4d0c-9a51-3f2b8f1e7a10"
  ],
  "created_at": "2026-02-10T08:00:00Z",
  "updated_at": "2026-09-14T15:30:00Z",
  "parts": [
    {
      "position": 0,
      "name": "Ouvrage",
      "description": "Code de l'ouvrage d'art.",
      "type": "text",
      "required": true,
      "min_value": null,
      "max_value": null,
      "max_length": 6,
      "pattern": "^OA[0-9]+$",
      "allowed_values": []
    },
    {
      "position": 1,
      "name": "Discipline",
      "description": null,
      "type": "text",
      "required": true,
      "min_value": null,
      "max_value": null,
      "max_length": null,
      "pattern": null,
      "allowed_values": [
        {
          "value": "STR",
          "label": "Structure",
          "group": null
        },
        {
          "value": "ARC",
          "label": "Architecture",
          "group": null
        }
      ]
    }
  ]
}
```

### POST /api/v1/naming-conventions/{naming_convention_id}/check

Contrôler des noms de fichiers (`checkFileNames`)

Même contrôle que l'App à l'import d'un fichier, sans rien déposer ni écrire. L'extension est ignorée (`.ifc`,
et la seconde d'un `.ifc.zip`). Chaque nom rend `compliant`, ses problèmes et, s'il est conforme, la valeur lue
pour chaque partie. Codes : `no_rule` (convention sans partie), `part_count` (nombre de parties attendu dans
`expected`), `required`, `not_a_number`, `out_of_range`, `too_long`, `not_in_list`, `pattern`. Un motif invalide ou
trop coûteux à évaluer refuse le nom. 500 noms au plus par appel ; un nom répété n'est contrôlé qu'une fois.

Scopes : `models:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `naming_convention_id` (obligatoire) | path | string (uuid) | Identifiant de la convention (`id` rendu par `GET /api/v1/naming-conventions`). |

**Corps de la requête** (`FileNameCheckRequest`)

- `file_names` (string[], obligatoire)

**Réponses**

- `200` Le résultat de chaque nom, dans l'ordre reçu. (FileNameCheck)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "naming_convention_id": "4d5e6f70-8192-4a3b-9c4d-5e6f708192a3",
  "compliant_count": 1,
  "results": [
    {
      "file_name": "OA12-STR.ifc",
      "compliant": true,
      "problems": [],
      "values": {
        "Ouvrage": "OA12",
        "Discipline": "STR"
      }
    },
    {
      "file_name": "OA12-XYZ.ifc",
      "compliant": false,
      "problems": [
        {
          "code": "not_in_list",
          "part": "Discipline",
          "expected": "STR, ARC"
        }
      ],
      "values": {}
    }
  ]
}
```

## Fichiers

Dépôt de fichiers par URL signée, avant création d'une révision ou d'un jeu d'exigences.

### POST /api/v1/uploads

Ouvrir un dépôt de fichier (`createUpload`)

Renvoie un point d'entrée TUS (reprise possible) et les en-têtes à joindre à chaque requête TUS
(`Authorization`, `Tus-Resumable`, `Upload-Metadata`). Le jeton de dépôt est valable 2 h (`expires_at`) :
au-delà, ouvrir un nouveau dépôt. Le fichier ne transite pas par l'API. Une fois le fichier envoyé, appeler
`complete`. Taille maximale selon le plan : `413` au-delà. Avec `purpose: model`, la clé doit porter
`models:write` ; avec `purpose: ids`, `ids:write`. Extensions : `.ifc` pour une maquette, `.ids` ou `.xlsx`
pour un jeu d'exigences.

Scopes : `models:write`, `ids:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Clé d'idempotence, gardée 24 h par organisation. |

**Corps de la requête** (`UploadCreate`)

- `file_name` (string, obligatoire)
- `size_bytes` (integer, obligatoire) : Taille du fichier en octets. Au-delà de la limite du plan (3 Gio au plus), réponse `413`.
- `purpose` (string, obligatoire ; `model`, `ids`) : `model` pour une maquette IFC, `ids` pour un jeu d'exigences.

**Réponses**

- `201` Dépôt ouvert. (Upload)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `413` Fichier plus gros que la limite du plan. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "7a1c2e3f-4b5d-4e6f-8a9b-0c1d2e3f4a5b",
  "status": "PENDING",
  "file_name": "LNPCA-S1-OA-PONT-0042_C03.ifc",
  "size_bytes": 184467200,
  "purpose": "model",
  "upload_url": "https://ooddxuvlammhnltcfqpm.supabase.co/storage/v1/upload/resumable",
  "upload_headers": {
    "Authorization": "Bearer eyJhbGciOi…",
    "Tus-Resumable": "1.0.0",
    "Upload-Metadata": "bucketName YXR0YWNobWVudHM=,objectName …"
  },
  "expires_at": "2026-10-21T10:00:00Z"
}
```

### POST /api/v1/uploads/{upload_id}/complete

Clore un dépôt de fichier (`completeUpload`)

Vérifie que le fichier est arrivé en entier (taille annoncée). L'empreinte MD5 fournie est conservée avec le
fichier ; elle n'est pas recalculée. `409` si le fichier n'est pas encore complet ou si le dépôt a expiré.
Rappeler `complete` sur un dépôt déjà clos renvoie le même résultat.

Scopes : `models:write`, `ids:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `upload_id` (obligatoire) | path | string (uuid) | Identifiant du dépôt rendu par `POST /api/v1/uploads`. |
| `Idempotency-Key` | header | string | Clé d'idempotence, gardée 24 h par organisation. |

**Corps de la requête**

- `md5_hex` (string) : Empreinte MD5 du fichier envoyé, conservée avec lui (non recalculée).

**Réponses**

- `200` Dépôt clos ; le fichier peut servir à une maquette ou un jeu d'exigences. (Upload)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "7a1c2e3f-4b5d-4e6f-8a9b-0c1d2e3f4a5b",
  "status": "COMPLETE",
  "file_name": "LNPCA-S1-OA-PONT-0042_C03.ifc",
  "size_bytes": 184467200,
  "purpose": "model",
  "upload_url": null,
  "upload_headers": null,
  "expires_at": "2026-10-21T10:00:00Z"
}
```

## IDS

Jeux d'exigences IDS et contrôles de conformité.

### GET /api/v1/revisions/{revision_id}/ids-results

Résultats IDS d'une révision (`listRevisionIdsResults`)

Un résultat par jeu d'exigences lié à la révision, avec le détail par spécification.
Une spécification est comptée dans le score si `applicable` est vrai. Une exigence de présence non
satisfaite a `applicable: true` et `entity_count_applicable: 0` : elle échoue, elle n'est pas « sans objet ».

Scopes : `ids:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `revision_id` (obligatoire) | path | string (uuid) | Identifiant de la révision (`latest_revision_id` d'une maquette, ou `id` d'une révision). |

**Réponses**

- `200` Les résultats. (object)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "reason": null,
  "data": [
    {
      "requirement_set_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
      "requirement_set_revision_id": "3d4e5f6a-7b8c-4d9e-8f1a-2b3c4d5e6f70",
      "status": "COMPLETE",
      "score_percent": 87.5,
      "specification_count": 8,
      "specification_count_applicable": 8,
      "specification_count_failed": 1,
      "completed_at": "2026-10-18T09:02:11Z",
      "specifications": [
        {
          "index": 0,
          "name": "Classification des ouvrages d'art",
          "status": "PARTIAL",
          "applicable": true,
          "score_percent": 91.2,
          "entity_count_applicable": 1250,
          "entity_count_valid": 1140,
          "entity_count_invalid": 110
        },
        {
          "index": 1,
          "name": "Présence d'un IfcSite géoréférencé",
          "status": "FAIL",
          "applicable": true,
          "score_percent": 0,
          "entity_count_applicable": 0,
          "entity_count_valid": 0,
          "entity_count_invalid": 0
        }
      ]
    }
  ]
}
```

### GET /api/v1/revisions/{revision_id}/ids-failures

Échecs IDS détaillés d'une révision pour un jeu d'exigences (`listRevisionIdsFailures`)

Pour chaque spécification en échec (applicable, `FAIL`, `PARTIAL` ou `MISMATCH`) : ce que l'IDS demande
(`applicability`, `requirements`), le genre d'échec (`failure`) et un échantillon d'objets fautifs avec, exigence
par exigence, ce que l'objet porte (`checks`). Le verdict reste celui du contrôle ; `checks` montre seulement les
valeurs lues dans la maquette (une propriété se lit sur l'objet, sinon sur son type).

Une source manquante est dite, jamais remplacée par un vide : `definitions_available` faux si le fichier IDS n'a
pas pu être lu, `details_available` faux si le contrôle n'a pas écrit la liste des objets en échec (contrôle
ancien : le relancer), `object_data_available` faux si le jeu de données de la révision n'existe plus. Tant que
`status` n'est pas `COMPLETE`, `data` est vide.

Scopes : `ids:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `revision_id` (obligatoire) | path | string (uuid) | Identifiant de la révision (`latest_revision_id` d'une maquette, ou `id` d'une révision). |
| `requirement_set_id` (obligatoire) | query | string (uuid) | Jeu d'exigences (`requirement_set_id` d'un résultat IDS de la révision). |
| `requirement_set_revision_id` | query | string (uuid) | Version du jeu ; la plus récente contrôlée sur cette révision par défaut. |
| `specification_index` | query | integer | Une seule spécification (`index` du résultat), en échec ou non. |
| `limit` | query | integer | Spécifications par page. |
| `object_limit` | query | integer | Objets fautifs montrés par spécification ; 0 pour les facettes seules. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |

**Réponses**

- `200` Les échecs détaillés. (IdsFailures)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "revision_id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b",
  "requirement_set_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
  "requirement_set_revision_id": "3d4e5f6a-7b8c-4d9e-8f1a-2b3c4d5e6f70",
  "status": "COMPLETE",
  "completed_at": "2026-10-18T09:02:11Z",
  "definitions_available": true,
  "details_available": true,
  "object_data_available": true,
  "data": [
    {
      "index": 3,
      "name": "Annotation/Beam/Building",
      "description": null,
      "instructions": null,
      "identifier": "r-0e6a1d4ef924",
      "status": "PARTIAL",
      "applicable": true,
      "cardinality": "optional",
      "failure": "INVALID_OBJECTS",
      "score_percent": 41.2,
      "entity_count_applicable": 120,
      "entity_count_invalid": 71,
      "applicability": [
        {
          "type": "ENTITY",
          "entity": "one of IFCBEAM | IFCBUILDING"
        }
      ],
      "requirements": [
        {
          "type": "PROPERTY",
          "cardinality": "required",
          "property_set": "DONNEES_SNCF_RESEAU",
          "name": "ID_LIGNE",
          "value": "pattern L[0-9]{3}",
          "data_type": "IFCTEXT"
        }
      ],
      "invalid_objects": [
        {
          "object_id": 80740,
          "global_id": "1EveRUJ9f8ABmwj$jhNe5l",
          "name": "Poutre P12",
          "ifc_class": "IfcBeam",
          "checks": [
            {
              "requirement": 0,
              "state": "PRESENT",
              "value": "830",
              "value_matches": false,
              "source": "object"
            }
          ]
        }
      ],
      "invalid_objects_total": 71
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### GET /api/v1/revisions/{revision_id}/ids-failures/bcf

Non-conformités IDS d'une révision en fichier BCF (`exportRevisionIdsFailuresBcf`)

Paquet BCF 2.1 (`.bcfzip`) des échecs d'un jeu d'exigences sur la révision : un topic par spécification en
échec, ses objets sélectionnés par GlobalId (2 000 par topic au plus, le reste est compté dans la description).
Une exigence de présence non satisfaite donne un topic sans point de vue. Le fichier ouvre dans Solibri,
BIMcollab, Revit ou Navisworks, et un même export réimporté met à jour ses topics au lieu de les dupliquer.

La réponse est un petit JSON ; le fichier se télécharge par `download_url`, lien signé valable une heure.
`topic_count` à 0 et `download_url` nul : aucune spécification en échec. `details_available` faux : le contrôle
n'a pas écrit la liste des objets testés (contrôle ancien : le relancer). Tant que `status` n'est pas
`COMPLETE`, aucun fichier n'est produit.

Scopes : `ids:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `revision_id` (obligatoire) | path | string (uuid) | Identifiant de la révision (`latest_revision_id` d'une maquette, ou `id` d'une révision). |
| `requirement_set_id` (obligatoire) | query | string (uuid) | Jeu d'exigences (`requirement_set_id` d'un résultat IDS de la révision). |
| `requirement_set_revision_id` | query | string (uuid) | Version du jeu ; la plus récente contrôlée sur cette révision par défaut. |
| `specification_index` | query | integer | Une seule spécification (`index` du résultat) ; toutes celles en échec par défaut. |

**Réponses**

- `200` Le fichier prêt, ou la raison de son absence. (IdsFailuresBcf)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "revision_id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b",
  "requirement_set_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
  "requirement_set_revision_id": "3d4e5f6a-7b8c-4d9e-8f1a-2b3c4d5e6f70",
  "status": "COMPLETE",
  "completed_at": "2026-10-18T09:02:11Z",
  "details_available": true,
  "topic_count": 12,
  "file_name": "non-conformites-ids-LNPCA_New.bcfzip",
  "size_bytes": 48213,
  "download_url": "https://storage.example.com/object/sign/attachments/cache/rev/validity/set/api-failures-all.bcfzip?token=...",
  "download_expires_at": "2026-10-18T10:05:00Z"
}
```

### GET /api/v1/projects/{project_id}/ids-summary

Synthèse IDS d'un projet (dernière révision de chaque maquette) (`getProjectIdsSummary`)

Agrégé côté serveur ; une seule requête, quel que soit le nombre de maquettes.

Scopes : `ids:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `project_id` (obligatoire) | path | string (uuid) | Identifiant du projet (`id` rendu par `GET /api/v1/projects`). |

**Réponses**

- `200` La synthèse. (IdsSummary)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "project_id": "5b0f3c3e-1d7a-4d0c-9a51-3f2b8f1e7a10",
  "model_count": 197,
  "model_count_checked": 190,
  "model_count_in_progress": 5,
  "model_count_error": 2,
  "score_percent": 82.4,
  "by_requirement_set": [
    {
      "requirement_set_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
      "name": "Exigences BIM LNPCA v3",
      "model_count_checked": 190,
      "score_percent": 82.4
    }
  ],
  "computed_at": "2026-10-21T07:00:00Z"
}
```

### POST /api/v1/spaces/{space_id}/requirement-sets

Ranger un jeu d'exigences existant dans un espace (`moveRequirementSetToSpace`)

Un jeu d'exigences vit à un seul niveau (organisation, espace ou projet) : cette opération le range dans l'espace,
comme « Promouvoir » dans l'App. Le jeu n'est pas copié : ses versions, ses liens aux projets et ses résultats
restent les mêmes. Déjà rangé dans cet espace : rendu tel quel. Un jeu de portée organisation est déjà visible
dans tous les espaces ; le ranger dans un seul le retirerait des autres, l'opération répond donc `409`. Pour
créer directement un jeu dans un espace, passer `space_id` à `POST /api/v1/requirement-sets`.

Scopes : `ids:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `space_id` (obligatoire) | path | string (uuid) | Identifiant de l'espace (`id` rendu par `GET /api/v1/spaces`, `space_id` d'un projet). |
| `Idempotency-Key` | header | string | Clé d'idempotence, gardée 24 h par organisation. |

**Corps de la requête** (`SpaceRequirementSet`)

- `requirement_set_id` (string (uuid), obligatoire)

**Réponses**

- `200` Le jeu, rangé dans l'espace. (RequirementSet)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
  "name": "Exigences BIM LNPCA v3",
  "format": "IDS",
  "scope": "SPACE",
  "scope_id": "bd52f342-c3ec-43f8-a3d0-90a7a7c0c269",
  "latest_revision_id": "3d4e5f6a-7b8c-4d9e-8f1a-2b3c4d5e6f70",
  "specification_count": 8,
  "created_at": "2026-02-10T14:00:00Z",
  "updated_at": "2026-11-12T09:00:00Z"
}
```

### GET /api/v1/requirement-sets

Lister les jeux d'exigences IDS (`listRequirementSets`)

Scopes : `ids:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `limit` | query | integer | Taille de page. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |
| `project_id` | query | string (uuid) | Jeux liés à ce projet seulement ; projet inconnu ou d'une autre organisation, 404. |
| `space_id` | query | string (uuid) | Jeux rangés dans cet espace seulement (les jeux de portée organisation, visibles partout, n'y sont pas répétés) ; espace inconnu ou d'une autre organisation, 404. |

**Réponses**

- `200` Une page de jeux d'exigences. (Page & object)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "data": [
    {
      "id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
      "name": "Exigences BIM LNPCA v3",
      "format": "IDS",
      "scope": "PROJECT",
      "scope_id": "5b0f3c3e-1d7a-4d0c-9a51-3f2b8f1e7a10",
      "latest_revision_id": "3d4e5f6a-7b8c-4d9e-8f1a-2b3c4d5e6f70",
      "specification_count": 8,
      "created_at": "2026-02-10T14:00:00Z",
      "updated_at": "2026-09-30T11:20:00Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### POST /api/v1/requirement-sets

Créer un jeu d'exigences depuis un fichier IDS déposé (`createRequirementSet`)

Portée du jeu : le projet `project_id` (et lié à lui), l'espace `space_id`, ou l'organisation sans l'un ni
l'autre (le créateur de la clé doit alors être administrateur de l'organisation). `project_id` et `space_id`
ensemble : `400`. Projet ou espace inconnu ou d'une autre organisation : `404`.

Scopes : `ids:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Clé d'idempotence, gardée 24 h par organisation. |

**Corps de la requête** (`RequirementSetCreate`)

- `name` (string, obligatoire)
- `upload_id` (string (uuid), obligatoire) : Dépôt clos (`purpose: ids`), fichier .ids ou .xlsx.
- `project_id` (string (uuid)) : Lie le jeu à ce projet ; sinon portée organisation.
- `space_id` (string (uuid)) : Range le jeu dans cet espace (portée espace) ; exclusif de `project_id`.

**Réponses**

- `201` Jeu créé. (RequirementSet)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
  "name": "Exigences BIM LNPCA v3",
  "format": "IDS",
  "scope": "PROJECT",
  "scope_id": "5b0f3c3e-1d7a-4d0c-9a51-3f2b8f1e7a10",
  "latest_revision_id": "3d4e5f6a-7b8c-4d9e-8f1a-2b3c4d5e6f70",
  "specification_count": 8,
  "created_at": "2026-10-21T08:00:00Z",
  "updated_at": "2026-10-21T08:00:00Z"
}
```

### GET /api/v1/requirement-sets/{requirement_set_id}

Lire un jeu d'exigences (`getRequirementSet`)

Scopes : `ids:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `requirement_set_id` (obligatoire) | path | string (uuid) | Identifiant du jeu d'exigences (`id` rendu par `GET /api/v1/requirement-sets`). |

**Réponses**

- `200` Le jeu d'exigences. (RequirementSet)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
  "name": "Exigences BIM LNPCA v3",
  "format": "IDS",
  "scope": "PROJECT",
  "scope_id": "5b0f3c3e-1d7a-4d0c-9a51-3f2b8f1e7a10",
  "latest_revision_id": "3d4e5f6a-7b8c-4d9e-8f1a-2b3c4d5e6f70",
  "specification_count": 8,
  "created_at": "2026-02-10T14:00:00Z",
  "updated_at": "2026-09-30T11:20:00Z"
}
```

### DELETE /api/v1/requirement-sets/{requirement_set_id}

Supprimer un jeu d'exigences (`deleteRequirementSet`)

**Définitif**, comme la suppression dans l'App : le jeu, toutes ses versions, ses liens aux projets et maquettes et
tous les résultats IDS obtenus avec lui sont effacés, sans restauration possible. Exige `confirm=true`. Refusé
(`409`) tant qu'un contrôle de ce jeu est en cours.

Scopes : `ids:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `requirement_set_id` (obligatoire) | path | string (uuid) | Identifiant du jeu d'exigences (`id` rendu par `GET /api/v1/requirement-sets`). |
| `confirm` (obligatoire) | query | string | `true` : confirme la suppression définitive. |

**Réponses**

- `204` Jeu supprimé.
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

### GET /api/v1/requirement-sets/{requirement_set_id}/specifications

Lire ce que demande un jeu d'exigences (`listRequirementSetSpecifications`)

Spécifications du fichier IDS, dans l'ordre du contrôle (`index` = position des résultats IDS) : applicabilité
(quels objets sont visés) et exigences (propriétés, attributs, classifications, matériaux attendus, avec leur
valeur ou leur contrainte). Ne demande ni maquette ni contrôle.

`specifications_available` faux : le fichier de cette version n'est pas un IDS lisible (`data` vide), à ne pas
confondre avec un jeu sans spécification.

Scopes : `ids:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `requirement_set_id` (obligatoire) | path | string (uuid) | Identifiant du jeu d'exigences (`id` rendu par `GET /api/v1/requirement-sets`). |
| `requirement_set_revision_id` | query | string (uuid) | Version du jeu (`requirement_set_revision_id` d'un résultat IDS) ; la plus récente par défaut. |
| `limit` | query | integer | Taille de page. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |

**Réponses**

- `200` Une page de spécifications. (RequirementSetSpecifications)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "requirement_set_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
  "requirement_set_revision_id": "3d4e5f6a-7b8c-4d9e-8f1a-2b3c4d5e6f70",
  "version": 3,
  "name": "Exigences BIM LNPCA v3",
  "format": "IDS",
  "specifications_available": true,
  "specification_count": 8,
  "data": [
    {
      "index": 0,
      "name": "Murs - résistance au feu",
      "description": "Chaque mur porte sa résistance au feu.",
      "instructions": null,
      "identifier": "EX-012",
      "cardinality": "required",
      "applicability": [
        {
          "type": "ENTITY",
          "entity": "IFCWALL"
        }
      ],
      "requirements": [
        {
          "type": "PROPERTY",
          "cardinality": "required",
          "property_set": "Pset_WallCommon",
          "name": "FireRating",
          "value": "one of REI30 | REI60 | REI120",
          "data_type": "IFCLABEL"
        }
      ]
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### POST /api/v1/ids-checks

Contrôler une révision avec un jeu d'exigences (`createIdsCheck`)

Lance le contrôle (ou le relance si la paire a déjà un résultat). Si le jeu n'est rattaché ni à la maquette
ni à un projet qui la contient, il est rattaché à la maquette, comme depuis l'application : ses autres
révisions seront contrôlées aussi. Le jeu contrôlé est la version épinglée par le rattachement, sinon la
dernière. Le résultat se lit ensuite dans `GET /api/v1/revisions/{revision_id}/ids-results`. Compte dans le
quota mensuel de contrôles du plan. `400` si la révision ou le jeu est inconnu dans l'organisation, `409`
si le jeu n'a aucune révision ou si la conversion de la révision a échoué.

Scopes : `ids:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Clé d'idempotence, gardée 24 h par organisation. |

**Corps de la requête** (`IdsCheckCreate`)

- `revision_id` (string (uuid), obligatoire)
- `requirement_set_id` (string (uuid), obligatoire)

**Réponses**

- `202` Traitement accepté ; suivre le job. (Job)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "1f2e3d4c-5b6a-4978-8a1b-2c3d4e5f6a7b",
  "type": "conversion",
  "status": "QUEUED",
  "percent_complete": 0,
  "eta_seconds": null,
  "target": {
    "type": "revision",
    "id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b"
  },
  "result": null,
  "error": null,
  "created_at": "2026-10-21T07:58:00Z",
  "started_at": null,
  "completed_at": null
}
```

## Exports

Export des données d'une révision.

### POST /api/v1/revisions/{revision_id}/exports

Exporter les données d'une révision (`createRevisionExport`)

Prépare une archive zip, un fichier par table (CSV par défaut, ou JSON avec `format: json`). Le jeu de
données de la révision doit être terminé (`409` sinon). Quand le job est `COMPLETE`, son `result` donne une
URL de téléchargement signée valable une heure, renouvelée à chaque lecture du job ; l'archive est gardée
48 h. Une table en échec est absente de l'archive et nommée dans `error` (code `EXPORT_PARTIAL`).

**Format des fichiers.** CSV : UTF-8 sans BOM, séparateur `|`, chaque valeur entre guillemets, fin de ligne CRLF,
une ligne d'en-tête. Les colonnes de jointure y figurent sous leurs deux noms (`asset_id` et `AssetId`, `name` et
`Name`…), et `RevisionId` peut figurer deux fois, comme le lisent les modèles Power BI. JSON : un tableau d'objets,
chaque colonne une seule fois, sous son nom en PascalCase. Dans les deux formats toute valeur est du **texte**
(nombres, booléens `true`/`false`, dates) : la convertir avant de calculer ou de trier. Une table sans ligne pour
la révision donne un fichier vide (en-tête seul, ou `[]`).

**Tables** (nom dans `tables` → fichier de l'archive). `AssetId` est l'identifiant de l'objet dans le fichier IFC
(`#123`) et relie les tables entre elles ; `GlobalId` est l'identifiant IFC stable d'une révision à l'autre.

`assets` (fichier `assets`), une ligne par objet : `AssetId`, `GlobalId`, `AssetClass`, `AssetClassName`, `Name`, `Description`, `ObjectType`, `Tag`, `LongName`, `PredefinedType`, `AssetTypeId`, `IsAsset`, `HasGeometry`, `IsSpatialAsset`, `IsGroupAsset`.

`asset_types` (fichier `asset-types`), une ligne par type d'objet : `AssetTypeId`, `AssetTypeClass`, `AssetTypeClassName`, `AssetTypeGlobalId`, `AssetTypeName`, `AssetTypePredefinedType`.

`properties` (fichier `properties`), une ligne par propriété d'un objet : `AssetId`, `AssetClassName`, `PropertySetName`, `Name`, `Value`, `Unit`, `Source`.

`qto` (fichier `qto`), une ligne par quantité d'un objet : `AssetId`, `QuantitySetName`, `Name`, `Value`, `Unit`, `Formula`.

`classification` (fichier `classification`), une ligne par classification d'un objet : `AssetId`, `ClassificationSystem`, `ClassificationIdentification`, `ClassificationValue`, `ClassificationTitle`.

`materials` (fichier `materials`), une ligne par matériau (couche, profil ou constituant) d'un objet : `AssetId`, `MaterialName`, `MaterialCategory`, `MaterialLayerName`, `MaterialLayerThickness`, `MaterialConstituentName`, `MaterialConstituentFraction`, `Source`.

`spatial_relation` (fichier `asset-spatial-parents`), une ligne par objet, avec son emplacement : `AssetId`, `AssetName`, `SiteAssetName`, `BuildingAssetName`, `BuildingStoreyAssetName`, `SpaceAssetName`, `FirstSpatialParentAssetName`.

`asset_tree` (fichier `asset-relations`), une ligne par lien parent → enfant de l'arborescence : `AssetId`, `ChildAssetId`, `ParentAssetId`, `ParentAssetClassName`, `RelationClass`, `ParentAssetIsSpatial`, `ParentAssetIsGroup`.

`asset_logical` (fichier `asset-logical-parents`), une ligne par objet, avec son système, sa zone, son groupe : `AssetId`, `AssetName`, `SystemAssetName`, `ZoneAssetName`, `GroupAssetName`.

`asset_relations` (fichier `asset-relations-direct`), une ligne par relation IFC d'un objet : `AssetId`, `RelationId`, `RelationClass`.

`relations` (fichier `relations`), une ligne par relation IFC : `RelationId`, `RelationClass`, `RelationClassName`, `RelationGlobalId`, `RelationName`.

`space_boundary_relation` (fichier `space-boundary-relations`), une ligne par limite d'un espace : `AssetId`, `RelatingSpaceAssetId`, `RelatingSpaceAssetClassName`, `PhysicalOrVirtualBoundary`, `InternalOrExternalBoundary`.

`asset_placement` (fichier `asset-placement`), une ligne par objet placé : `AssetId`, `X`, `Y`, `Z`, `Unit`, `Xm`, `Ym`, `Zm` (en mètres).

`revisions` (fichier `revisions`), une ligne par révision : `Model Name`, `Schema`, `FileName`, `FileSize`, `NumberOfAssets`, `NumberOfSpaces`, `NumberOfProxies`, `NumberOfProperties`, `GlobalArea`, `SitePointX`/`Y`/`Z`, adresse du bâtiment.

Chaque fichier commence par `RevisionId`. La liste complète des colonnes d'une table est l'en-tête de son fichier.

Scopes : `exports:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `revision_id` (obligatoire) | path | string (uuid) | Identifiant de la révision (`latest_revision_id` d'une maquette, ou `id` d'une révision). |
| `Idempotency-Key` | header | string | Clé d'idempotence, gardée 24 h par organisation. |

**Corps de la requête** (`ExportCreate`)

- `tables` (string[]) : Tables à inclure ; toutes par défaut.
- `format` (string ; `csv`, `json`) : Format des fichiers de l'archive.

**Réponses**

- `202` Traitement accepté ; suivre le job. (Job)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "1f2e3d4c-5b6a-4978-8a1b-2c3d4e5f6a7b",
  "type": "conversion",
  "status": "QUEUED",
  "percent_complete": 0,
  "eta_seconds": null,
  "target": {
    "type": "revision",
    "id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b"
  },
  "result": null,
  "error": null,
  "created_at": "2026-10-21T07:58:00Z",
  "started_at": null,
  "completed_at": null
}
```

## Visa

Grille de visa des projets et fiches de visa des maquettes, en lecture.

### GET /api/v1/projects/{project_id}/visa-grid

Lire la grille de visa d'un projet (`getVisaGrid`)

Critères contrôlés au visa des maquettes du projet, par section (`DIFFUSION`, `STRUCTURATION`, `REFERENCE`,
`MODELISATION`, `DATA`) et par phase, avec leur coefficient (1 à 3) et le contrôle automatique qui propose un
statut (`auto_check`), plus les pièces attendues à chaque diffusion. `customized` est faux tant que le projet garde
la grille par défaut. La grille se modifie dans l'App.

Scopes : `projects:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `project_id` (obligatoire) | path | string (uuid) | Identifiant du projet (`id` rendu par `GET /api/v1/projects`). |

**Réponses**

- `200` La grille. (VisaGrid)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "project_id": "5b0f3c3e-1d7a-4d0c-9a51-3f2b8f1e7a10",
  "customized": true,
  "criteria": [
    {
      "id": "naming",
      "section": "DIFFUSION",
      "label": "Nom du fichier conforme à la convention",
      "scope": "ALL",
      "origin": "AUTO",
      "auto_check": "file_naming",
      "weight": 2,
      "phases": []
    }
  ],
  "pieces": [
    {
      "id": "ifc",
      "label": "Maquette IFC",
      "pattern": null
    },
    {
      "id": "note",
      "label": "Note de modélisation",
      "pattern": "*NOTE*.pdf"
    }
  ],
  "file_name_pattern": null,
  "max_file_size_mb": 200,
  "updated_at": "2026-10-12T09:00:00Z",
  "updated_by": "julie.martin@exemple.fr"
}
```

### GET /api/v1/projects/{project_id}/visa-sheets

Lister les fiches de visa d'un projet (`listVisaSheets`)

Une fiche par révision de maquette visée, des plus récentes aux plus anciennes : phase, numéro de diffusion,
état (`DRAFT` brouillon, `SIGNED` signée), verdict (`ACCEPTED`, `ACCEPTED_WITH_OBS`, `REFUSED`) et taux de
conformité `score` entre 0 et 1 (VSO 1, VAO 0,5, refusé 0, pondérés ; nul si aucun critère n'est évalué). Le
détail des critères se lit avec `GET /api/v1/projects/{project_id}/visa-sheets/{visa_sheet_id}`.

Scopes : `projects:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `project_id` (obligatoire) | path | string (uuid) | Identifiant du projet (`id` rendu par `GET /api/v1/projects`). |
| `limit` | query | integer | Taille de page. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |
| `model_id` | query | string (uuid) | Maquette visée. |
| `status` | query | string | `DRAFT` ou `SIGNED`. |

**Réponses**

- `200` Une page de fiches. (Page & object)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "data": [
    {
      "id": "8e9f0a1b-2c3d-4e5f-8a6b-7c8d9e0f1a2b",
      "project_id": "5b0f3c3e-1d7a-4d0c-9a51-3f2b8f1e7a10",
      "model_id": "7c2e9f10-3b4a-4d5e-8f6a-1b2c3d4e5f60",
      "revision_id": "1f2e3d4c-5b6a-4978-8695-a4b3c2d1e0f9",
      "revision_version": 3,
      "diffusion_no": 2,
      "phase": "APD",
      "status": "SIGNED",
      "verdict": "ACCEPTED_WITH_OBS",
      "score": 0.82,
      "created_by": "julie.martin@exemple.fr",
      "created_at": "2026-10-20T08:00:00Z",
      "updated_at": "2026-10-21T16:30:00Z",
      "signed_by": "julie.martin@exemple.fr",
      "signed_at": "2026-10-21T16:30:00Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### GET /api/v1/projects/{project_id}/visa-sheets/{visa_sheet_id}

Lire une fiche de visa (`getVisaSheet`)

La fiche avec chaque critère évalué : statut (`VSO` visa sans observation, `VAO` avec observations, `REFUSE`,
`NC` non contrôlé, `NA` non applicable ; nul tant qu'il n'est pas évalué), commentaire, proposition du contrôle
automatique (`auto_proposal`, `auto_summary`), pièces trouvées ou manquantes, commentaire global, historique
(création, signature, réouverture avec son motif) et fiche de la diffusion précédente de la même maquette.

Scopes : `projects:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `project_id` (obligatoire) | path | string (uuid) | Identifiant du projet (`id` rendu par `GET /api/v1/projects`). |
| `visa_sheet_id` (obligatoire) | path | string (uuid) | Identifiant de la fiche (`id` rendu par `GET /api/v1/projects/{project_id}/visa-sheets`). |

**Réponses**

- `200` La fiche. (VisaSheet)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "8e9f0a1b-2c3d-4e5f-8a6b-7c8d9e0f1a2b",
  "project_id": "5b0f3c3e-1d7a-4d0c-9a51-3f2b8f1e7a10",
  "model_id": "7c2e9f10-3b4a-4d5e-8f6a-1b2c3d4e5f60",
  "revision_id": "1f2e3d4c-5b6a-4978-8695-a4b3c2d1e0f9",
  "revision_version": 3,
  "diffusion_no": 2,
  "phase": "APD",
  "status": "SIGNED",
  "verdict": "ACCEPTED_WITH_OBS",
  "score": 0.75,
  "created_by": "julie.martin@exemple.fr",
  "created_at": "2026-10-20T08:00:00Z",
  "updated_at": "2026-10-21T16:30:00Z",
  "signed_by": "julie.martin@exemple.fr",
  "signed_at": "2026-10-21T16:30:00Z",
  "global_comment": "Reprendre les niveaux avant la diffusion PRO.",
  "items": [
    {
      "criterion_id": "naming",
      "section": "DIFFUSION",
      "label": "Nom du fichier conforme à la convention",
      "scope": "ALL",
      "origin": "AUTO",
      "auto_check": "file_naming",
      "weight": 2,
      "status": "VAO",
      "comment": "Discipline absente du nom.",
      "auto_proposal": "VAO",
      "auto_summary": "1 problème de nommage",
      "evidence": null
    }
  ],
  "pieces": [
    {
      "piece_id": "ifc",
      "label": "Maquette IFC",
      "present": true,
      "source": "AUTO",
      "detail": null
    }
  ],
  "history": [
    {
      "action": "CREATED",
      "by": "julie.martin@exemple.fr",
      "at": "2026-10-20T08:00:00Z",
      "reason": null
    },
    {
      "action": "SIGNED",
      "by": "julie.martin@exemple.fr",
      "at": "2026-10-21T16:30:00Z",
      "reason": null
    }
  ],
  "previous_sheet_id": null
}
```

## Documents

Documents de la bibliothèque visibles depuis un projet, leurs versions et leurs fichiers, en lecture.

### GET /api/v1/projects/{project_id}/documents

Lister les documents visibles depuis un projet (`listProjectDocuments`)

Les documents de la bibliothèque que l'App montre depuis ce projet, des plus récents aux plus anciens : ceux de
l'organisation (`level: organization`), ceux de l'espace du projet ou d'un espace parent (`space`), et ceux du
projet lui-même (`project`). Chaque document décrit le fichier de sa version la plus récente ; l'historique des
versions se lit avec `GET /api/v1/documents/{document_id}` et le fichier avec
`GET /api/v1/documents/{document_id}/download`.

Scopes : `projects:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `project_id` (obligatoire) | path | string (uuid) | Identifiant du projet (`id` rendu par `GET /api/v1/projects`). |
| `limit` | query | integer | Taille de page. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |

**Réponses**

- `200` Une page de documents. (Page & object)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "data": [
    {
      "id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
      "name": "Plan RDC",
      "description": "Plan du rez-de-chaussée, indice B.",
      "type": "OTHER",
      "level": "space",
      "space_id": "0b8a2c47-5a0e-4a7e-8f0d-0c6d2b7e9c31",
      "project_id": null,
      "version": 2,
      "version_count": 2,
      "file_name": "plan-rdc-B.pdf",
      "content_type": "application/pdf",
      "size_bytes": 482113,
      "created_by": "julie.martin@exemple.fr",
      "created_at": "2026-10-20T08:00:00Z",
      "modified_by": "julie.martin@exemple.fr",
      "modified_at": "2026-10-22T10:15:00Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### GET /api/v1/documents/{document_id}

Lire un document et ses versions (`getDocument`)

Le document avec toutes ses versions, de la plus récente à la plus ancienne : numéro, nom du fichier, type de
contenu, taille, auteur et date de dépôt. Seuls les documents de l'organisation de la clé sont lus.

Scopes : `projects:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `document_id` (obligatoire) | path | string (uuid) | Identifiant du document (`id` rendu par `GET /api/v1/projects/{project_id}/documents`). |

**Réponses**

- `200` Le document. (Document)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
  "name": "Plan RDC",
  "description": "Plan du rez-de-chaussée, indice B.",
  "type": "OTHER",
  "level": "space",
  "space_id": "0b8a2c47-5a0e-4a7e-8f0d-0c6d2b7e9c31",
  "project_id": null,
  "version": 2,
  "version_count": 2,
  "file_name": "plan-rdc-B.pdf",
  "content_type": "application/pdf",
  "size_bytes": 482113,
  "created_by": "julie.martin@exemple.fr",
  "created_at": "2026-10-20T08:00:00Z",
  "modified_by": "julie.martin@exemple.fr",
  "modified_at": "2026-10-22T10:15:00Z",
  "versions": [
    {
      "id": "7b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e",
      "version": 2,
      "file_name": "plan-rdc-B.pdf",
      "content_type": "application/pdf",
      "size_bytes": 482113,
      "created_by": "julie.martin@exemple.fr",
      "created_at": "2026-10-22T10:15:00Z"
    },
    {
      "id": "8c9d0e1f-2a3b-4c4d-8e5f-6a7b8c9d0e1f",
      "version": 1,
      "file_name": "plan-rdc-A.pdf",
      "content_type": "application/pdf",
      "size_bytes": 471020,
      "created_by": "julie.martin@exemple.fr",
      "created_at": "2026-10-20T08:00:00Z"
    }
  ]
}
```

### GET /api/v1/documents/{document_id}/download

Obtenir un lien de téléchargement d'un document (`downloadDocument`)

Un lien signé vers le fichier d'une version, la plus récente par défaut, valable une heure. Le lien s'ouvre sans
clé d'API ; ne le transmettez qu'aux personnes qui peuvent lire ce document. Un fichier de l'ancien stockage, qui
ne peut pas être servi par un lien, répond 409 : il se télécharge depuis l'App.

Scopes : `projects:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `document_id` (obligatoire) | path | string (uuid) | Identifiant du document (`id` rendu par `GET /api/v1/projects/{project_id}/documents`). |
| `version` | query | integer | Numéro de version (`version` d'une entrée de `versions`) ; la plus récente par défaut. |

**Réponses**

- `200` Le lien et la description du fichier. (DocumentDownload)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "document_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
  "version": 2,
  "file_name": "plan-rdc-B.pdf",
  "content_type": "application/pdf",
  "size_bytes": 482113,
  "download_url": "https://storage.example.com/object/sign/attachments/plan-rdc-B.pdf?token=...&download=plan-rdc-B.pdf",
  "download_expires_at": "2026-10-22T11:15:00Z"
}
```

## Kairnial

Projets du connecteur Kairnial et exports zip de leurs fichiers.

### GET /api/v1/kairnial-projects

Lister les projets Kairnial accessibles (`listKairnialProjects`)

Les projets Kairnial que le créateur de la clé voit dans le connecteur Kairnial de l'App, par nom. `file_count` et
`size_bytes` comptent la dernière version de chaque fichier, d'une seule extension si `extension` est donnée
(par exemple `dwg`). La liste vient de la dernière synchronisation du connecteur.

Scopes : `projects:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `limit` | query | integer | Taille de page. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |
| `extension` | query | string | Extension de fichier comptée, sans le point (`dwg`, `pdf`…). |

**Réponses**

- `200` Une page de projets Kairnial. (Page & object)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "data": [
    {
      "project_key": "hopital",
      "name": "SAS BIM - HOPITAL",
      "file_count": 16,
      "size_bytes": 143276544
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### POST /api/v1/kairnial-exports

Exporter en zip des fichiers d'un projet Kairnial (`createKairnialExport`)

Rassemble dans un ou plusieurs zips la dernière version de chaque fichier d'un projet Kairnial ayant l'une des
extensions demandées, dans tout le projet ou dans un dossier et ses sous-dossiers (`folder_id`). Dans le zip,
chaque fichier garde le chemin de son dossier Kairnial. Les zips sont coupés vers 100 Mo.

Kairnial ne sert un fichier qu'avec la connexion d'un utilisateur : l'export tourne avec la connexion Kairnial du
créateur de la clé (`409` s'il n'en a pas de valide) et ne voit que ses projets. Le travail est asynchrone :
suivre `GET /api/v1/kairnial-exports/{export_id}` jusqu'à `COMPLETE`. Un seul export à la fois par
organisation (`409` sinon) ; 5 000 fichiers et 20 Go au plus par export. Un fichier que Kairnial ne rend pas est
écarté et nommé dans `skipped`. Les zips sont gardés 7 jours.

Scopes : `projects:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Clé d'idempotence, gardée 24 h par organisation. |

**Corps de la requête** (`KairnialExportCreate`)

- `project_key` (string, obligatoire) : `project_key` rendu par `GET /api/v1/kairnial-projects`.
- `extensions` (string[], obligatoire) : Extensions retenues, sans le point et sans tenir compte de la casse.
- `folder_id` (string | null) : Dossier Kairnial ; ses sous-dossiers sont compris. Tout le projet par défaut.

**Réponses**

- `202` Export mis en file ; le suivre par son `Location`. (KairnialExport)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "2b3c4d5e-6f70-4a81-9b2c-3d4e5f6a7b8c",
  "project_key": "hopital",
  "project_name": "SAS BIM - HOPITAL",
  "extensions": [
    "dwg"
  ],
  "folder_id": null,
  "status": "QUEUED",
  "file_count": 16,
  "done_count": 0,
  "total_bytes": 143276544,
  "parts": [],
  "skipped": [],
  "error": null,
  "created_at": "2026-11-05T10:02:00Z",
  "started_at": null,
  "completed_at": null,
  "expires_at": null
}
```

### GET /api/v1/kairnial-exports/{export_id}

Suivre un export Kairnial (`getKairnialExport`)

L'état de l'export (`QUEUED`, `PROCESSING`, `COMPLETE`, `ERROR`, `EXPIRED` après 7 jours), sa progression
(`done_count` sur `file_count`) et, une fois `COMPLETE`, ses zips avec un lien signé chacun, valable une heure et
renouvelé à chaque lecture. Les liens s'ouvrent sans clé d'API.

Scopes : `projects:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `export_id` (obligatoire) | path | string (uuid) | Identifiant de l'export (`id` rendu par `POST /api/v1/kairnial-exports`). |

**Réponses**

- `200` L'export. (KairnialExport)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "2b3c4d5e-6f70-4a81-9b2c-3d4e5f6a7b8c",
  "project_key": "hopital",
  "project_name": "SAS BIM - HOPITAL",
  "extensions": [
    "dwg"
  ],
  "folder_id": null,
  "status": "COMPLETE",
  "file_count": 16,
  "done_count": 16,
  "total_bytes": 143276544,
  "parts": [
    {
      "index": 1,
      "file_name": "kairnial-sas-bim-hopital-dwg-1.zip",
      "file_count": 11,
      "size_bytes": 98213450,
      "download_url": "https://storage.example.com/object/sign/attachments/kairnial-exports/kairnial-sas-bim-hopital-dwg-1.zip?token=...",
      "download_expires_at": "2026-11-05T11:15:00Z"
    },
    {
      "index": 2,
      "file_name": "kairnial-sas-bim-hopital-dwg-2.zip",
      "file_count": 5,
      "size_bytes": 40117231,
      "download_url": "https://storage.example.com/object/sign/attachments/kairnial-exports/kairnial-sas-bim-hopital-dwg-2.zip?token=...",
      "download_expires_at": "2026-11-05T11:15:00Z"
    }
  ],
  "skipped": [],
  "error": null,
  "created_at": "2026-11-05T10:02:00Z",
  "started_at": "2026-11-05T10:02:01Z",
  "completed_at": "2026-11-05T10:06:40Z",
  "expires_at": "2026-11-12T10:06:40Z"
}
```

## Issues

Points à traiter sur les maquettes (issues BCF de l'App) et leurs commentaires.

### GET /api/v1/issue-boards

Lister les tableaux d'issues (`listIssueBoards`)

Un tableau par espace racine de l'organisation (`source` `seveup`, modifiable), plus les projets Trimble Connect
ou Catenda synchronisés (`trimble`, `catenda`), en lecture seule ici comme dans l'App : leurs issues se modifient
dans l'outil d'origine. Chaque tableau donne les statuts, types et priorités acceptés par `PATCH
/api/v1/issues/{issue_id}`. Liste complète en une page.

Scopes : `issues:read`

**Réponses**

- `200` Les tableaux d'issues. (Page & object)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "data": [
    {
      "id": "3e7d1c55-8a2b-4f0e-9c6d-1b2a3c4d5e6f",
      "name": "SNCF",
      "source": "seveup",
      "space_id": "0b8a2c47-5a0e-4a7e-8f0d-0c6d2b7e9c31",
      "editable": true,
      "statuses": [
        {
          "name": "Ouvert",
          "type": "open"
        },
        {
          "name": "En cours",
          "type": "open"
        },
        {
          "name": "Résolu",
          "type": "open"
        },
        {
          "name": "Fermé",
          "type": "closed"
        }
      ],
      "types": [
        "Clash",
        "Question",
        "Remarque"
      ],
      "priorities": [
        "Basse",
        "Normale",
        "Haute",
        "Critique"
      ]
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### GET /api/v1/issues

Lister les issues (`listIssues`)

Issues non supprimées de l'organisation, des plus anciennes aux plus récentes. `board_id` limite à un tableau,
`model_id` aux issues posées sur une maquette, `status_type` aux issues ouvertes (`open`) ou fermées (`closed`)
quel que soit le nom du statut ; une issue « Résolu », en attente de clôture, est ouverte. La liste ne porte pas les commentaires (`comment_count` seulement) : les lire
avec `GET /api/v1/issues/{issue_id}`.

Scopes : `issues:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `limit` | query | integer | Taille de page. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |
| `board_id` | query | string (uuid) | Tableau (`id` rendu par `GET /api/v1/issue-boards`). |
| `model_id` | query | string (uuid) | Maquette sur laquelle l'issue est posée. |
| `status_type` | query | string | `open` (statuts ouverts, en cours et résolus en attente de clôture) ou `closed`. |

**Réponses**

- `200` Une page d'issues. (Page & object)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "data": [
    {
      "id": "9a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
      "board_id": "3e7d1c55-8a2b-4f0e-9c6d-1b2a3c4d5e6f",
      "ref": "#12",
      "title": "Pset_WallCommon absent sur les murs du niveau R+1",
      "description": "Compléter IsExternal et FireRating.",
      "status": "Ouvert",
      "status_type": "open",
      "type": "Clash",
      "priority": "Haute",
      "assigned_to": "bet-structure@exemple.fr",
      "due_date": "2026-11-20T00:00:00Z",
      "storey": "R+1",
      "models": [
        {
          "model_id": "7c2e9f10-3b4a-4d5e-8f6a-1b2c3d4e5f60",
          "revision_id": "1f2e3d4c-5b6a-4978-8695-a4b3c2d1e0f9"
        }
      ],
      "comment_count": 2,
      "viewpoint_count": 1,
      "editable": true,
      "created_by": "julie.martin@exemple.fr",
      "created_at": "2026-11-02T10:15:00Z",
      "modified_by": "julie.martin@exemple.fr",
      "modified_at": "2026-11-03T08:40:00Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### POST /api/v1/issues

Créer une issue (`createIssue`)

Comme « Nouvelle issue » dans le viewer, sans point de vue 3D : l'issue va dans le tableau de l'espace racine de
la maquette et pointe la révision `revision_id` (la dernière sinon). Statut ouvert du tableau ; `type` et
`priority` prennent ceux du tableau, `Remarque` et `Normale` par défaut. Signée du créateur de la clé.

Scopes : `issues:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Clé d'idempotence, gardée 24 h par organisation. |

**Corps de la requête** (`IssueCreate`)

- `model_id` (string (uuid), obligatoire)
- `revision_id` (string (uuid) | null) : Révision de cette maquette ; la dernière si absent.
- `title` (string, obligatoire)
- `description` (string | null)
- `type` (string | null) : Nom d'un type du tableau ; `Remarque` si absent.
- `priority` (string | null ; `Basse`, `Normale`, `Haute`, `Critique`, `null`)
- `assigned_to` (string | null)
- `due_date` (string (date-time) | null)

**Réponses**

- `201` Issue créée. (Issue)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "9a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
  "board_id": "3e7d1c55-8a2b-4f0e-9c6d-1b2a3c4d5e6f",
  "ref": "#12",
  "title": "Pset_WallCommon absent sur les murs du niveau R+1",
  "description": "Compléter IsExternal et FireRating.",
  "status": "Ouvert",
  "status_type": "open",
  "type": "Clash",
  "priority": "Haute",
  "assigned_to": "bet-structure@exemple.fr",
  "due_date": "2026-11-20T00:00:00Z",
  "storey": null,
  "models": [
    {
      "model_id": "7c2e9f10-3b4a-4d5e-8f6a-1b2c3d4e5f60",
      "revision_id": "1f2e3d4c-5b6a-4978-8695-a4b3c2d1e0f9"
    }
  ],
  "comment_count": 0,
  "viewpoint_count": 0,
  "editable": true,
  "created_by": "julie.martin@exemple.fr",
  "created_at": "2026-11-04T09:00:00Z",
  "modified_by": "julie.martin@exemple.fr",
  "modified_at": "2026-11-04T09:00:00Z",
  "comments": []
}
```

### GET /api/v1/issues/{issue_id}

Lire une issue (`getIssue`)

L'issue avec ses commentaires, du plus ancien au plus récent.

Scopes : `issues:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `issue_id` (obligatoire) | path | string (uuid) | Identifiant de l'issue (`id` rendu par `GET /api/v1/issues`). |

**Réponses**

- `200` L'issue. (Issue)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "9a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
  "board_id": "3e7d1c55-8a2b-4f0e-9c6d-1b2a3c4d5e6f",
  "ref": "#12",
  "title": "Pset_WallCommon absent sur les murs du niveau R+1",
  "description": "Compléter IsExternal et FireRating.",
  "status": "En cours",
  "status_type": "open",
  "type": "Clash",
  "priority": "Haute",
  "assigned_to": "bet-structure@exemple.fr",
  "due_date": "2026-11-20T00:00:00Z",
  "storey": "R+1",
  "models": [
    {
      "model_id": "7c2e9f10-3b4a-4d5e-8f6a-1b2c3d4e5f60",
      "revision_id": "1f2e3d4c-5b6a-4978-8695-a4b3c2d1e0f9"
    }
  ],
  "comment_count": 1,
  "viewpoint_count": 1,
  "editable": true,
  "created_by": "julie.martin@exemple.fr",
  "created_at": "2026-11-02T10:15:00Z",
  "modified_by": "bet-structure@exemple.fr",
  "modified_at": "2026-11-03T08:40:00Z",
  "comments": [
    {
      "id": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
      "text": "Corrigé dans la prochaine révision.",
      "created_by": "bet-structure@exemple.fr",
      "created_at": "2026-11-03T08:40:00Z"
    }
  ]
}
```

### PATCH /api/v1/issues/{issue_id}

Modifier une issue (`updateIssue`)

Seuls les champs présents changent ; `null` vide `description`, `assigned_to` ou `due_date`. `status` et `type`
se donnent par leur nom, parmi ceux du tableau (`GET /api/v1/issue-boards`), sans tenir compte de la casse.
Chaque changement entre dans l'historique de l'issue, comme depuis l'App. Une issue d'un projet Trimble ou
Catenda synchronisé répond `409`. La suppression n'est pas offerte : l'App ne sait pas restaurer une issue.

Scopes : `issues:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `issue_id` (obligatoire) | path | string (uuid) | Identifiant de l'issue (`id` rendu par `GET /api/v1/issues`). |

**Corps de la requête** (`IssueUpdate`)

- `title` (string)
- `description` (string | null)
- `status` (string) : Nom d'un statut du tableau.
- `type` (string) : Nom d'un type du tableau.
- `priority` (string ; `Basse`, `Normale`, `Haute`, `Critique`)
- `assigned_to` (string | null)
- `due_date` (string (date-time) | null)

**Réponses**

- `200` L'issue modifiée, avec ses commentaires. (Issue)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "9a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
  "board_id": "3e7d1c55-8a2b-4f0e-9c6d-1b2a3c4d5e6f",
  "ref": "#12",
  "title": "Pset_WallCommon absent sur les murs du niveau R+1",
  "description": "Compléter IsExternal et FireRating.",
  "status": "Fermé",
  "status_type": "closed",
  "type": "Clash",
  "priority": "Haute",
  "assigned_to": "bet-structure@exemple.fr",
  "due_date": "2026-11-20T00:00:00Z",
  "storey": "R+1",
  "models": [
    {
      "model_id": "7c2e9f10-3b4a-4d5e-8f6a-1b2c3d4e5f60",
      "revision_id": "1f2e3d4c-5b6a-4978-8695-a4b3c2d1e0f9"
    }
  ],
  "comment_count": 0,
  "viewpoint_count": 1,
  "editable": true,
  "created_by": "julie.martin@exemple.fr",
  "created_at": "2026-11-02T10:15:00Z",
  "modified_by": "julie.martin@exemple.fr",
  "modified_at": "2026-11-04T09:00:00Z",
  "comments": []
}
```

### POST /api/v1/issues/{issue_id}/comments

Commenter une issue (`createIssueComment`)

Commentaire signé du créateur de la clé, ajouté à la fin du fil. Issue synchronisée depuis Trimble ou Catenda `409`.

Scopes : `issues:write`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `issue_id` (obligatoire) | path | string (uuid) | Identifiant de l'issue (`id` rendu par `GET /api/v1/issues`). |
| `Idempotency-Key` | header | string | Clé d'idempotence, gardée 24 h par organisation. |

**Corps de la requête** (`IssueCommentCreate`)

- `text` (string, obligatoire)

**Réponses**

- `201` Commentaire ajouté. (IssueComment)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
  "text": "Corrigé dans la prochaine révision.",
  "created_by": "bet-structure@exemple.fr",
  "created_at": "2026-11-03T08:40:00Z"
}
```

## Jobs

Suivi des traitements longs.

### GET /api/v1/jobs/{job_id}

Suivre un traitement long (`getJob`)

Un job de conversion a pour identifiant celui de la révision ; son statut couvre conversion, jeu de données
et scène. Scope exigé selon le type : `models:read` (conversion), `ids:read` (contrôle IDS), `exports:read`
(export). Codes d'`error` : `PROCESSING_FAILED`, `IDS_CHECK_FAILED`, `EXPORT_FAILED`, `EXPORT_EXPIRED`,
`EXPORT_UNAVAILABLE`, et `EXPORT_PARTIAL` sur un export `COMPLETE` dont des tables manquent.

Scopes : `models:read`, `ids:read`, `exports:read`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `job_id` (obligatoire) | path | string (uuid) | Identifiant du job, rendu par toute opération qui répond `202` (aussi dans l'en-tête `Location`). |

**Réponses**

- `200` Le job. (Job)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "1f2e3d4c-5b6a-4978-8a1b-2c3d4e5f6a7b",
  "type": "export",
  "status": "COMPLETE",
  "percent_complete": 100,
  "eta_seconds": null,
  "target": {
    "type": "revision",
    "id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b"
  },
  "result": {
    "download_url": "https://ooddxuvlammhnltcfqpm.supabase.co/storage/v1/object/sign/exports/dataset-4f6a1b2c.zip?token=eyJhbGciOi",
    "download_expires_at": "2026-10-21T09:00:00Z",
    "size_bytes": 52428800
  },
  "error": null,
  "created_at": "2026-10-21T07:58:00Z",
  "started_at": "2026-10-21T07:58:02Z",
  "completed_at": "2026-10-21T07:59:40Z"
}
```

## Webhooks

Abonnements aux événements de l'organisation.

### GET /api/v1/webhooks

Lister les abonnements webhook (`listWebhooks`)

Scopes : `webhooks:manage`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `limit` | query | integer | Taille de page. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |

**Réponses**

- `200` Une page d'abonnements. (Page & object)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "data": [
    {
      "id": "6e7f8a9b-0c1d-4e2f-8a3b-4c5d6e7f8a9b",
      "name": "ERP chantier",
      "url": "https://erp.example.com/hooks/seveup",
      "events": [
        "revision.processing.completed",
        "ids_check.completed"
      ],
      "active": true,
      "last_delivered_at": "2026-10-20T18:00:00Z",
      "last_status": 200,
      "created_at": "2026-09-01T10:00:00Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### POST /api/v1/webhooks

Créer un abonnement webhook (`createWebhook`)

Le secret de signature n'est rendu qu'ici, une seule fois.

Scopes : `webhooks:manage`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Clé d'idempotence, gardée 24 h par organisation. |

**Corps de la requête** (`WebhookCreate`)

- `name` (string, obligatoire)
- `url` (string (uri), obligatoire) : HTTPS uniquement, 2000 caractères au plus.
- `events` (WebhookEventType[], obligatoire)

**Réponses**

- `201` Abonnement créé. (Webhook & object)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "6e7f8a9b-0c1d-4e2f-8a3b-4c5d6e7f8a9b",
  "name": "ERP chantier",
  "url": "https://erp.example.com/hooks/seveup",
  "events": [
    "revision.processing.completed",
    "ids_check.completed"
  ],
  "active": true,
  "last_delivered_at": null,
  "last_status": null,
  "created_at": "2026-10-21T08:00:00Z",
  "secret": "whsec_<montré une seule fois>"
}
```

### DELETE /api/v1/webhooks/{webhook_id}

Supprimer un abonnement webhook (`deleteWebhook`)

Scopes : `webhooks:manage`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `webhook_id` (obligatoire) | path | string (uuid) | Identifiant de l'abonnement webhook. |

**Réponses**

- `204` Abonnement supprimé, avec son journal ; les livraisons en attente ne partent plus.
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

### GET /api/v1/webhooks/{webhook_id}/deliveries

Journal de livraison d'un abonnement (`listWebhookDeliveries`)

Les livraisons des 30 derniers jours, les plus récentes d'abord.

Scopes : `webhooks:manage`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `webhook_id` (obligatoire) | path | string (uuid) | Identifiant de l'abonnement webhook. |
| `limit` | query | integer | Taille de page. |
| `cursor` | query | string | Curseur opaque rendu par la page précédente (`next_cursor`). |

**Réponses**

- `200` Une page de livraisons. (Page & object)
- `400` Requête invalide. (Problem)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "data": [
    {
      "id": "0b1c2d3e-4f50-4a6b-8c7d-9e0f1a2b3c4d",
      "event": "ids_check.completed",
      "status": "delivered",
      "attempts": 1,
      "response_code": 200,
      "error": null,
      "created_at": "2026-10-21T09:02:11Z",
      "delivered_at": "2026-10-21T09:02:12Z",
      "next_attempt_at": null
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### POST /api/v1/webhooks/{webhook_id}/deliveries/{delivery_id}/retry

Rejouer une livraison (`retryWebhookDelivery`)

Remet la livraison en file, avec le même `webhook-id` : un destinataire qui l'avait déjà traitée la reconnaît.
Une livraison encore en attente est rendue telle quelle. `409` si l'abonnement est désactivé ou si le corps de
l'événement n'est plus conservé.

Scopes : `webhooks:manage`

**Paramètres**

| Nom | Où | Type | Description |
|---|---|---|---|
| `webhook_id` (obligatoire) | path | string (uuid) | Identifiant de l'abonnement webhook. |
| `delivery_id` (obligatoire) | path | string (uuid) | Identifiant de la livraison, lu dans le journal de livraison de l'abonnement. |
| `Idempotency-Key` | header | string | Clé d'idempotence, gardée 24 h par organisation. |

**Réponses**

- `202` Livraison remise en file. (WebhookDelivery)
- `401` Clé absente, inconnue, expirée ou révoquée. (Problem)
- `403` Scope manquant (`SCOPE_MISSING`), plan sans accès à l'API ou à la fonction demandée (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`), route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`) ou modèle de tableau de bord réservé (`TEMPLATE_RESERVED`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué) : `NOT_FOUND`, ou `TEMPLATE_NOT_FOUND` pour le modèle d'un tableau de bord à créer. (Problem)
- `409` Idempotency-Key déjà utilisée avec un autre corps, ou état incompatible. (Problem)
- `429` Débit de la clé dépassé (`RATE_LIMITED`) ou quota mensuel du plan atteint (`QUOTA_EXCEEDED`). (Problem)
- `500` Erreur inattendue du serveur, déjà journalisée de notre côté ; réessayer plus tard. (Problem)
- `503` Capacité de traitement saturée pour l'instant ; réessayer après `Retry-After`. (Problem)

Exemple de réponse :

```json
{
  "id": "0b1c2d3e-4f50-4a6b-8c7d-9e0f1a2b3c4d",
  "event": "ids_check.completed",
  "status": "pending",
  "attempts": 0,
  "response_code": 401,
  "error": "HTTP 401",
  "created_at": "2026-10-21T09:02:11Z",
  "delivered_at": null,
  "next_attempt_at": "2026-10-22T07:20:00Z"
}
```

## Événements webhook

### revision.processing.completed

Une révision est entièrement traitée

Livraison au format Standard Webhooks : en-têtes `webhook-id` (stable entre les essais), `webhook-timestamp`
(secondes Unix) et `webhook-signature` (`v1,<base64 HMAC-SHA256(clé, id.timestamp.corps)>`, la clé étant le
secret `whsec_` décodé du base64). Les bibliothèques Standard Webhooks vérifient tout cela. Refuser un
horodatage de plus de 5 minutes. Répondre `2xx` en moins de 10 s ; sinon nouvel essai à délai croissant (1 min,
5 min, 30 min, 2 h), 5 essais en tout. Un `4xx` autre que `408` et `429` arrête les essais ; une redirection
n'est jamais suivie. `job_id`, quand il est présent, est celui que `GET /api/v1/jobs/{job_id}` suit.

```json
{
  "type": "revision.processing.completed",
  "timestamp": "2026-10-18T08:55:00Z",
  "data": {
    "revision_id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b",
    "model_id": "9d2e7f10-6c4b-4f2a-8e11-2a7c5b3d9e01",
    "job_id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b",
    "status": "COMPLETE"
  }
}
```

### revision.processing.failed

Le traitement d'une révision a échoué

Envoyé quand une étape passe en `ERROR`, comme le montre `GET /api/v1/revisions/{revision_id}`. Une relance
automatique qui réussit ensuite donne `revision.processing.completed`.

```json
{
  "type": "revision.processing.failed",
  "timestamp": "2026-10-18T08:55:00Z",
  "data": {
    "revision_id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b",
    "model_id": "9d2e7f10-6c4b-4f2a-8e11-2a7c5b3d9e01",
    "job_id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b",
    "status": "ERROR",
    "error_message": "Fichier IFC illisible"
  }
}
```

### ids_check.completed

Un contrôle IDS est terminé

```json
{
  "type": "ids_check.completed",
  "timestamp": "2026-10-18T09:02:11Z",
  "data": {
    "revision_id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b",
    "requirement_set_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
    "requirement_revision_id": "3d4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f6a",
    "job_id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b",
    "status": "COMPLETE",
    "score_percent": 87.5
  }
}
```

### ids_check.failed

Un contrôle IDS a échoué

```json
{
  "type": "ids_check.failed",
  "timestamp": "2026-10-18T09:02:11Z",
  "data": {
    "revision_id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b",
    "requirement_set_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
    "requirement_revision_id": "3d4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f6a",
    "status": "ERROR",
    "error_message": "Le contrôle n'a pas produit de résultat dans le délai"
  }
}
```

### export.ready

Un export est prêt à télécharger

```json
{
  "type": "export.ready",
  "timestamp": "2026-10-21T07:59:40Z",
  "data": {
    "job_id": "1f2e3d4c-5b6a-4978-8a1b-2c3d4e5f6a7b",
    "revision_id": "4f6a1b2c-3d4e-4f50-8a6b-7c8d9e0f1a2b"
  }
}
```

## Versions et dépréciation

- La version majeure est dans le chemin : `/api/v1`. À l'intérieur d'une version, seuls des changements compatibles sont publiés : nouvelles opérations, nouveaux champs de réponse, nouveaux paramètres facultatifs. Votre code doit ignorer les champs qu'il ne connaît pas et traiter un code d'erreur inconnu d'après le statut HTTP : la liste des codes peut s'allonger.
- Un changement incompatible (champ retiré ou renommé, type modifié, paramètre rendu obligatoire) passe par une nouvelle version, `/api/v2`. La version précédente reste servie au moins 12 mois après la sortie de la suivante.
- Dans `v1`, un champ n'est jamais retiré. Une opération vouée à disparaître est annoncée au moins 6 mois avant son retrait : entrée dans le journal des changements (aussi en flux Atom), e-mail aux administrateurs des organisations dont une clé l'a appelée dans les 90 derniers jours, 6 mois puis 1 mois avant le retrait, et en-têtes `Deprecation` (RFC 9745), `Sunset` (RFC 8594) et `Link` (`rel="deprecation"`, vers le journal) sur ses réponses. Elle fonctionne normalement jusqu'à la date de retrait, puis répond `410` avec le code `GONE`.
- Seule exception : un correctif de sécurité peut s'appliquer sans préavis. Il est alors décrit dans le journal des changements le jour même.

## Journal des changements

- 2026-11-13, v1.18.0 : Nouvelle opération : GET /api/v1/organization.
- 2026-11-12, v1.17.0 : Nouvelle opération : GET /api/v1/models.
- 2026-11-12, v1.16.0 : Nouvelle opération : GET /api/v1/dashboard-templates, GET /api/v1/spaces, GET /api/v1/spaces/{space_id}/dashboards, POST /api/v1/spaces/{space_id}/dashboards, POST /api/v1/spaces/{space_id}/requirement-sets ; Nouveau paramètre facultatif : GET /api/v1/requirement-sets ; Nouveau champ facultatif dans la requête : POST /api/v1/requirement-sets ; Nouveau statut d'erreur documenté : POST /api/v1/requirement-sets.
- 2026-11-10, v1.15.0 : Nouveau champ de réponse : GET /api/v1/revisions/{revision_id}/ids-results.
- 2026-11-09, v1.14.0 : Nouveau paramètre facultatif : GET /api/v1/revisions/{revision_id}/objects ; Nouveau champ de réponse : GET /api/v1/revisions/{revision_id}/objects.
- 2026-11-05, v1.13.0 : Nouvelle opération : POST /api/v1/kairnial-exports, GET /api/v1/kairnial-exports/{export_id}, GET /api/v1/kairnial-projects.
- 2026-11-05, v1.12.0 : Nouvelle opération : GET /api/v1/documents/{document_id}, GET /api/v1/documents/{document_id}/download, GET /api/v1/projects/{project_id}/documents.
- 2026-11-04, v1.11.0 : Nouvelle opération : GET /api/v1/projects/{project_id}/visa-grid, GET /api/v1/projects/{project_id}/visa-sheets, GET /api/v1/projects/{project_id}/visa-sheets/{visa_sheet_id}.
- 2026-11-04, v1.10.0 : Nouvelle opération : GET /api/v1/models/{model_id}/naming-check, GET /api/v1/naming-conventions, GET /api/v1/naming-conventions/{naming_convention_id}, POST /api/v1/naming-conventions/{naming_convention_id}/check.
- 2026-11-04, v1.9.0 : Nouvelle opération (6 opérations).
- 2026-11-04, v1.8.0 : Nouvelle opération : DELETE /api/v1/models/{model_id}, PATCH /api/v1/models/{model_id}, DELETE /api/v1/requirement-sets/{requirement_set_id}, PATCH /api/v1/revisions/{revision_id}.
- 2026-11-04, v1.7.0 : Nouvelle opération : POST /api/v1/projects, DELETE /api/v1/projects/{project_id}, PATCH /api/v1/projects/{project_id}.
- 2026-11-04, v1.6.0 : Nouvelle opération : GET /api/v1/requirement-sets/{requirement_set_id}, GET /api/v1/requirement-sets/{requirement_set_id}/specifications.
- 2026-11-03, v1.5.0 : Nouvelle opération : GET /api/v1/revisions/{revision_id}/ids-failures/bcf.
- 2026-10-27, v1.4.0 : Nouveau champ de réponse (8 opérations) ; Nouvelle opération : GET /api/v1/revisions/{revision_id}/ids-failures, GET /api/v1/revisions/{revision_id}/objects.
- 2026-10-22, v1.3.0 : Nouveau statut d'erreur documenté : GET /api/v1/requirement-sets.
- 2026-10-22, v1.2.0 : Codes d'erreur : la liste de `code` devient ouverte (`x-extensible-enum`), un code inconnu se traite d'après le statut HTTP. Nouveau code `GONE` et réponse `410` pour une opération retirée à sa date `x-sunset`, annoncée au moins 6 mois avant par les en-têtes `Deprecation` et `Sunset`.
- 2026-10-22, v1.1.0 : Nouvel en-tête de réponse documenté (22 opérations) ; Format d'en-tête de réponse précisé (22 opérations) ; Nouveau statut d'erreur documenté (22 opérations) ; Format de champ de réponse précisé (22 opérations).
- 2026-10-22, v1.0.1 : Adresse officielle de l'API : https://api.seve-up.app. L'adresse https://seveup-gateway.onrender.com répond toujours, mais ne fait plus partie du contrat.
- 2026-10-21, v1.0.0 : Première version publique : projets, maquettes, révisions, dépôts de fichiers (TUS), jeux d'exigences, contrôles IDS, jobs, exports et webhooks (Standard Webhooks).
