# Lister les projets

Tes projets, avec leurs sites, leur score sur 100 et leurs problèmes ouverts. Les identifiants des sites (`targets[].id`) servent à [lancer un scan](https://docs.rustine.io/api/scans/create).

```
GET https://api.rustine.io/v1/projects
```

Permission OAuth : `scans:read` · une clé d'API les a toutes

## Paramètres de requête

- `limit` (integer) Combien d'éléments par page, de 1 à 100 (20 par défaut).
- `starting_after` (string) Le curseur : l'identifiant du dernier élément de la page précédente.

## Réponse (200 OK)

- `object` (string, obligatoire) Le type d'objet, toujours le même pour une route donnée.
- `data` (array of object, obligatoire) Les éléments de la page.
  - `id` (string, obligatoire) L'identifiant du projet.
  - `object` (string, obligatoire) Le type d'objet, toujours le même pour une route donnée.
  - `name` (string, obligatoire) Le nom du projet.
  - `score` (number | null, obligatoire) Le score du projet sur 100, `null` avant le premier scan terminé.
  - `grade` (string | null, obligatoire) La note de A à F, `null` avant le premier scan terminé. (`A`, `B`, `C`, `D`, `F`)
  - `open_issues` (integer, obligatoire) Les problèmes ouverts sur tous les sites du projet, masqués compris.
  - `paused` (boolean, obligatoire) En pause, parce que l'offre couvre moins de projets : lisible, mais ni scan, ni revérification, ni correctif, ni nouvelle adresse.
  - `last_scan_at` (string | null, obligatoire) La fin du dernier scan terminé, tous sites confondus.
  - `targets` (array of object, obligatoire) Les sites et dépôts du projet.
    - `id` (string, obligatoire) L'identifiant du site ou du dépôt : c'est lui que tu passes pour lancer un scan.
    - `object` (string, obligatoire) Le type d'objet, toujours le même pour une route donnée.
    - `project_id` (string, obligatoire) Le projet auquel il appartient.
    - `kind` (string, obligatoire) `domain` (un site) ou `repository` (un dépôt GitHub). (`domain`, `server`, `repository`)
    - `address` (string, obligatoire) Le nom d'hôte du site (`app.monsaas.fr`) ou `propriétaire/dépôt` pour un dépôt.
    - `verified` (boolean, obligatoire) Vrai quand ton espace a prouvé qu'il contrôle ce site. Sans preuve, le détail des problèmes reste masqué et seuls les scans rapides tournent.
    - `score` (number | null, obligatoire) Le score sur 100, `null` avant le premier scan terminé.
    - `grade` (string | null, obligatoire) La note de A à F, `null` avant le premier scan terminé. (`A`, `B`, `C`, `D`, `F`)
    - `last_scan_at` (string | null, obligatoire) La fin du dernier scan terminé.
    - `hidden_issues` (object | null, obligatoire) Site non prouvé : ce que ses problèmes masqués représentent, en nombre seulement (jamais lesquels ni où). `null` quand rien n'est masqué.
      - `urgent` (integer, obligatoire) Problèmes critiques et élevés ouverts, masqués jusqu'à la preuve.
      - `later` (integer, obligatoire) Problèmes moyens et faibles ouverts, masqués jusqu'à la preuve.
- `has_more` (boolean, obligatoire) Vrai s'il reste des éléments après cette page : rappelle la route avec `starting_after`.

## Exemples

### curl

```bash
# La clé reste dans une variable, jamais dans l'adresse
curl https://api.rustine.io/v1/projects \
  -H "Authorization: Bearer $RUSTINE_API_KEY"
```

### Node

```js
const res = await fetch(
  "https://api.rustine.io/v1/projects",
  {
    headers: {
      Authorization: `Bearer ${process.env.RUSTINE_API_KEY}`,
    },
  },
)
const page = await res.json()
```

### Python

```python
import os

import requests

page = requests.get(
    "https://api.rustine.io/v1/projects",
    headers={
        "Authorization": f"Bearer {os.environ['RUSTINE_API_KEY']}",
    },
).json()
```

### Réponse : 200 OK

```json
{
  "object": "list",
  "data": [
    {
      "id": "4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b",
      "object": "project",
      "name": "Example SaaS",
      "score": 62,
      "grade": "C",
      "open_issues": 3,
      "paused": false,
      "last_scan_at": "2026-10-13T08:42:10.000Z",
      "targets": [
        {
          "id": "8d2e4f6a-9b1c-4d3e-a5f7-0b2c4d6e8f1a",
          "object": "target",
          "project_id": "4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b",
          "kind": "domain",
          "address": "app.example.com",
          "verified": true,
          "score": 62,
          "grade": "C",
          "last_scan_at": "2026-10-13T08:42:10.000Z",
          "hidden_issues": null
        },
        {
          "id": "c1a3e5f7-2b4d-4f6a-8c0e-1a3b5c7d9e2f",
          "object": "target",
          "project_id": "4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b",
          "kind": "repository",
          "address": "example/saas",
          "verified": true,
          "score": 81,
          "grade": "B",
          "last_scan_at": "2026-10-12T17:03:55.000Z",
          "hidden_issues": null
        }
      ]
    }
  ],
  "has_more": false
}
```

## Erreurs

| Code | error.code | Ce qui se passe |
| --- | --- | --- |
| 400 | `parameter_invalid` | Un paramètre manque ou n'est pas valable ; `param` le nomme. |
| 400 | `cursor_invalid` | `starting_after` n'est l'identifiant d'aucun élément de cette liste. |
| 401 | `authentication_required` | Aucune clé dans la requête. Passe-la dans l'en-tête `Authorization: Bearer rst_…`, jamais dans l'adresse. |
| 401 | `invalid_api_key` | La clé ou le jeton OAuth est faux, révoqué ou expiré. Vérifie qu'il est copié en entier, ou crée une nouvelle clé. |
| 402 | `api_plan_required` | L'espace est sur l'offre Gratuite. L'API vient avec Pro, ou à l'essai 14 jours sur Solo. |
| 402 | `api_trial_ended` | L'essai de 14 jours de Solo est terminé. Passe à Pro : tes clés remarchent aussitôt. |
| 403 | `scope_missing` | Le jeton OAuth n'a pas la permission que demande cette route (indiquée sur sa page). Reconnecte l'agent pour l'accorder. Une clé d'API a toutes les permissions. |
| 429 | `rate_limited` | Trop de requêtes pour cette clé (120 par minute). Attends `Retry-After` secondes. |
| 500 | `internal_error` | Une erreur de notre côté, signalée automatiquement. Réessaie plus tard ; donne le `Request-Id` au support si elle revient. |
| 503 | `service_unavailable` | Rustine est indisponible un instant. Réessaie avec la même `Idempotency-Key`. |

https://docs.rustine.io/api/errors
