# API SeveUp v1.3.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}'
```

## 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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (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
}
```

### 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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué). (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"
}
```

## 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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué). (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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué). (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/{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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué). (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"
}
```

### 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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué). (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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué). (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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué). (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"
}
```

## 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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué). (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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué). (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": [
    {
      "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/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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué). (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"
}
```

### 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. |

**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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué). (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`)

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.

**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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (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"
}
```

### 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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (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`).

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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué). (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
}
```

## 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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué). (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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué). (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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué). (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 (`PLAN_REQUIRED`), écriture refusée en plan TRIAL (`PLAN_READ_ONLY`) ou route non ouverte aux clés (`API_KEY_ROUTE_FORBIDDEN`). (Problem)
- `404` Ressource inexistante ou appartenant à une autre organisation (jamais distingué). (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-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).
