# API REST

L’API de Portal donne accès aux projets, factures, documents et clients d’un espace. Elle permet aussi d’y écrire, comme le serveur MCP, et de gérer les webhooks qui préviennent ton outil. Elle passe par les mêmes contrôles que l’interface. Pour un agent IA, utilise plutôt le serveur MCP, voir [Connecter un agent IA](/portal/docs/guides/agents-ia/).

La description complète, au format OpenAPI 3.1, est publique :

```
https://app.hgdc.ch/portal/api/v1/openapi.json
```

## Adresse et clé

Toutes les routes commencent par `https://app.hgdc.ch/portal/api/v1`. Chaque requête porte une clé d’API :

```sh
curl https://app.hgdc.ch/portal/api/v1/projects \
  -H "Authorization: Bearer hgdc_portal_..."
```

Tu crées la clé dans **Réglages**, section **Clés d’API**. Donne-lui un nom, choisis les droits et la durée (7, 30 ou 90 jours, ou 1 an). Copie-la : elle ne s’affiche qu’une fois. Portal n’en garde que l’empreinte : une clé perdue se remplace, elle ne se retrouve pas.

| Droits                  | Portée                     | Ce qu’elle permet |
| ----------------------- | -------------------------- | ----------------- |
| **Lecture seule**       | `portal:read`              | Les routes `GET`  |
| **Lecture et écriture** | `portal:read portal:write` | Toutes les routes |

Une clé est à toi et à un seul espace. Elle agit avec tes droits, dans cet espace seulement : une ressource d’un autre espace répond comme une ressource inconnue. Tu as au plus 10 clés actives par espace.

Elle s’arrête quand elle expire, quand tu la révoques, ou quand tu quittes l’équipe de l’espace. Un propriétaire ou un admin voit les clés actives des membres dans **Réglages** et peut en révoquer une, par exemple après la perte d’un ordinateur.

L’API est réservée à l’équipe. Un client invité reçoit `403`.

## Routes

| Méthode  | Route                           | Droits          | Ce qu’elle fait                                                                                                          |
| -------- | ------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `GET`    | `/projects`                     | Lecture         | Liste les projets. Filtre `status` : `active`, `paused`, `done`, `archived`                                              |
| `GET`    | `/projects/{id}`                | Lecture         | Lit un projet                                                                                                            |
| `GET`    | `/invoices`                     | Lecture         | Liste les factures. Filtre `status` : `draft`, `issued`, `paid`, `cancelled`                                             |
| `GET`    | `/invoices/{id}`                | Lecture         | Lit une facture, avec ses lignes. Montants en centimes                                                                   |
| `GET`    | `/documents?project_id=`        | Lecture         | Liste les documents d’un projet                                                                                          |
| `GET`    | `/clients`                      | Lecture         | Liste les clients. Filtre `archived` : `true` ou `false`                                                                 |
| `GET`    | `/clients/{id}`                 | Lecture         | Lit une fiche client                                                                                                     |
| `GET`    | `/webhooks`                     | Lecture, admin  | Liste les adresses qui reçoivent les événements                                                                          |
| `POST`   | `/webhooks`                     | Écriture, admin | Ajoute une adresse : `url`, `events`, `description` facultative                                                          |
| `DELETE` | `/webhooks/{id}`                | Écriture, admin | Supprime une adresse                                                                                                     |
| `POST`   | `/projects/{id}/messages`       | Écriture        | Écrit dans le fil du projet : `body`, `action_id` facultatif. L’équipe et les clients du projet sont prévenus            |
| `POST`   | `/projects/{id}/time-entries`   | Écriture        | Saisit du temps : `date` (AAAA-MM-JJ), `minutes`, `phase_id` et `note` facultatifs. Le client ne le voit pas             |
| `POST`   | `/projects/{id}/phases`         | Écriture        | Ajoute une étape : `name`, `description` et `due_date` facultatifs. Le client la voit                                    |
| `POST`   | `/phases/{id}/steps`            | Écriture        | Ajoute une tâche à une étape : `title`, `description`, `due_date`, `internal` (tâche cachée au client)                   |
| `POST`   | `/projects/{id}/client-actions` | Écriture        | Demande une action au client : `title`, `description`, `due_date`, `phase_id`. Les clients du projet reçoivent un e-mail |
| `POST`   | `/prospects`                    | Écriture        | Ajoute un prospect à l’espace de la clé : `company`, puis `contact_name`, `email`, `phone`, `notes` facultatifs          |

Les webhooks sont réservés aux propriétaires et aux admins de l’espace : la clé d’un membre reçoit `403`. Un espace a au plus 20 adresses. Le secret de signature d’un webhook créé par l’API n’est rendu qu’une fois, dans la réponse du `POST`. Les événements et la vérification de la signature sont décrits dans [Événements des webhooks](/portal/docs/reference/evenements-webhooks/).

## Écrire

Les écritures prennent un corps JSON (`Content-Type: application/json`, 64 Ko au plus) et rendent `201` avec l’identifiant créé :

```sh
curl https://app.hgdc.ch/portal/api/v1/projects/k57.../phases   -H "Authorization: Bearer hgdc_portal_..."   -H "Idempotency-Key: maquettes-2026-11"   -H "Content-Type: application/json"   -d '{ "name": "Maquettes" }'
```

```json
{ "id": "k97…", "object": "phase", "summary": "Étape « Maquettes » ajoutée." }
```

Elles font exactement ce que fait le serveur MCP, avec tes droits : un champ inconnu est refusé, et un rôle insuffisant reçoit `403`. Un message ou une action client **prévient les clients** : une clé en écriture vaut ton accord, sans confirmation.

### Rejouer sans écrire deux fois

Ajoute `Idempotency-Key` (1 à 255 caractères, sans espace) pour pouvoir renvoyer une requête qui a échoué sans savoir si elle est passée. Avec la même clé et la même requête, Portal rend le résultat du premier appel (en-tête `Idempotent-Replayed: true`) et n’écrit rien de plus. Avec la même clé et une autre requête, il répond `409`. La clé est retenue 24 heures. Elle ne s’applique pas à la création d’un webhook.

## Pagination

Les listes rendent une page à la fois :

```json
{
  "data": [{ "id": "k57…", "name": "Site vitrine", "status": "active" }],
  "has_more": true,
  "next_cursor": "eyJ…"
}
```

`limit` choisit la taille de la page (20 par défaut, 100 au plus). Pour la suivante, renvoie `next_cursor` dans `cursor`, avec les mêmes filtres.

## Erreurs

Les erreurs suivent le format `application/problem+json` (RFC 9457) :

```json
{ "type": "https://hgdc.ch/problems/not-found", "title": "Ressource introuvable", "status": 404, "detail": "Ressource introuvable." }
```

| Code  | Ce qu’il veut dire                                                                                                                                                                                                                                                 |
| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400` | `limit` ou `cursor` invalide, ou corps qui n’est pas du JSON                                                                                                                                                                                                       |
| `401` | Clé absente, mal copiée, expirée ou révoquée. La réponse porte l’en-tête `WWW-Authenticate`. Crée une autre clé                                                                                                                                                    |
| `403` | Clé ou jeton sans la portée demandée (`insufficient_scope`, par exemple une écriture avec la lecture seule), webhooks sans être admin, ou client invité. Le corps est du JSON d’erreur et l’en-tête `WWW-Authenticate` nomme la portée qui manque (`portal:write`) |
| `404` | Ressource inconnue, ou d’un autre espace                                                                                                                                                                                                                           |
| `405` | Méthode inconnue sur cette route                                                                                                                                                                                                                                   |
| `409` | `Idempotency-Key` déjà utilisée pour une autre requête                                                                                                                                                                                                             |
| `415` | `POST` sans `Content-Type: application/json`                                                                                                                                                                                                                       |
| `422` | Filtre ou corps refusé. Le détail, champ par champ, est dans `errors`                                                                                                                                                                                              |
| `429` | Trop d’appels. Attends la durée de l’en-tête `Retry-After`                                                                                                                                                                                                         |

## Limites

| Quoi                                     | Limite                        |
| ---------------------------------------- | ----------------------------- |
| Lectures par clé                         | 600 par minute, rafale de 120 |
| Écritures par clé                        | 120 par minute, rafale de 30  |
| Échecs d’authentification par adresse IP | 20 par 10 minutes             |

Une réponse `429` porte l’en-tête `Retry-After` et les en-têtes `RateLimit` de la limite atteinte.
