# Ajouter un site

Ajoute une adresse à un projet : un sous-domaine (`app.`, `api.`) ou un autre domaine du même produit.

```
POST https://api.rustine.io/v1/projects/{project_id}/targets
```

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

> Seulement un site qui est à toi ou que tu as le droit de tester. Le nombre d'adresses par projet dépend de l'offre (5 en Solo, 20 en Pro) : au-delà, `402 address_limit_reached`.

## Paramètres de chemin

- `project_id` (string, obligatoire) Le projet.

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

- `url` (string, obligatoire) L'adresse du site (`https://app.monsaas.fr`). Seulement un site qui est à toi ou que tu as le droit de tester.

## Réponse (201 Created)

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

## Exemples

### curl

```bash
# La clé reste dans une variable, jamais dans l'adresse
curl https://api.rustine.io/v1/projects/4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b/targets \
  -H "Authorization: Bearer $RUSTINE_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://api.example.com"}'
```

### Node

```js
const res = await fetch(
  "https://api.rustine.io/v1/projects/4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b/targets",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.RUSTINE_API_KEY}`,
      "Idempotency-Key": crypto.randomUUID(),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ url: "https://api.example.com" }),
  },
)
const target = await res.json()
```

### Python

```python
import os
import uuid

import requests

target = requests.post(
    "https://api.rustine.io/v1/projects/4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b/targets",
    headers={
        "Authorization": f"Bearer {os.environ['RUSTINE_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={"url": "https://api.example.com"},
).json()
```

### Réponse : 201 Created

```json
{
  "id": "8d2e4f6a-9b1c-4d3e-a5f7-0b2c4d6e8f1a",
  "object": "target",
  "project_id": "4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b",
  "kind": "domain",
  "address": "api.example.com",
  "verified": false,
  "score": null,
  "grade": null,
  "last_scan_at": null,
  "hidden_issues": 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 | `url_invalid` | L'adresse n'est pas l'URL valable d'un site public. |
| 400 | `url_private` | Rustine ne scanne que des sites publics, jamais une adresse locale ou privée. |
| 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 | `address_limit_reached` | Le projet suit déjà autant d'adresses que ton offre le permet. |
| 402 | `domain_taken` | Sur l'offre Gratuite, un domaine déjà suivi par un autre compte gratuit ne peut être ni ajouté ni scanné. |
| 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. |
| 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 | `site_excluded` | Le propriétaire de ce site a demandé à ne pas être scanné. Rustine ne le scannera pas. |
| 409 | `target_exists` | Le projet suit déjà cette adresse. |
| 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. |
| 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
