# SeveUp API v1.3.0

> Documentation of the SeveUp public API, generated from the OpenAPI contract.

- Developer page: https://www.seve-up.app/en/developers
- OpenAPI contract (YAML): https://www.seve-up.app/openapi/public-v1.yaml
- Changelog (JSON): https://www.seve-up.app/openapi/changelog-v1.json
- Changelog (Atom feed): https://www.seve-up.app/openapi/changelog-v1.xml
- Base URL: 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).

## Getting started

From the key to an IDS check result, in seven calls. Examples use `curl` and `jq`; every request carries `Authorization: Bearer` followed by the key.

### 1. Create a key

In SeveUp App, organisation console, "API keys" section (administrators only): "Public API key", tick the scopes you need (`projects:read`, `models:read`, `models:write`, `ids:read`, `ids:write`). The `sk_…` secret is shown once. The same section shows API usage: calls, monthly quotas and errors.

```bash
export SEVEUP_API=https://api.seve-up.app
export SEVEUP_KEY=sk_…   # organisation key
```

### 2. List projects

Cursor-paginated response: `data`, `has_more`, `next_cursor`.

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

### 3. Open an upload

The file does not go through the API: the response gives a TUS upload address and its headers, valid for 2 h. `Idempotency-Key` lets you replay the call without creating a second upload.

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

### 4. Send the file

TUS 1.0 protocol: create, then send the bytes. For a large file, use a TUS client (tus-js-client, tuspy…) that resumes after a cut, with 6 MB chunks. Then `complete` checks that the whole file arrived.

```bash
URL=$(jq -r .upload_url upload.json)
AUTH=$(jq -r '.upload_headers.Authorization' upload.json)
META=$(jq -r '.upload_headers["Upload-Metadata"]' upload.json)
LOC=$(curl -s -i -X POST "$URL" -H "Authorization: $AUTH" -H "Tus-Resumable: 1.0.0" \
  -H "Upload-Metadata: $META" -H "Upload-Length: $SIZE" \
  | 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 @"$FILE"
curl -s -X POST "$SEVEUP_API/api/v1/uploads/$UPLOAD/complete" \
  -H "Authorization: Bearer $SEVEUP_KEY" -H "Content-Type: application/json" -d '{}'
```

### 5. Create the model

Conversion starts right away: `202` response with a job, and its address in `Location`. For a new version of an existing model: `POST /api/v1/models/{model_id}/revisions`.

```bash
PROJECT=…   # a project id from step 2
curl -s -X POST "$SEVEUP_API/api/v1/projects/$PROJECT/models" \
  -H "Authorization: Bearer $SEVEUP_KEY" -H "Content-Type: application/json" \
  -d "{\"name\": \"My model\", \"upload_id\": \"$UPLOAD\"}" > job.json
REVISION=$(jq -r .target.id job.json)
```

### 6. Follow the processing

Statuses: `QUEUED`, `PROCESSING`, then `COMPLETE` or `ERROR` (with `error.code`). Instead of polling, a `revision.processing.completed` webhook can notify you.

```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. Check against an IDS and read the result

Pick an existing requirement set, or upload an `.ids` (upload with `purpose: ids`, then `POST /api/v1/requirement-sets`). The check is a job like the others. In the result, a specification counts in the score when `applicable` is true.

```bash
SET=$(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\": \"$SET\"}"
# … follow the job as in step 6, then:
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`

**Parameters**

| Name | In | 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). |

**Responses**

- `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)

Response example:

```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`

**Parameters**

| Name | In | Type | Description |
|---|---|---|---|
| `project_id` (required) | path | string (uuid) | Identifiant du projet (`id` rendu par `GET /api/v1/projects`). |

**Responses**

- `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)

Response example:

```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`

**Parameters**

| Name | In | Type | Description |
|---|---|---|---|
| `project_id` (required) | 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`). |

**Responses**

- `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)

Response example:

```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`

**Parameters**

| Name | In | Type | Description |
|---|---|---|---|
| `project_id` (required) | 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. |

**Request body** (`ModelCreate`)

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

**Responses**

- `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)

Response example:

```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`

**Parameters**

| Name | In | Type | Description |
|---|---|---|---|
| `model_id` (required) | path | string (uuid) | Identifiant de la maquette (`id` rendu par la liste des maquettes du projet). |

**Responses**

- `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)

Response example:

```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`

**Parameters**

| Name | In | Type | Description |
|---|---|---|---|
| `model_id` (required) | 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`). |

**Responses**

- `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)

Response example:

```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`

**Parameters**

| Name | In | Type | Description |
|---|---|---|---|
| `model_id` (required) | 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. |

**Request body** (`RevisionCreate`)

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

**Responses**

- `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)

Response example:

```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`

**Parameters**

| Name | In | Type | Description |
|---|---|---|---|
| `revision_id` (required) | path | string (uuid) | Identifiant de la révision (`latest_revision_id` d'une maquette, ou `id` d'une révision). |

**Responses**

- `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)

Response example:

```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`

**Parameters**

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

**Request body** (`UploadCreate`)

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

**Responses**

- `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)

Response example:

```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`

**Parameters**

| Name | In | Type | Description |
|---|---|---|---|
| `upload_id` (required) | 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. |

**Request body**

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

**Responses**

- `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)

Response example:

```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`

**Parameters**

| Name | In | Type | Description |
|---|---|---|---|
| `revision_id` (required) | path | string (uuid) | Identifiant de la révision (`latest_revision_id` d'une maquette, ou `id` d'une révision). |

**Responses**

- `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)

Response example:

```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`

**Parameters**

| Name | In | Type | Description |
|---|---|---|---|
| `project_id` (required) | path | string (uuid) | Identifiant du projet (`id` rendu par `GET /api/v1/projects`). |

**Responses**

- `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)

Response example:

```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`

**Parameters**

| Name | In | 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. |

**Responses**

- `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)

Response example:

```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`

**Parameters**

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

**Request body** (`RequirementSetCreate`)

- `name` (string, required)
- `upload_id` (string (uuid), required): Dépôt clos (`purpose: ids`), fichier .ids ou .xlsx.
- `project_id` (string (uuid)): Lie le jeu à ce projet ; sinon portée organisation.

**Responses**

- `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)

Response example:

```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`

**Parameters**

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

**Request body** (`IdsCheckCreate`)

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

**Responses**

- `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)

Response example:

```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`

**Parameters**

| Name | In | Type | Description |
|---|---|---|---|
| `revision_id` (required) | 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. |

**Request body** (`ExportCreate`)

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

**Responses**

- `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)

Response example:

```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`

**Parameters**

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

**Responses**

- `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)

Response example:

```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`

**Parameters**

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

**Responses**

- `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)

Response example:

```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`

**Parameters**

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

**Request body** (`WebhookCreate`)

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

**Responses**

- `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)

Response example:

```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`

**Parameters**

| Name | In | Type | Description |
|---|---|---|---|
| `webhook_id` (required) | path | string (uuid) | Identifiant de l'abonnement webhook. |

**Responses**

- `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`

**Parameters**

| Name | In | Type | Description |
|---|---|---|---|
| `webhook_id` (required) | 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`). |

**Responses**

- `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)

Response example:

```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`

**Parameters**

| Name | In | Type | Description |
|---|---|---|---|
| `webhook_id` (required) | path | string (uuid) | Identifiant de l'abonnement webhook. |
| `delivery_id` (required) | 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. |

**Responses**

- `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)

Response example:

```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"
}
```

## Webhook events

### 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"
  }
}
```

## Versioning and deprecation

- The major version is in the path: `/api/v1`. Within a version, only compatible changes are released: new operations, new response fields, new optional parameters. Your code must ignore fields it does not know and handle an unknown error code by its HTTP status: the list of codes may grow.
- An incompatible change (removed or renamed field, changed type, parameter made required) goes into a new version, `/api/v2`. The previous version stays available for at least 12 months after the next one ships.
- In `v1`, a field is never removed. An operation due to disappear is announced at least 6 months before removal: changelog entry (also as an Atom feed), e-mail to administrators of organisations whose key called it in the last 90 days, 6 months then 1 month before removal, and `Deprecation` (RFC 9745), `Sunset` (RFC 8594) and `Link` (`rel="deprecation"`, to the changelog) headers on its responses. It works normally until the removal date, then answers `410` with the code `GONE`.
- Only exception: a security fix may apply without notice. It is then described in the changelog the same day.

## Changelog

- 2026-10-22, v1.3.0: New documented error status: GET /api/v1/requirement-sets.
- 2026-10-22, v1.2.0: Error codes: the `code` list becomes open (`x-extensible-enum`); handle an unknown code by its HTTP status. New `GONE` code and `410` response for an operation removed at its `x-sunset` date, announced at least 6 months before with `Deprecation` and `Sunset` headers.
- 2026-10-22, v1.1.0: New documented response header (22 operations); Response header format specified (22 operations); New documented error status (22 operations); Response field format specified (22 operations).
- 2026-10-22, v1.0.1: Official API address: https://api.seve-up.app. The https://seveup-gateway.onrender.com address still answers but is no longer part of the contract.
- 2026-10-21, v1.0.0: First public version: projects, models, revisions, file uploads (TUS), requirement sets, IDS checks, jobs, exports and webhooks (Standard Webhooks).
