# Lancer un scan

Démarre un scan sur un site de ton projet, ou une analyse sur un dépôt GitHub. Le scan tourne en arrière-plan : la réponse arrive tout de suite (`201`, statut `queued`) et tu suis l'avancement avec [Suivre un scan](https://docs.rustine.io/api/scans/get).

```
POST https://api.rustine.io/v1/targets/{target_id}/scans
```

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

> Un **audit complet** ne part que sur un site prouvé (DNS, fichier ou balise meta) : la loi interdit de tester un site qu'on ne contrôle pas. Sans preuve, demande un `quick` : Rustine lit seulement ce que tout visiteur voit. Si un scan du site tourne déjà, ou si un scan du même type a fini dans l'heure, tu reçois celui-là (`200`, `reused: true`) et rien n'est décompté.

## Paramètres de chemin

- `target_id` (string, obligatoire) Le site ou le dépôt.

## En-têtes

- `Idempotency-Key` (string) Une clé unique (1 à 255 caractères imprimables, un UUID convient) pour rejouer la requête sans risque : la première réponse est renvoyée pendant 24 heures ; la même clé avec une autre requête répond `409 idempotency_conflict`.

## Corps de la requête

- `kind` (string) Le type de scan : `quick` (par défaut pour un site) lit ce que tout visiteur voit, `audit` ajoute des contrôles en lecture seule (site prouvé, Solo et Pro), `repository` analyse un dépôt GitHub (le seul type, et le défaut, pour un dépôt). (`quick`, `audit`, `repository`)

## Réponse (201 Created)

- `id` (string, obligatoire) L'identifiant du scan.
- `object` (string, obligatoire) Le type d'objet, toujours le même pour une route donnée.
- `target_id` (string, obligatoire) Le site ou le dépôt scanné.
- `kind` (string, obligatoire) `quick` (ce que tout visiteur voit), `audit` (ajoute des contrôles en lecture seule, sites prouvés seulement) ou `repository` (analyse d'un dépôt GitHub). (`quick`, `audit`, `repository`)
- `status` (string, obligatoire) Les scans tournent en arrière-plan : interroge jusqu'à `done` ou `failed`. (`queued`, `running`, `done`, `failed`)
- `score` (number | null, obligatoire) Le score sur 100, une fois terminé.
- `grade` (string | null, obligatoire) La note, une fois terminé. (`A`, `B`, `C`, `D`, `F`)
- `coverage` (number | null, obligatoire) La part des contrôles qui ont pu tourner (de 0 à 1), une fois terminé.
- `audit_fallback` (boolean, obligatoire) Un audit était demandé mais la preuve du site ne tenait plus : un scan rapide a tourné à la place, et aucun audit n'a été décompté.
- `reused` (boolean, obligatoire) Vrai quand la requête a reçu un scan existant au lieu d'un nouveau (un en cours, ou un du même type dans l'heure) ; rien n'a été décompté.
- `next_real_scan_at` (string | null, obligatoire) Avec `reused` pour un scan récent : quand un nouveau scan de ce type pourra partir.
- `created_at` (string, obligatoire) Quand il a été demandé (ISO 8601, UTC).
- `finished_at` (string | null, obligatoire) Quand il s'est terminé, `null` tant qu'il tourne.

## Exemples

### curl

```bash
# La clé reste dans une variable, jamais dans l'adresse
curl https://api.rustine.io/v1/targets/8d2e4f6a-9b1c-4d3e-a5f7-0b2c4d6e8f1a/scans \
  -H "Authorization: Bearer $RUSTINE_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"kind":"audit"}'
```

### Node

```js
const res = await fetch(
  "https://api.rustine.io/v1/targets/8d2e4f6a-9b1c-4d3e-a5f7-0b2c4d6e8f1a/scans",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.RUSTINE_API_KEY}`,
      "Idempotency-Key": crypto.randomUUID(),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ kind: "audit" }),
  },
)
const scan = await res.json()
```

### Python

```python
import os
import uuid

import requests

scan = requests.post(
    "https://api.rustine.io/v1/targets/8d2e4f6a-9b1c-4d3e-a5f7-0b2c4d6e8f1a/scans",
    headers={
        "Authorization": f"Bearer {os.environ['RUSTINE_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={"kind": "audit"},
).json()
```

### Réponse : 201 Created

```json
{
  "id": "e7f9a1b3-5c7d-4e9f-b1a3-c5d7e9f1a3b5",
  "object": "scan",
  "target_id": "8d2e4f6a-9b1c-4d3e-a5f7-0b2c4d6e8f1a",
  "kind": "audit",
  "status": "queued",
  "score": null,
  "grade": null,
  "coverage": null,
  "audit_fallback": false,
  "reused": false,
  "next_real_scan_at": null,
  "created_at": "2026-10-13T08:41:02.000Z",
  "finished_at": null
}
```

## Erreurs

| Code | error.code | Ce qui se passe |
| --- | --- | --- |
| 400 | `body_invalid` | Le corps de la requête n'est pas du JSON valable. Vérifie l'en-tête `Content-Type: application/json`. |
| 400 | `parameter_invalid` | Un paramètre manque ou n'est pas valable ; `param` le nomme. |
| 400 | `target_kind_invalid` | Le type de scan ne va pas avec la cible : `repository` pour un dépôt GitHub, `quick` ou `audit` pour un site. |
| 400 | `idempotency_key_invalid` | L'en-tête `Idempotency-Key` doit faire de 1 à 255 caractères imprimables. |
| 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. |
| 402 | `audit_not_in_plan` | L'offre Gratuite ne fait que des scans rapides ; l'audit complet vient avec Solo et Pro. |
| 402 | `repositories_not_in_plan` | L'analyse des dépôts GitHub vient avec Solo et Pro. |
| 402 | `scan_quota_exceeded` | Les scans rapides de la période sont épuisés, et il ne reste aucun crédit de pack. |
| 402 | `audit_quota_exceeded` | Les audits complets de la période sont épuisés, et il ne reste aucun crédit de pack. |
| 402 | `project_paused` | Le projet est en pause : ton offre couvre moins de projets que ton espace n'en a. Ses problèmes restent lisibles, mais rien de nouveau ne tourne. |
| 402 | `domain_taken` | Sur l'offre Gratuite, un domaine déjà suivi par un autre compte gratuit ne peut être ni ajouté ni scanné. |
| 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. |
| 404 | `resource_not_found` | Rien avec cet identifiant dans l'espace de la clé. Un objet d'un autre espace répond pareil : son existence n'est jamais révélée. |
| 409 | `proof_required` | Le site doit d'abord être prouvé comme le tien (audit complet, correctif, marquage). Prouve-le dans l'app : DNS, fichier ou balise meta. |
| 409 | `site_excluded` | Le propriétaire de ce site a demandé à ne pas être scanné. Rustine ne le scannera pas. |
| 409 | `github_unavailable` | L'analyse des dépôts GitHub n'est pas disponible pour le moment. |
| 409 | `idempotency_conflict` | Cette `Idempotency-Key` a déjà servi pour une autre requête (méthode, chemin ou corps). Une nouvelle action prend une nouvelle clé. |
| 409 | `idempotency_in_progress` | Une requête avec cette `Idempotency-Key` est encore en cours. Réessaie dans un instant. |
| 429 | `rate_limited` | Trop de requêtes pour cette clé (120 par minute). Attends `Retry-After` secondes. |
| 429 | `repository_analyses_limit_reached` | L'espace a lancé beaucoup d'analyses de dépôt ces dernières 24 heures. Réessaie plus tard. |
| 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`. |
| 503 | `platform_busy` | Rustine ne lance pas de nouveau scan pendant quelques minutes. Réessaie plus tard ; rien n'a été décompté. |

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