# Rustine docs (français) # Introduction > L'API et le serveur MCP de Rustine. Ton code ou ton agent lance les scans, lit les problèmes et récupère les correctifs, sans passer par l'app. Rustine trouve les failles de ton site ou de ton dépôt GitHub, te donne un score sur 100 et, pour chaque problème, un correctif prêt à coller dans ton agent de code. Tout ce que fait l'app, tu peux le faire d'ici : - **L'API REST** (`https://api.rustine.io/v1`) pour tes scripts, ta CI, n8n ou Make : lancer un scan après un déploiement, bloquer une mise en ligne si un problème critique apparaît, revérifier après un correctif. - **Le serveur MCP** (`https://mcp.rustine.io`) pour ton agent : Claude, ChatGPT, Claude Code, Cursor ou VS Code appellent Rustine eux-mêmes, et vérifient avant de te dire « c'est fini ». ## Par où commencer 1. [Premier appel en 2 minutes](https://docs.rustine.io/quickstart.md) : une clé, une commande `curl`, ta première réponse. 2. [Authentification](https://docs.rustine.io/authentication.md) : où mettre la clé (jamais dans l'adresse). 3. [La référence de l'API](https://docs.rustine.io/api.md) : chaque route, ses paramètres et ses exemples en curl, Node et Python. Ton agent préfère le MCP ? Va directement à [Brancher ton agent](https://docs.rustine.io/mcp/connect.md). ## Ce que Rustine ne fait pas Rustine ne scanne que les sites de **tes** projets, en lecture seule. Un audit complet ne part que sur un site dont tu as prouvé qu'il est à toi : la loi interdit de tester un site qu'on ne contrôle pas. Et Rustine ne touche jamais à ton code : le correctif est un texte, c'est ton agent ou toi qui l'appliquez. ## Pour les agents Chaque page existe en Markdown à la même adresse suivie de `.md`. [`/llms.txt`](https://docs.rustine.io/llms.txt) donne le sommaire, [`/llms-full.txt`](https://docs.rustine.io/llms-full.txt) toute la doc d'un bloc, et [`/openapi.json`](https://docs.rustine.io/openapi.json) la spécification de l'API. ## Exemples ### curl ```bash curl https://api.rustine.io/v1/account \ -H "Authorization: Bearer $RUSTINE_API_KEY" ``` ### Réponse : 200 OK ```json { "object": "account", "plan": "pro", "api_access": { "allowed": true, "trial_ends_at": null }, "quotas": { "quick_scans": { "limit": 300, "used": 12, "remaining": 288 }, "audits": { "limit": 180, "used": 4, "remaining": 176 }, "fixes": { "limit": 90, "used": 7, "remaining": 83 } } } ``` --- # Premier appel en 2 minutes > Crée une clé, range-la dans une variable d'environnement et appelle l'API. Puis lance ton premier scan. ## 1. Crée une clé Dans l'app, ouvre [Réglages › Clés d'API](https://app.rustine.io/fr/settings?section=apiKeys), donne un nom à la clé (« CI de monsaas ») et clique sur **Créer la clé**. Il te faut l'offre Pro, ou l'essai de l'offre Solo (voir [Accès](https://docs.rustine.io/access.md)), et la double authentification activée. La clé commence par `rst_` et ne s'affiche qu'une fois. Copie-la tout de suite. ## 2. Range-la dans une variable ```bash export RUSTINE_API_KEY="rst_..." ``` Dans ta CI, mets-la dans les secrets (GitHub Actions : **Settings › Secrets and variables › Actions**). Jamais dans un fichier versionné, jamais dans une adresse. ## 3. Appelle l'API La commande à droite liste tes projets. Si tu vois ton projet et son score, tout marche. Une réponse `401` veut dire que la clé est absente ou mal copiée : voir [Erreurs](https://docs.rustine.io/api/errors#invalid_api_key). ## 4. Lance un scan Prends l'`id` d'un site dans `targets` et lance un scan rapide : ```bash curl https://api.rustine.io/v1/targets/8d2e4f6a-9b1c-4d3e-a5f7-0b2c4d6e8f1a/scans \ -H "Authorization: Bearer $RUSTINE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"kind":"quick"}' ``` Le scan part en arrière-plan. Interroge [Suivre un scan](https://docs.rustine.io/api/scans/get.md) toutes les quelques secondes jusqu'à `done`, puis [liste les problèmes](https://docs.rustine.io/api/issues/list.md) du projet. ## Et ensuite - Le correctif d'un problème : [Débloquer le correctif](https://docs.rustine.io/api/issues/unlock-fix.md). Colle-le dans ton agent, déploie, puis [revérifie](https://docs.rustine.io/api/issues/recheck.md). - Ton agent peut faire tout ça seul : [Brancher ton agent](https://docs.rustine.io/mcp/connect.md). ## Exemples ### curl ```bash 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() console.log(page.data.map((p) => `${p.name}: ${p.score}/100`)) ``` ### 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() print([f"{p['name']}: {p['score']}/100" for p in page["data"]]) ``` ### Réponse : 200 OK ```json { "object": "list", "data": [ { "id": "4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b", "object": "project", "name": "Mon SaaS", "score": 62, "grade": "C", "open_issues": 3, "targets": [ { "id": "8d2e4f6a-9b1c-4d3e-a5f7-0b2c4d6e8f1a", "kind": "domain", "address": "app.monsaas.fr", "verified": true } ] } ], "has_more": false } ``` --- # Authentification > Une clé rst_ dans l'en-tête Authorization pour tes scripts, OAuth pour les agents. Jamais de clé dans une adresse. ## Les clés d'API Une clé se crée dans [Réglages › Clés d'API](https://app.rustine.io/fr/settings?section=apiKeys). Elle commence par `rst_`, vaut pour tout ton espace et ne s'affiche qu'une fois : Rustine n'en garde qu'une empreinte. Elle se passe **uniquement** dans l'en-tête : ```http Authorization: Bearer rst_... ``` Jamais dans l'adresse (`?key=…`) : une URL finit dans les journaux des serveurs, l'historique du navigateur et les captures d'écran. - Une clé par usage (« CI », « n8n », « script de déploiement ») : tu sais laquelle révoquer. - Une clé a toutes les permissions de ton espace : lire, scanner, débloquer un correctif, marquer un problème. - Les messages d'erreur et les textes des problèmes arrivent dans la langue du propriétaire de la clé. - Si une clé fuit, clique sur **Révoquer** : elle cesse de marcher aussitôt. Crée-en une autre. ## OAuth, pour les agents Claude, ChatGPT, Claude Code, Cursor et VS Code se branchent sur le [serveur MCP](https://docs.rustine.io/mcp/connect.md) avec OAuth : pas de clé à copier, tu cliques sur **Autoriser** et l'agent renouvelle son accès seul. Tu vois chaque agent branché dans **Réglages › Agents connectés**, et tu le déconnectes d'un clic. Le jeton d'accès OAuth (`rat_…`) marche aussi sur l'API, dans le même en-tête. Il n'a que les permissions accordées à l'agent : chaque page de la référence dit laquelle la route demande (`scans:read`, `scans:run`, `fixes:unlock` ou `issues:write`). Sans elle, la réponse est `403 scope_missing`. ## Les conditions - Une offre qui ouvre l'API et le MCP : Pro, ou l'essai de l'offre Solo ([Accès](https://docs.rustine.io/access.md)). - La double authentification activée sur ton compte Rustine, pour créer une clé ou autoriser un agent. ## Les refus | Statut | `error.code` | Ce qui se passe | | --- | --- | --- | | 401 | [`authentication_required`](https://docs.rustine.io/api/errors#authentication_required) | Aucune clé dans la requête. | | 401 | [`invalid_api_key`](https://docs.rustine.io/api/errors#invalid_api_key) | Clé fausse, révoquée ou expirée. | | 403 | [`scope_missing`](https://docs.rustine.io/api/errors#scope_missing) | Le jeton OAuth n'a pas la permission. | | 402 | [`api_plan_required`](https://docs.rustine.io/api/errors#api_plan_required) | L'offre n'ouvre pas l'API. | ## Exemples ### curl ```bash curl https://api.rustine.io/v1/account \ -H "Authorization: Bearer $RUSTINE_API_KEY" ``` ### Réponse : 401 Unauthorized ```json { "error": { "type": "authentication", "code": "invalid_api_key", "message": "Cette clé d'API ou ce jeton est invalide, révoqué ou expiré.", "doc_url": "https://docs.rustine.io/api/errors#invalid_api_key" } } ``` --- # Erreurs > Le format des erreurs, quand réessayer, et chaque code que l'API peut renvoyer. Une erreur a toujours la même forme, quel que soit l'appel : ```json { "error": { "type": "invalid_request", "code": "parameter_invalid", "message": "Paramètre invalide : limit.", "param": "limit", "doc_url": "https://docs.rustine.io/api/errors#parameter_invalid" } } ``` - `type` : la catégorie, pour savoir qui doit agir (toi, ton offre, ou Rustine). - `code` : le cas précis. C'est lui qu'il faut tester dans ton code. - `message` : une phrase pour un humain, dans la langue du propriétaire de la clé. Journalise-la, ne la teste pas : sa formulation peut changer. - `param` : avec `invalid_request`, le paramètre en cause. - `doc_url` : la ligne de cette page qui explique le code. - `upgrade_url` : avec `plan_limit`, où changer d'offre ou acheter un pack. Chaque réponse porte aussi un en-tête `Request-Id` : donne-le au support si une erreur revient. ## Quand réessayer - `429` : oui, après le nombre de secondes de l'en-tête `Retry-After`. - `500`, `502` et `503` : oui, un peu plus tard. Pour une écriture, renvoie la même [`Idempotency-Key`](https://docs.rustine.io/idempotency.md) : l'action ne sera pas faite deux fois. - `409 idempotency_in_progress` : oui, dans un instant. - Les autres `4xx` : non. La même requête recevra la même réponse ; corrige-la d'abord, ou change d'offre pour un `402`. ## Tous les codes | error.code | HTTP | error.type | Ce que ça veut dire | | --- | --- | --- | --- | | `body_invalid` | 400 | `invalid_request` | Le corps de la requête n'est pas du JSON valable. Vérifie l'en-tête `Content-Type: application/json`. | | `cursor_invalid` | 400 | `invalid_request` | `starting_after` n'est l'identifiant d'aucun élément de cette liste. | | `idempotency_key_invalid` | 400 | `invalid_request` | L'en-tête `Idempotency-Key` doit faire de 1 à 255 caractères imprimables. | | `parameter_invalid` | 400 | `invalid_request` | Un paramètre manque ou n'est pas valable ; `param` le nomme. | | `reason_required` | 400 | `invalid_request` | Marquer un faux positif ou un risque accepté demande une raison, de 10 à 500 caractères. | | `target_kind_invalid` | 400 | `invalid_request` | Le type de scan ne va pas avec la cible : `repository` pour un dépôt GitHub, `quick` ou `audit` pour un site. | | `url_invalid` | 400 | `invalid_request` | L'adresse n'est pas l'URL valable d'un site public. | | `url_private` | 400 | `invalid_request` | Rustine ne scanne que des sites publics, jamais une adresse locale ou privée. | | `authentication_required` | 401 | `authentication` | Aucune clé dans la requête. Passe-la dans l'en-tête `Authorization: Bearer rst_…`, jamais dans l'adresse. | | `invalid_api_key` | 401 | `authentication` | 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é. | | `address_limit_reached` | 402 | `plan_limit` | Le projet suit déjà autant d'adresses que ton offre le permet. | | `api_plan_required` | 402 | `plan_limit` | L'espace est sur l'offre Gratuite. L'API vient avec Pro, ou à l'essai 14 jours sur Solo. | | `api_trial_ended` | 402 | `plan_limit` | L'essai de 14 jours de Solo est terminé. Passe à Pro : tes clés remarchent aussitôt. | | `audit_not_in_plan` | 402 | `plan_limit` | L'offre Gratuite ne fait que des scans rapides ; l'audit complet vient avec Solo et Pro. | | `audit_quota_exceeded` | 402 | `plan_limit` | Les audits complets de la période sont épuisés, et il ne reste aucun crédit de pack. | | `domain_taken` | 402 | `plan_limit` | Sur l'offre Gratuite, un domaine déjà suivi par un autre compte gratuit ne peut être ni ajouté ni scanné. | | `fix_quota_exceeded` | 402 | `plan_limit` | Les correctifs de la période sont épuisés, et il ne reste aucun crédit de pack. Un correctif déjà débloqué reste lisible. | | `plan_limit` | 402 | `plan_limit` | Ton offre ne comprend pas cette action. `upgrade_url` mène à l'offre qui la permet. | | `project_limit_reached` | 402 | `plan_limit` | Tous les projets de ton offre sont pris. Ajoute un projet en plus ou change d'offre. | | `project_paused` | 402 | `plan_limit` | 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. | | `repositories_not_in_plan` | 402 | `plan_limit` | L'analyse des dépôts GitHub vient avec Solo et Pro. | | `scan_quota_exceeded` | 402 | `plan_limit` | Les scans rapides de la période sont épuisés, et il ne reste aucun crédit de pack. | | `scope_missing` | 403 | `authentication` | 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. | | `fix_locked` | 404 | `not_found` | Le correctif de ce problème n'est pas encore débloqué : `POST /issues/{issue_id}/fix` le débloque. | | `resource_not_found` | 404 | `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. | | `route_not_found` | 404 | `not_found` | Cette route n'existe pas. Vérifie la méthode et le chemin dans la référence. | | `conflict` | 409 | `conflict` | La requête ne va pas avec l'état actuel de l'objet. Relis-le, puis réessaie. | | `github_unavailable` | 409 | `conflict` | L'analyse des dépôts GitHub n'est pas disponible pour le moment. | | `idempotency_conflict` | 409 | `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é. | | `idempotency_in_progress` | 409 | `conflict` | Une requête avec cette `Idempotency-Key` est encore en cours. Réessaie dans un instant. | | `issue_already_fixed` | 409 | `conflict` | Le problème est corrigé : il n'y a rien à marquer. | | `proof_required` | 409 | `conflict` | Le site doit d'abord être prouvé comme le tien (audit complet, correctif, marquage). Prouve-le dans l'app : DNS, fichier ou balise meta. | | `site_excluded` | 409 | `conflict` | Le propriétaire de ce site a demandé à ne pas être scanné. Rustine ne le scannera pas. | | `target_exists` | 409 | `conflict` | Le projet suit déjà cette adresse. | | `rate_limited` | 429 | `rate_limited` | Trop de requêtes pour cette clé (120 par minute). Attends `Retry-After` secondes. | | `recheck_limit_reached` | 429 | `rate_limited` | Trop de revérifications sur ce projet aujourd'hui (60 par jour). Réessaie demain. | | `repository_analyses_limit_reached` | 429 | `rate_limited` | L'espace a lancé beaucoup d'analyses de dépôt ces dernières 24 heures. Réessaie plus tard. | | `internal_error` | 500 | `api_error` | Une erreur de notre côté, signalée automatiquement. Réessaie plus tard ; donne le `Request-Id` au support si elle revient. | | `recheck_failed` | 502 | `api_error` | La revérification n'a pas pu aller au bout (site injoignable ?). Vérifie que le site répond, puis réessaie. | | `platform_busy` | 503 | `rate_limited` | Rustine ne lance pas de nouveau scan pendant quelques minutes. Réessaie plus tard ; rien n'a été décompté. | | `service_unavailable` | 503 | `api_error` | Rustine est indisponible un instant. Réessaie avec la même `Idempotency-Key`. | ## Les types Teste `type` pour décider quoi faire, `code` pour savoir exactement ce qui s'est passé. | error.type | Ce que ça veut dire | | --- | --- | | `invalid_request` | La requête elle-même est à corriger. | | `authentication` | La clé ou le jeton ne passe pas, ou n'a pas la permission. | | `plan_limit` | L'offre ou le quota ne le permet pas ; `upgrade_url` dit quoi faire. | | `not_found` | L'objet ou la route n'existe pas pour cette clé. | | `rate_limited` | Trop de requêtes, ou un plafond atteint : attends un peu. | | `conflict` | L'action ne va pas avec l'état actuel (site non prouvé, clé déjà servie…). | | `api_error` | Un souci côté Rustine : réessaie. | ## Exemples ### Node ```js const res = await fetch(url, { headers }) if (!res.ok) { const { error } = await res.json() if (error.type === "plan_limit") { console.log(`Offre ou quota : ${error.upgrade_url}`) } else if (res.status === 429 || res.status >= 500) { const wait = Number(res.headers.get("Retry-After") ?? 30) await new Promise((r) => setTimeout(r, wait * 1000)) // ...puis réessaie, avec la même Idempotency-Key } else { throw new Error(`${error.code}: ${error.message}`) } } ``` ### Réponse : 402 Payment Required ```json { "error": { "type": "plan_limit", "code": "audit_quota_exceeded", "message": "Les audits complets de la période sont épuisés.", "doc_url": "https://docs.rustine.io/api/errors#audit_quota_exceeded", "upgrade_url": "https://app.rustine.io/settings?section=offer" } } ``` --- # Pagination > Les listes arrivent par pages. limit fixe la taille, starting_after reprend après le dernier élément, has_more dit s'il en reste. Toutes les routes qui renvoient plusieurs éléments répondent avec la même enveloppe : ```json { "object": "list", "data": [...], "has_more": true } ``` ## Les paramètres - `limit` : combien d'éléments par page, de 1 à 100. 20 par défaut. - `starting_after` : l'`id` du dernier élément de la page précédente. La page suivante commence juste après. Quand `has_more` vaut `false`, tu as tout. L'ordre ne change pas d'une page à l'autre : les problèmes du plus grave au moins grave, les scans du plus récent au plus ancien. Le curseur suit un élément, pas un numéro de page : si un nouveau scan arrive pendant que tu parcours la liste, tu ne vois pas deux fois le même élément. Un `starting_after` qui n'est pas dans la liste répond `400 cursor_invalid`. ## Exemples ### Node ```js const project = "4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b" const issues = [] let after do { const url = new URL(`https://api.rustine.io/v1/projects/${project}/issues`) url.searchParams.set("limit", "100") if (after) url.searchParams.set("starting_after", after) const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.RUSTINE_API_KEY}` }, }) const page = await res.json() issues.push(...page.data) after = page.has_more ? page.data.at(-1).id : undefined } while (after) ``` ### Python ```python import os import requests project = "4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b" issues, after = [], None while True: params = {"limit": 100} if after: params["starting_after"] = after page = requests.get( f"https://api.rustine.io/v1/projects/{project}/issues", headers={"Authorization": f"Bearer {os.environ['RUSTINE_API_KEY']}"}, params=params, ).json() issues += page["data"] if not page["has_more"]: break after = page["data"][-1]["id"] ``` ### Réponse : 200 OK ```json { "object": "list", "data": [ { "id": "a3c5e7f9-1b2d-4c6e-8f0a-2b4d6f8a1c3e", "object": "issue" } ], "has_more": true } ``` --- # Limites et quotas > 120 requêtes par minute et par clé, les quotas de ton offre sur chaque période, et les quelques plafonds qui protègent Rustine et les sites scannés. ## Le débit : 120 requêtes par minute et par clé Chaque réponse le dit dans ses en-têtes : `X-RateLimit-Limit` (le plafond), `X-RateLimit-Remaining` (ce qu'il reste dans la minute) et `X-RateLimit-Reset` (quand la fenêtre repart, en secondes Unix). Au-delà, l'API répond `429 rate_limited` avec `Retry-After` : attends ce délai et reprends. Le serveur MCP suit la même limite. Pour suivre un scan, une requête toutes les 3 à 5 secondes suffit largement. ## Les quotas de ton offre Ton offre te donne des scans rapides, des audits complets et des correctifs **par projet**, sur une période de 30 jours ; chaque projet en plus ajoute sa part. Une fois le quota de la période épuisé, Rustine prend sur tes crédits de packs s'il t'en reste. Sinon, l'action répond `402` (`scan_quota_exceeded`, `audit_quota_exceeded` ou `fix_quota_exceeded`) avec `upgrade_url`. [Ton offre et tes quotas](https://docs.rustine.io/api/account.md) te dit où tu en es. Ce qui ne coûte rien : - lire tes projets, tes scans et tes problèmes ; - relire un correctif déjà débloqué ; - relancer un scan du même type sur le même site dans l'heure : Rustine renvoie le résultat récent (`reused: true`) sans rien décompter ; - analyser un dépôt GitHub ; - revérifier un problème, dans la limite de 60 revérifications par projet et par jour. ## Les plafonds de Rustine Quelques plafonds protègent la plateforme et les sites scannés. Tu ne devrais pas les croiser, mais ton code doit savoir quoi faire : - **Trop de scans en même temps sur Rustine**, ou plafond du jour atteint pour toute la plateforme : un nouveau scan reçoit `503 platform_busy`. Réessaie dans quelques minutes ; rien n'a été décompté. - **Revérifications** : 60 par projet et par jour, puis `429 recheck_limit_reached`. - **Analyses de dépôt** : un nombre limité par espace sur 24 heures, poussées comprises, puis `429 repository_analyses_limit_reached`. Une revérification sur un dépôt relance au plus une analyse par quart d'heure. Ces plafonds peuvent être resserrés en cas d'abus, sans préavis : traite toujours `429` et `503` comme « réessaie plus tard ». | Offre | Projets compris | Scans rapides | Audits complets | Correctifs | Adresses par projet | API et MCP | | --- | --- | --- | --- | --- | --- | --- | | Gratuit | 1 | 10 | 0 | 3 offerts, une fois | 1 | non | | Solo | 1 | 30 | 60 | 15 | 5 | essai de 14 jours | | Pro | 3 | 100 | 60 | 30 | 20 | inclus | Scans rapides, audits complets, correctifs : par projet et par mois. ## Exemples ### curl ```bash curl -i https://api.rustine.io/v1/projects \ -H "Authorization: Bearer $RUSTINE_API_KEY" ``` ### Réponse : 429 Too Many Requests ```http HTTP/1.1 429 Too Many Requests Retry-After: 12 X-RateLimit-Limit: 120 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1792061520 { "error": { "type": "rate_limited", "code": "rate_limited", "message": "Trop de requêtes pour cette clé (120 par minute).", "doc_url": "https://docs.rustine.io/api/errors#rate_limited" } } ``` --- # Idempotence > Envoie une Idempotency-Key avec tes POST. Si la requête est rejouée (timeout, CI relancée), Rustine renvoie la première réponse au lieu de refaire l'action. Un réseau coupe, un job de CI est relancé, ton script part en timeout : tu ne sais pas si l'action a eu lieu. Avec une clé d'idempotence, tu peux renvoyer la même requête sans risque. ## Comment faire Ajoute l'en-tête `Idempotency-Key` à tes requêtes `POST`, avec une valeur unique pour l'action (1 à 255 caractères imprimables) : l'identifiant de ton déploiement, de ton job, ou un UUID. ```http Idempotency-Key: deploy-4f9c2e1 ``` - **Même clé, même requête, dans les 24 heures** : Rustine renvoie la première réponse, avec l'en-tête `Idempotent-Replayed: true`. Rien n'est refait, rien n'est décompté une seconde fois. - **Même clé, autre requête** (méthode, chemin ou corps) : `409 idempotency_conflict`. Une nouvelle action prend une nouvelle clé. - **Même clé pendant que la première requête tourne encore** : `409 idempotency_in_progress`. Réessaie dans un instant. - Une clé mal formée : `400 idempotency_key_invalid`. Les `GET` et le `PATCH` d'un problème sont déjà sans effet en double : pas besoin de clé. ## À quoi ça sert le plus - **Débloquer un correctif** : le rejouer ne décompte jamais deux correctifs. - **Lancer un scan** depuis la CI : un job relancé ne lance pas un second scan. (Sans clé, un scan du même type sur le même site dans l'heure renvoie déjà le résultat récent, avec `reused: true`.) - **Créer un projet ou ajouter un site** : pas de doublon si la première réponse s'est perdue. - **Après une `500` ou une `503`** : renvoie la même requête avec la même clé. ## Exemples ### curl ```bash curl -i https://api.rustine.io/v1/issues/a3c5e7f9-1b2d-4c6e-8f0a-2b4d6f8a1c3e/fix \ -X POST \ -H "Authorization: Bearer $RUSTINE_API_KEY" \ -H "Idempotency-Key: fix-$GITHUB_RUN_ID" ``` ### Réponse : 200 OK (rejouée) ```http HTTP/1.1 200 OK Idempotent-Replayed: true { "object": "fix", "issue_id": "a3c5e7f9-1b2d-4c6e-8f0a-2b4d6f8a1c3e", "charged": true, "fixes_left": 82 } ``` --- # Versions > L'API est en v1. Ce qui peut changer sans prévenir (des ajouts), ce qui ne changera jamais dans la v1, et l'en-tête Rustine-Version. La version majeure est dans l'adresse : `/v1`. Chaque réponse porte en plus l'en-tête `Rustine-Version`, la date du contrat qui a répondu (`2026-10-13` aujourd'hui). Journalise-le : si un comportement te surprend, il dit quelle version du contrat l'a produit. ## Ce que Rustine promet dans la v1 Rien de ce qui marche aujourd'hui ne casse : - aucun champ retiré ni renommé, aucun type changé ; - aucun nouveau paramètre obligatoire ; - aucune route retirée ; - le format des erreurs et des listes reste le même, et un `code` d'erreur garde son sens. ## Ce qui peut arriver sans prévenir Des ajouts, que ton code doit savoir ignorer. La date de `Rustine-Version` avance alors : - de nouvelles routes ; - de nouveaux champs dans les réponses ; - de nouveaux paramètres optionnels ; - de nouvelles valeurs dans une liste fermée (`status`, `severity`, `family`…) : prévois un cas « autre » ; - de nouveaux codes d'erreur : traite un code inconnu selon son `type`. ## Une v2, un jour Un changement qui casserait ton code passera par une nouvelle version, `/v2`, annoncée dans le [Changelog](https://docs.rustine.io/changelog.md) et par e-mail aux détenteurs d'une clé. La v1 continuera de répondre **au moins 12 mois** après cette annonce, le temps que tu migres ; la date de son arrêt sera donnée dès l'annonce. ## Exemples ### curl ```bash curl -sI https://api.rustine.io/v1/account \ -H "Authorization: Bearer $RUSTINE_API_KEY" | grep -i rustine-version ``` ### Réponse : 200 OK ```http Rustine-Version: 2026-10-13 ``` --- # Accès > L'API et le serveur MCP sont inclus dans l'offre Pro, et à l'essai pendant 14 jours sur l'offre Solo. | Offre | API et serveur MCP | | --- | --- | | Gratuit | non (`402 api_plan_required`) | | Solo | essai de 14 jours, une seule fois | | Pro | inclus | ## L'essai de Solo Sur l'offre Solo, l'essai dure 14 jours, une seule fois par espace. Il commence au premier appel de ton script ou de ton agent (API ou MCP), pas à la création de la clé : tu peux préparer ton intégration avant. [Ton offre et tes quotas](https://docs.rustine.io/api/account.md) donne sa date de fin (`api_access.trial_ends_at`). À la fin de l'essai, l'API répond `402 api_trial_ended` avec `upgrade_url`, et le serveur MCP répond par une erreur d'outil lisible (jamais un `403`, que les agents prennent pour « reconnecte-toi »). Passe à Pro : tes clés et tes agents branchés repartent tout de suite, sans rien reconfigurer. ## Créer une clé Une clé ne se crée que si ton offre ouvre l'API (Pro, ou Solo tant que l'essai n'a pas commencé ou court), avec la double authentification activée. Les clés créées avant restent listées et révocables. [Créer une clé](https://app.rustine.io/fr/settings?section=apiKeys) · [Comparer les offres](https://rustine.io/aide/offres) --- # Référence de l'API > L'adresse, les formats et les conventions communes à toutes les routes de l'API v1, puis chaque route une par une. **Adresse : `https://api.rustine.io/v1`** - **Authentification** : `Authorization: Bearer rst_…` ([Authentification](https://docs.rustine.io/authentication.md)). - **Format** : JSON en entrée et en sortie, champs en `snake_case`, dates en ISO 8601 (UTC), identifiants en UUID. - **Listes** : `{ "object": "list", "data": [...], "has_more": false }`, avec `limit` et `starting_after` ([Pagination](https://docs.rustine.io/pagination.md)). - **Erreurs** : `{ "error": { "type", "code", "message", "doc_url" } }` ([Erreurs](https://docs.rustine.io/api/errors.md)). - **Écritures** : envoie une `Idempotency-Key` sur les `POST` ([Idempotence](https://docs.rustine.io/idempotency.md)). - **Langue** : les messages d'erreur et les textes des problèmes arrivent dans la langue du propriétaire de la clé. Chaque réponse porte `Request-Id` (à donner au support), `Rustine-Version` (la version du contrat qui a répondu, voir [Versions](https://docs.rustine.io/versioning.md)) et les en-têtes `X-RateLimit-*` ([Limites et quotas](https://docs.rustine.io/rate-limits.md)). La spécification complète est servie telle quelle sur [`/openapi.json`](https://docs.rustine.io/openapi.json) (OpenAPI 3.1) : importe-la dans Postman, Insomnia ou un générateur de client. ## Les routes - `GET /projects`: [Lister les projets](https://docs.rustine.io/api/projects/list.md) - `POST /projects`: [Créer un projet](https://docs.rustine.io/api/projects/create.md) - `GET /projects/{project_id}`: [Lire un projet](https://docs.rustine.io/api/projects/get.md) - `POST /projects/{project_id}/targets`: [Ajouter un site](https://docs.rustine.io/api/projects/add-site.md) - `POST /targets/{target_id}/scans`: [Lancer un scan](https://docs.rustine.io/api/scans/create.md) - `GET /scans/{scan_id}`: [Suivre un scan](https://docs.rustine.io/api/scans/get.md) - `GET /projects/{project_id}/scans`: [Historique des scans](https://docs.rustine.io/api/scans/list.md) - `GET /projects/{project_id}/issues`: [Lister les problèmes](https://docs.rustine.io/api/issues/list.md) - `GET /issues/{issue_id}`: [Lire un problème](https://docs.rustine.io/api/issues/get.md) - `POST /issues/{issue_id}/fix`: [Débloquer le correctif](https://docs.rustine.io/api/issues/unlock-fix.md) - `GET /issues/{issue_id}/fix`: [Relire le correctif](https://docs.rustine.io/api/issues/fix.md) - `POST /issues/{issue_id}/recheck`: [Revérifier](https://docs.rustine.io/api/issues/recheck.md) - `PATCH /issues/{issue_id}`: [Marquer un problème](https://docs.rustine.io/api/issues/update.md) - `GET /account`: [Ton offre et tes quotas](https://docs.rustine.io/api/account.md) ## Exemples ### curl ```bash curl -i https://api.rustine.io/v1/account \ -H "Authorization: Bearer $RUSTINE_API_KEY" ``` ### Réponse : 200 OK ```http HTTP/1.1 200 OK Content-Type: application/json Request-Id: req_6f1c0d9a2b7e4c3d5a8b9e0f Rustine-Version: 2026-10-13 X-RateLimit-Limit: 120 X-RateLimit-Remaining: 119 X-RateLimit-Reset: 1792061520 ``` --- # 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 --- # Créer un projet Crée un projet, avec l'adresse de son premier site si tu la donnes. Le site n'est pas encore prouvé : tu peux y lancer un scan `quick` tout de suite, l'audit complet attendra la preuve. ``` POST https://api.rustine.io/v1/projects ``` Permission OAuth : `scans:run` · une clé d'API les a toutes > N'ajoute qu'un site qui est à toi ou que tu as le droit de tester. Un projet compte dans ton offre (1 compris en Solo, 3 en Pro, puis des projets en plus) : au-delà, `402 project_limit_reached`. ## 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 - `name` (string, obligatoire) Le nom du projet, de 1 à 80 caractères. - `url` (string) Le premier site du projet (optionnel). Seulement un site qui est à toi ou que tu as le droit de tester. ## Réponse (201 Created) - `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. ## 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" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"name":"Example SaaS","url":"https://app.example.com"}' ``` ### Node ```js const res = await fetch( "https://api.rustine.io/v1/projects", { method: "POST", headers: { Authorization: `Bearer ${process.env.RUSTINE_API_KEY}`, "Idempotency-Key": crypto.randomUUID(), "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example SaaS", url: "https://app.example.com" }), }, ) const project = await res.json() ``` ### Python ```python import os import uuid import requests project = requests.post( "https://api.rustine.io/v1/projects", headers={ "Authorization": f"Bearer {os.environ['RUSTINE_API_KEY']}", "Idempotency-Key": str(uuid.uuid4()), }, json={"name": "Example SaaS", "url": "https://app.example.com"}, ).json() ``` ### Réponse : 201 Created ```json { "id": "4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b", "object": "project", "name": "Example SaaS", "score": null, "grade": null, "open_issues": 0, "paused": false, "last_scan_at": null, "targets": [ { "id": "8d2e4f6a-9b1c-4d3e-a5f7-0b2c4d6e8f1a", "object": "target", "project_id": "4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b", "kind": "domain", "address": "app.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 | `project_limit_reached` | Tous les projets de ton offre sont pris. Ajoute un projet en plus ou change d'offre. | | 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. | | 409 | `site_excluded` | Le propriétaire de ce site a demandé à ne pas être scanné. Rustine ne le scannera pas. | | 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 --- # Lire un projet Un projet, ses sites et son dernier score. Pratique après un scan pour voir la note bouger. ``` GET https://api.rustine.io/v1/projects/{project_id} ``` Permission OAuth : `scans:read` · une clé d'API les a toutes ## Paramètres de chemin - `project_id` (string, obligatoire) Le projet. ## Réponse (200 OK) - `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. ## 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 \ -H "Authorization: Bearer $RUSTINE_API_KEY" ``` ### Node ```js const res = await fetch( "https://api.rustine.io/v1/projects/4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b", { headers: { Authorization: `Bearer ${process.env.RUSTINE_API_KEY}`, }, }, ) const project = await res.json() ``` ### Python ```python import os import requests project = requests.get( "https://api.rustine.io/v1/projects/4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b", headers={ "Authorization": f"Bearer {os.environ['RUSTINE_API_KEY']}", }, ).json() ``` ### Réponse : 200 OK ```json { "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 } ] } ``` ## Erreurs | Code | error.code | Ce qui se passe | | --- | --- | --- | | 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. | | 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. | | 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 --- # 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 --- # 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 --- # Suivre un scan Où en est un scan. Interroge cette route toutes les 3 à 5 secondes jusqu'à `done` ou `failed` ; un scan rapide prend en général moins d'une minute. ``` GET https://api.rustine.io/v1/scans/{scan_id} ``` Permission OAuth : `scans:read` · une clé d'API les a toutes ## Paramètres de chemin - `scan_id` (string, obligatoire) Le scan. ## Réponse (200 OK) - `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/scans/e7f9a1b3-5c7d-4e9f-b1a3-c5d7e9f1a3b5 \ -H "Authorization: Bearer $RUSTINE_API_KEY" ``` ### Node ```js const res = await fetch( "https://api.rustine.io/v1/scans/e7f9a1b3-5c7d-4e9f-b1a3-c5d7e9f1a3b5", { headers: { Authorization: `Bearer ${process.env.RUSTINE_API_KEY}`, }, }, ) const scan = await res.json() ``` ### Python ```python import os import requests scan = requests.get( "https://api.rustine.io/v1/scans/e7f9a1b3-5c7d-4e9f-b1a3-c5d7e9f1a3b5", headers={ "Authorization": f"Bearer {os.environ['RUSTINE_API_KEY']}", }, ).json() ``` ### Réponse : 200 OK ```json { "id": "e7f9a1b3-5c7d-4e9f-b1a3-c5d7e9f1a3b5", "object": "scan", "target_id": "8d2e4f6a-9b1c-4d3e-a5f7-0b2c4d6e8f1a", "kind": "audit", "status": "done", "score": 62, "grade": "C", "coverage": 0.94, "audit_fallback": false, "reused": false, "next_real_scan_at": null, "created_at": "2026-10-13T08:41:02.000Z", "finished_at": "2026-10-13T08:42:10.000Z" } ``` ## Erreurs | Code | error.code | Ce qui se passe | | --- | --- | --- | | 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. | | 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. | | 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 --- # Historique des scans Les scans d'un projet, du plus récent au plus ancien. La profondeur de l'historique dépend de ton offre. ``` GET https://api.rustine.io/v1/projects/{project_id}/scans ``` Permission OAuth : `scans:read` · une clé d'API les a toutes ## Paramètres de chemin - `project_id` (string, obligatoire) Le projet. ## 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 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. - `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/4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b/scans \ -H "Authorization: Bearer $RUSTINE_API_KEY" ``` ### Node ```js const res = await fetch( "https://api.rustine.io/v1/projects/4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b/scans", { 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/4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b/scans", headers={ "Authorization": f"Bearer {os.environ['RUSTINE_API_KEY']}", }, ).json() ``` ### Réponse : 200 OK ```json { "object": "list", "data": [ { "id": "e7f9a1b3-5c7d-4e9f-b1a3-c5d7e9f1a3b5", "object": "scan", "target_id": "8d2e4f6a-9b1c-4d3e-a5f7-0b2c4d6e8f1a", "kind": "audit", "status": "done", "score": 62, "grade": "C", "coverage": 0.94, "audit_fallback": false, "reused": false, "next_real_scan_at": null, "created_at": "2026-10-13T08:41:02.000Z", "finished_at": "2026-10-13T08:42:10.000Z" } ], "has_more": true } ``` ## 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. | | 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. | | 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 --- # Lister les problèmes Les problèmes d'un projet, du plus grave au moins grave, avec leur niveau de confiance. Filtre par statut, gravité ou site. ``` GET https://api.rustine.io/v1/projects/{project_id}/issues ``` Permission OAuth : `scans:read` · une clé d'API les a toutes > Les problèmes d'un site pas encore prouvé restent masqués : le site donne seulement leur nombre (`targets[].hidden_issues` dans [Lire un projet](https://docs.rustine.io/api/projects/get)), jusqu'à ce que tu prouves qu'il est à toi. ## Paramètres de chemin - `project_id` (string, obligatoire) Le projet. ## 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. - `status` (string) Seulement les problèmes dans ce statut. (`open`, `fixed`, `false_positive`, `accepted_risk`) - `severity` (string) Seulement les problèmes de cette gravité. (`critical`, `high`, `medium`, `low`, `info`) - `target_id` (string) Seulement les problèmes de ce site ou de ce dépôt. ## 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 problème ; il le garde d'un scan à l'autre. - `object` (string, obligatoire) Le type d'objet, toujours le même pour une route donnée. - `project_id` (string, obligatoire) Le projet du problème. - `target_id` (string, obligatoire) Le site ou le dépôt où il a été trouvé. - `address` (string, obligatoire) Le nom d'hôte du site ou `propriétaire/dépôt`. - `control_id` (string, obligatoire) Le contrôle qui l'a trouvé (identifiant stable). - `family` (string, obligatoire) La famille du contrôle : base de données, secrets, fichiers exposés… (`database`, `secrets`, `exposedFiles`, `personalData`, `vulnerableVersions`, `configuration`, `compliance`, `server`, `code`) - `severity` (string, obligatoire) La gravité, de `critical` à `info`. (`critical`, `high`, `medium`, `low`, `info`) - `confidence` (string, obligatoire) À quel point Rustine est sûr : `confirmed` (vu), `likely` (très probable), `estimated` (déduit, par exemple d'une version). (`confirmed`, `likely`, `estimated`) - `status` (string, obligatoire) `open` ; `fixed` (un scan ou une revérification ne le trouve plus) ; `false_positive` (sorti du score) ; `accepted_risk` (reste dans le score). (`open`, `fixed`, `false_positive`, `accepted_risk`) - `title` (string, obligatoire) Le problème en une ligne, dans la langue du propriétaire de la clé. - `explanation` (string, obligatoire) Le risque en mots simples, dans la langue du propriétaire de la clé. - `detail` (string, obligatoire) Le détail technique, dans la langue du propriétaire de la clé. - `location` (string, obligatoire) Où : un chemin, une table, ou `fichier:ligne@commit` dans un dépôt. - `file` (string | null, obligatoire) Problème de dépôt : le fichier. - `line` (integer | null, obligatoire) Problème de dépôt : la ligne. - `commit` (string | null, obligatoire) Problème de dépôt : le commit analysé. - `evidence` (string, obligatoire) Ce que le contrôle a observé, secrets masqués. - `fix_unlocked` (boolean, obligatoire) Vrai si son correctif est débloqué : le relire ne coûte rien. - `status_reason` (string | null, obligatoire) Pourquoi il a été marqué faux positif ou risque accepté. - `marked_by` (string | null, obligatoire) Qui l'a marqué : un agent, le nom d'une clé d'API, ou `app`. - `first_seen_at` (string, obligatoire) Le premier scan qui l'a trouvé. - `last_seen_at` (string, obligatoire) Le dernier scan qui l'a trouvé. - `fixed_at` (string | null, obligatoire) Quand il a été trouvé corrigé. - `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/4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b/issues \ -H "Authorization: Bearer $RUSTINE_API_KEY" ``` ### Node ```js const res = await fetch( "https://api.rustine.io/v1/projects/4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b/issues", { 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/4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b/issues", headers={ "Authorization": f"Bearer {os.environ['RUSTINE_API_KEY']}", }, ).json() ``` ### Réponse : 200 OK ```json { "object": "list", "data": [ { "id": "2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f", "object": "issue", "project_id": "4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b", "target_id": "8d2e4f6a-9b1c-4d3e-a5f7-0b2c4d6e8f1a", "address": "app.example.com", "control_id": "exposed-env-file", "family": "exposedFiles", "severity": "high", "confidence": "confirmed", "status": "open", "title": "Your .env file is readable online.", "explanation": "Your secrets are served to everyone.", "detail": "GET /.env → 200", "location": "/.env", "file": null, "line": null, "commit": null, "evidence": "GET /.env → 200 (STRIPE_SECRET_KEY=sk_live_••••)", "fix_unlocked": false, "status_reason": null, "marked_by": null, "first_seen_at": "2026-10-13T08:42:10.000Z", "last_seen_at": "2026-10-13T08:42:10.000Z", "fixed_at": 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. | | 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. | | 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 --- # Lire un problème Un problème avec sa gravité, ce que Rustine a observé et l'endroit où il l'a vu. Le correctif se demande à part, avec [Débloquer le correctif](https://docs.rustine.io/api/issues/unlock-fix). ``` GET https://api.rustine.io/v1/issues/{issue_id} ``` Permission OAuth : `scans:read` · une clé d'API les a toutes ## Paramètres de chemin - `issue_id` (string, obligatoire) Le problème. ## Réponse (200 OK) - `id` (string, obligatoire) L'identifiant du problème ; il le garde d'un scan à l'autre. - `object` (string, obligatoire) Le type d'objet, toujours le même pour une route donnée. - `project_id` (string, obligatoire) Le projet du problème. - `target_id` (string, obligatoire) Le site ou le dépôt où il a été trouvé. - `address` (string, obligatoire) Le nom d'hôte du site ou `propriétaire/dépôt`. - `control_id` (string, obligatoire) Le contrôle qui l'a trouvé (identifiant stable). - `family` (string, obligatoire) La famille du contrôle : base de données, secrets, fichiers exposés… (`database`, `secrets`, `exposedFiles`, `personalData`, `vulnerableVersions`, `configuration`, `compliance`, `server`, `code`) - `severity` (string, obligatoire) La gravité, de `critical` à `info`. (`critical`, `high`, `medium`, `low`, `info`) - `confidence` (string, obligatoire) À quel point Rustine est sûr : `confirmed` (vu), `likely` (très probable), `estimated` (déduit, par exemple d'une version). (`confirmed`, `likely`, `estimated`) - `status` (string, obligatoire) `open` ; `fixed` (un scan ou une revérification ne le trouve plus) ; `false_positive` (sorti du score) ; `accepted_risk` (reste dans le score). (`open`, `fixed`, `false_positive`, `accepted_risk`) - `title` (string, obligatoire) Le problème en une ligne, dans la langue du propriétaire de la clé. - `explanation` (string, obligatoire) Le risque en mots simples, dans la langue du propriétaire de la clé. - `detail` (string, obligatoire) Le détail technique, dans la langue du propriétaire de la clé. - `location` (string, obligatoire) Où : un chemin, une table, ou `fichier:ligne@commit` dans un dépôt. - `file` (string | null, obligatoire) Problème de dépôt : le fichier. - `line` (integer | null, obligatoire) Problème de dépôt : la ligne. - `commit` (string | null, obligatoire) Problème de dépôt : le commit analysé. - `evidence` (string, obligatoire) Ce que le contrôle a observé, secrets masqués. - `fix_unlocked` (boolean, obligatoire) Vrai si son correctif est débloqué : le relire ne coûte rien. - `status_reason` (string | null, obligatoire) Pourquoi il a été marqué faux positif ou risque accepté. - `marked_by` (string | null, obligatoire) Qui l'a marqué : un agent, le nom d'une clé d'API, ou `app`. - `first_seen_at` (string, obligatoire) Le premier scan qui l'a trouvé. - `last_seen_at` (string, obligatoire) Le dernier scan qui l'a trouvé. - `fixed_at` (string | null, obligatoire) Quand il a été trouvé corrigé. ## Exemples ### curl ```bash # La clé reste dans une variable, jamais dans l'adresse curl https://api.rustine.io/v1/issues/2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f \ -H "Authorization: Bearer $RUSTINE_API_KEY" ``` ### Node ```js const res = await fetch( "https://api.rustine.io/v1/issues/2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f", { headers: { Authorization: `Bearer ${process.env.RUSTINE_API_KEY}`, }, }, ) const issue = await res.json() ``` ### Python ```python import os import requests issue = requests.get( "https://api.rustine.io/v1/issues/2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f", headers={ "Authorization": f"Bearer {os.environ['RUSTINE_API_KEY']}", }, ).json() ``` ### Réponse : 200 OK ```json { "id": "2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f", "object": "issue", "project_id": "4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b", "target_id": "8d2e4f6a-9b1c-4d3e-a5f7-0b2c4d6e8f1a", "address": "app.example.com", "control_id": "exposed-env-file", "family": "exposedFiles", "severity": "high", "confidence": "confirmed", "status": "open", "title": "Your .env file is readable online.", "explanation": "Your secrets are served to everyone.", "detail": "GET /.env → 200", "location": "/.env", "file": null, "line": null, "commit": null, "evidence": "GET /.env → 200 (STRIPE_SECRET_KEY=sk_live_••••)", "fix_unlocked": false, "status_reason": null, "marked_by": null, "first_seen_at": "2026-10-13T08:42:10.000Z", "last_seen_at": "2026-10-13T08:42:10.000Z", "fixed_at": null } ``` ## Erreurs | Code | error.code | Ce qui se passe | | --- | --- | --- | | 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. | | 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. | | 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 --- # Débloquer le correctif Débloque le correctif d'un problème et le renvoie : un prompt écrit pour ton agent de code, à coller dans Cursor, Claude Code ou Lovable. ``` POST https://api.rustine.io/v1/issues/{issue_id}/fix ``` Permission OAuth : `fixes:unlock` · une clé d'API les a toutes > Le premier appel pour un problème **décompte un correctif** de ton offre (`charged: true`) ; les suivants sont gratuits. Envoie une [`Idempotency-Key`](https://docs.rustine.io/idempotency) : un appel rejoué ne décompte jamais deux fois. Rustine ne modifie jamais ton code. ## Paramètres de chemin - `issue_id` (string, obligatoire) Le problème. ## 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`. ## Réponse (200 OK) - `object` (string, obligatoire) Le type d'objet, toujours le même pour une route donnée. - `issue_id` (string, obligatoire) Le problème. - `prompt` (string, obligatoire) Le correctif, sous forme d'instructions pour ton agent de code (Cursor, Claude Code…), à appliquer dans ton code. Rustine ne modifie jamais ton code. - `ai_fix` (string | null, obligatoire) Un correctif écrit par Rustine (diff unifié), s'il a été généré dans l'app. - `charged` (boolean, obligatoire) Vrai si cet appel a débloqué le correctif et en a décompté un ; faux s'il l'était déjà. - `fixes_left` (integer, obligatoire) Les correctifs qui te restent sur la période après cet appel (quota et crédits de packs). ## Exemples ### curl ```bash # La clé reste dans une variable, jamais dans l'adresse curl https://api.rustine.io/v1/issues/2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f/fix \ -X POST \ -H "Authorization: Bearer $RUSTINE_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` ### Node ```js const res = await fetch( "https://api.rustine.io/v1/issues/2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f/fix", { method: "POST", headers: { Authorization: `Bearer ${process.env.RUSTINE_API_KEY}`, "Idempotency-Key": crypto.randomUUID(), }, }, ) const fix = await res.json() ``` ### Python ```python import os import uuid import requests fix = requests.post( "https://api.rustine.io/v1/issues/2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f/fix", headers={ "Authorization": f"Bearer {os.environ['RUSTINE_API_KEY']}", "Idempotency-Key": str(uuid.uuid4()), }, ).json() ``` ### Réponse : 200 OK ```json { "object": "fix", "issue_id": "2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f", "prompt": "Your .env file is served publicly at https://app.example.com/.env.\n1. Remove .env from the public directory of the build…\n2. Rotate every secret it contained…", "ai_fix": null, "charged": true, "fixes_left": 82 } ``` ## Erreurs | Code | error.code | Ce qui se passe | | --- | --- | --- | | 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 | `fix_quota_exceeded` | Les correctifs de la période sont épuisés, et il ne reste aucun crédit de pack. Un correctif déjà débloqué reste lisible. | | 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 | `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 | `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 --- # Relire le correctif Relit un correctif déjà débloqué, sans rien décompter. S'il n'est pas encore débloqué, la réponse est `404 fix_locked` et rien n'est débloqué : passe par [Débloquer le correctif](https://docs.rustine.io/api/issues/unlock-fix). ``` GET https://api.rustine.io/v1/issues/{issue_id}/fix ``` Permission OAuth : `scans:read` · une clé d'API les a toutes ## Paramètres de chemin - `issue_id` (string, obligatoire) Le problème. ## Réponse (200 OK) - `object` (string, obligatoire) Le type d'objet, toujours le même pour une route donnée. - `issue_id` (string, obligatoire) Le problème. - `prompt` (string, obligatoire) Le correctif, sous forme d'instructions pour ton agent de code (Cursor, Claude Code…), à appliquer dans ton code. Rustine ne modifie jamais ton code. - `ai_fix` (string | null, obligatoire) Un correctif écrit par Rustine (diff unifié), s'il a été généré dans l'app. ## Exemples ### curl ```bash # La clé reste dans une variable, jamais dans l'adresse curl https://api.rustine.io/v1/issues/2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f/fix \ -H "Authorization: Bearer $RUSTINE_API_KEY" ``` ### Node ```js const res = await fetch( "https://api.rustine.io/v1/issues/2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f/fix", { headers: { Authorization: `Bearer ${process.env.RUSTINE_API_KEY}`, }, }, ) const fix = await res.json() ``` ### Python ```python import os import requests fix = requests.get( "https://api.rustine.io/v1/issues/2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f/fix", headers={ "Authorization": f"Bearer {os.environ['RUSTINE_API_KEY']}", }, ).json() ``` ### Réponse : 200 OK ```json { "object": "fix", "issue_id": "2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f", "prompt": "Your .env file is served publicly at https://app.example.com/.env.\n1. Remove .env from the public directory of the build…\n2. Rotate every secret it contained…", "ai_fix": null } ``` ## Erreurs | Code | error.code | Ce qui se passe | | --- | --- | --- | | 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. | | 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. | | 404 | `fix_locked` | Le correctif de ce problème n'est pas encore débloqué : `POST /issues/{issue_id}/fix` le débloque. | | 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 --- # Revérifier Refait le même contrôle au même endroit, sur ton site en ligne, juste après ton déploiement. Pour un dépôt, Rustine relance une analyse de la branche par défaut (`pending`, avec son `scan_id`). ``` POST https://api.rustine.io/v1/issues/{issue_id}/recheck ``` Permission OAuth : `scans:run` · une clé d'API les a toutes > Gratuit, dans la limite de 60 revérifications par projet et par jour. Déploie (ou pousse) le correctif avant : Rustine regarde ce qui est en ligne. ## Paramètres de chemin - `issue_id` (string, obligatoire) Le problème. ## 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`. ## Réponse (200 OK) - `object` (string, obligatoire) Le type d'objet, toujours le même pour une route donnée. - `issue_id` (string, obligatoire) Le problème revérifié. - `status` (string, obligatoire) `fixed` : le problème est fermé. `still_open` : toujours là, avec ce que Rustine a vu. `pending` : pour un dépôt, une nouvelle analyse est lancée (`scan_id`) ; suis-la, puis relis le problème. (`fixed`, `still_open`, `pending`) - `checked_at` (string, obligatoire) Quand le contrôle a tourné. - `evidence` (string, obligatoire) Ce que le contrôle a vu cette fois. - `scan_id` (string | null, obligatoire) Problème de dépôt : l'analyse qui tranchera. ## Exemples ### curl ```bash # La clé reste dans une variable, jamais dans l'adresse curl https://api.rustine.io/v1/issues/2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f/recheck \ -X POST \ -H "Authorization: Bearer $RUSTINE_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` ### Node ```js const res = await fetch( "https://api.rustine.io/v1/issues/2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f/recheck", { method: "POST", headers: { Authorization: `Bearer ${process.env.RUSTINE_API_KEY}`, "Idempotency-Key": crypto.randomUUID(), }, }, ) const recheck = await res.json() ``` ### Python ```python import os import uuid import requests recheck = requests.post( "https://api.rustine.io/v1/issues/2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f/recheck", headers={ "Authorization": f"Bearer {os.environ['RUSTINE_API_KEY']}", "Idempotency-Key": str(uuid.uuid4()), }, ).json() ``` ### Réponse : 200 OK ```json { "object": "recheck", "issue_id": "2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f", "status": "fixed", "checked_at": "2026-10-13T09:15:40.000Z", "evidence": "GET /.env → 404", "scan_id": null } ``` ## Erreurs | Code | error.code | Ce qui se passe | | --- | --- | --- | | 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 | `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 | `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 | `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 | `recheck_limit_reached` | Trop de revérifications sur ce projet aujourd'hui (60 par jour). Réessaie demain. | | 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. | | 502 | `recheck_failed` | La revérification n'a pas pu aller au bout (site injoignable ?). Vérifie que le site répond, puis réessaie. | | 503 | `service_unavailable` | Rustine est indisponible un instant. Réessaie avec la même `Idempotency-Key`. | https://docs.rustine.io/api/errors --- # Marquer un problème Marque un problème comme faux positif ou risque accepté, ou le rouvre. La raison s'affiche dans l'app avec le nom de ta clé. ``` PATCH https://api.rustine.io/v1/issues/{issue_id} ``` Permission OAuth : `issues:write` · une clé d'API les a toutes > Un faux positif sort du score ; un risque accepté y reste. Pour un problème corrigé, ne le marque pas : [revérifie-le](https://docs.rustine.io/api/issues/recheck), Rustine le fermera. ## Paramètres de chemin - `issue_id` (string, obligatoire) Le problème. ## Corps de la requête - `status` (string, obligatoire) `false_positive` (sort du score), `accepted_risk` (reste dans le score) ou `open` pour annuler. (`open`, `false_positive`, `accepted_risk`) - `reason` (string) Pourquoi, avec ce que tu as vérifié (fichier, ligne, raison), de 10 à 500 caractères. Obligatoire pour `false_positive` et `accepted_risk` ; affichée dans l'app avec le nom de ta clé. ## Réponse (200 OK) - `id` (string, obligatoire) L'identifiant du problème ; il le garde d'un scan à l'autre. - `object` (string, obligatoire) Le type d'objet, toujours le même pour une route donnée. - `project_id` (string, obligatoire) Le projet du problème. - `target_id` (string, obligatoire) Le site ou le dépôt où il a été trouvé. - `address` (string, obligatoire) Le nom d'hôte du site ou `propriétaire/dépôt`. - `control_id` (string, obligatoire) Le contrôle qui l'a trouvé (identifiant stable). - `family` (string, obligatoire) La famille du contrôle : base de données, secrets, fichiers exposés… (`database`, `secrets`, `exposedFiles`, `personalData`, `vulnerableVersions`, `configuration`, `compliance`, `server`, `code`) - `severity` (string, obligatoire) La gravité, de `critical` à `info`. (`critical`, `high`, `medium`, `low`, `info`) - `confidence` (string, obligatoire) À quel point Rustine est sûr : `confirmed` (vu), `likely` (très probable), `estimated` (déduit, par exemple d'une version). (`confirmed`, `likely`, `estimated`) - `status` (string, obligatoire) `open` ; `fixed` (un scan ou une revérification ne le trouve plus) ; `false_positive` (sorti du score) ; `accepted_risk` (reste dans le score). (`open`, `fixed`, `false_positive`, `accepted_risk`) - `title` (string, obligatoire) Le problème en une ligne, dans la langue du propriétaire de la clé. - `explanation` (string, obligatoire) Le risque en mots simples, dans la langue du propriétaire de la clé. - `detail` (string, obligatoire) Le détail technique, dans la langue du propriétaire de la clé. - `location` (string, obligatoire) Où : un chemin, une table, ou `fichier:ligne@commit` dans un dépôt. - `file` (string | null, obligatoire) Problème de dépôt : le fichier. - `line` (integer | null, obligatoire) Problème de dépôt : la ligne. - `commit` (string | null, obligatoire) Problème de dépôt : le commit analysé. - `evidence` (string, obligatoire) Ce que le contrôle a observé, secrets masqués. - `fix_unlocked` (boolean, obligatoire) Vrai si son correctif est débloqué : le relire ne coûte rien. - `status_reason` (string | null, obligatoire) Pourquoi il a été marqué faux positif ou risque accepté. - `marked_by` (string | null, obligatoire) Qui l'a marqué : un agent, le nom d'une clé d'API, ou `app`. - `first_seen_at` (string, obligatoire) Le premier scan qui l'a trouvé. - `last_seen_at` (string, obligatoire) Le dernier scan qui l'a trouvé. - `fixed_at` (string | null, obligatoire) Quand il a été trouvé corrigé. ## Exemples ### curl ```bash # La clé reste dans une variable, jamais dans l'adresse curl https://api.rustine.io/v1/issues/2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f \ -X PATCH \ -H "Authorization: Bearer $RUSTINE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"status":"false_positive","reason":"README.md:42 shows a placeholder key (sk_test_xxxx) that opens nothing."}' ``` ### Node ```js const res = await fetch( "https://api.rustine.io/v1/issues/2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f", { method: "PATCH", headers: { Authorization: `Bearer ${process.env.RUSTINE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ status: "false_positive", reason: "README.md:42 shows a placeholder key (sk_test_xxxx) that opens nothing." }), }, ) const issue = await res.json() ``` ### Python ```python import os import requests issue = requests.patch( "https://api.rustine.io/v1/issues/2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f", headers={ "Authorization": f"Bearer {os.environ['RUSTINE_API_KEY']}", }, json={ "status": "false_positive", "reason": "README.md:42 shows a placeholder key (sk_test_xxxx) that opens nothing.", }, ).json() ``` ### Réponse : 200 OK ```json { "id": "2a4c6e8f-0b1d-4f3a-9c5e-7a9b1c3d5e7f", "object": "issue", "project_id": "4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b", "target_id": "8d2e4f6a-9b1c-4d3e-a5f7-0b2c4d6e8f1a", "address": "app.example.com", "control_id": "exposed-env-file", "family": "exposedFiles", "severity": "high", "confidence": "confirmed", "status": "false_positive", "title": "Your .env file is readable online.", "explanation": "Your secrets are served to everyone.", "detail": "GET /.env → 200", "location": "/.env", "file": null, "line": null, "commit": null, "evidence": "GET /.env → 200 (STRIPE_SECRET_KEY=sk_live_••••)", "fix_unlocked": false, "status_reason": "README.md:42 shows a placeholder key (sk_test_xxxx) that opens nothing.", "marked_by": "CI script", "first_seen_at": "2026-10-13T08:42:10.000Z", "last_seen_at": "2026-10-13T08:42:10.000Z", "fixed_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 | `reason_required` | Marquer un faux positif ou un risque accepté demande une raison, de 10 à 500 caractères. | | 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. | | 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 | `issue_already_fixed` | Le problème est corrigé : il n'y a rien à marquer. | | 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. | | 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 --- # Ton offre et tes quotas Ton offre, l'accès à l'API, et ce qu'il te reste sur la période en scans rapides, audits et correctifs. Appelle-la avant une boucle de scans pour ne pas buter sur un `402`. ``` GET https://api.rustine.io/v1/account ``` Permission OAuth : `scans:read` · une clé d'API les a toutes ## Réponse (200 OK) - `object` (string, obligatoire) Le type d'objet, toujours le même pour une route donnée. - `workspace` (object, obligatoire) Ton espace. - `id` (string, obligatoire) L'identifiant de l'espace. - `name` (string, obligatoire) Le nom de l'espace. - `plan` (string, obligatoire) L'offre en cours : `free`, `solo` ou `pro`. (`free`, `solo`, `pro`) - `api_access` (object, obligatoire) L'accès à l'API et au serveur MCP. - `allowed` (boolean, obligatoire) Vrai si l'API et le serveur MCP répondent en ce moment. - `trial_ends_at` (string | null, obligatoire) Solo seulement : la fin de l'essai de 14 jours une fois commencé ; `null` sur Pro, ou avant le premier appel. - `period` (object, obligatoire) La période de quotas en cours (30 jours). - `start` (string, obligatoire) Le début de la période en cours. - `end` (string, obligatoire) Quand les compteurs repartent à zéro. - `quotas` (object, obligatoire) Ce que la période te donne, et ce que tu en as utilisé. - `quick_scans` (object, obligatoire) Les scans rapides. - `limit` (integer, obligatoire) Inclus sur la période (tous les projets payés par l'espace). - `used` (integer, obligatoire) Déjà utilisés sur la période. - `remaining` (integer, obligatoire) Restants sur la période, avant les crédits de packs. - `audits` (object, obligatoire) Les audits complets. - `limit` (integer, obligatoire) Inclus sur la période (tous les projets payés par l'espace). - `used` (integer, obligatoire) Déjà utilisés sur la période. - `remaining` (integer, obligatoire) Restants sur la période, avant les crédits de packs. - `fixes` (object, obligatoire) Les correctifs. - `limit` (integer, obligatoire) Inclus sur la période (tous les projets payés par l'espace). - `used` (integer, obligatoire) Déjà utilisés sur la période. - `remaining` (integer, obligatoire) Restants sur la période, avant les crédits de packs. - `free_fixes_left` (integer, obligatoire) Les correctifs offerts une fois à un espace Gratuit, pas encore utilisés. - `pack_credits` (object, obligatoire) Les crédits de packs, utilisés une fois le quota de la période épuisé. - `scans` (integer, obligatoire) Crédits de scans. - `fixes` (integer, obligatoire) Crédits de correctifs. - `projects` (object, obligatoire) Tes projets. - `limit` (integer, obligatoire) Les projets ouverts que permet l'offre (compris et en plus). - `used` (integer, obligatoire) Les projets de l'espace, en pause compris. - `paused` (integer, obligatoire) Les projets en pause, parce que l'offre en couvre moins. - `addresses_per_project` (integer, obligatoire) Les adresses qu'un projet peut suivre avec cette offre. - `features` (object, obligatoire) Ce que l'offre ouvre. - `full_audit` (boolean, obligatoire) Les audits complets (Solo, Pro). - `repositories` (boolean, obligatoire) L'analyse des dépôts GitHub (Solo, Pro). - `ai_fix` (boolean, obligatoire) AI Fix, Rustine écrit le correctif (Solo, Pro). - `alerts` (boolean, obligatoire) Les alertes de baisse de score et de nouvelle faille connue (Pro). ## Exemples ### curl ```bash # La clé reste dans une variable, jamais dans l'adresse curl https://api.rustine.io/v1/account \ -H "Authorization: Bearer $RUSTINE_API_KEY" ``` ### Node ```js const res = await fetch( "https://api.rustine.io/v1/account", { headers: { Authorization: `Bearer ${process.env.RUSTINE_API_KEY}`, }, }, ) const account = await res.json() ``` ### Python ```python import os import requests account = requests.get( "https://api.rustine.io/v1/account", headers={ "Authorization": f"Bearer {os.environ['RUSTINE_API_KEY']}", }, ).json() ``` ### Réponse : 200 OK ```json { "object": "account", "workspace": { "id": "0b6f4a52-3c1e-4d7a-9f2e-5a8c1d3e7b90", "name": "example.com" }, "plan": "pro", "api_access": { "allowed": true, "trial_ends_at": null }, "period": { "start": "2026-10-01T00:00:00.000Z", "end": "2026-10-31T00:00:00.000Z" }, "quotas": { "quick_scans": { "limit": 300, "used": 12, "remaining": 288 }, "audits": { "limit": 180, "used": 4, "remaining": 176 }, "fixes": { "limit": 90, "used": 7, "remaining": 83 } }, "free_fixes_left": 0, "pack_credits": { "scans": 0, "fixes": 10 }, "projects": { "limit": 3, "used": 2, "paused": 0 }, "addresses_per_project": 20, "features": { "full_audit": true, "repositories": true, "ai_fix": true, "alerts": true } } ``` ## Erreurs | Code | error.code | Ce qui se passe | | --- | --- | --- | | 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 --- # Brancher ton agent > Claude, ChatGPT, Claude Code, Cursor ou VS Code se branchent sur https://mcp.rustine.io, avec OAuth ou une clé d'API. Le serveur MCP de Rustine laisse ton agent lister tes projets, lancer un scan, lire les problèmes, récupérer un correctif et demander une revérification. Rustine ne modifie jamais ton code : c'est ton agent qui applique le correctif, chez toi. **Adresse du serveur : `https://mcp.rustine.io`** Avant de commencer : une offre qui ouvre le MCP (Pro, ou l'essai de 14 jours sur Solo, voir [Accès](https://docs.rustine.io/access.md)) et la double authentification activée sur Rustine. ## Avec OAuth (recommandé) Aucune clé à copier. Au premier appel, ton agent ouvre la page d'autorisation de Rustine : tu vois le compte et l'offre sur lesquels il agira, tu cliques sur **Autoriser**, et il renouvelle son accès seul. | Agent | Où le brancher | Pas à pas | | --- | --- | --- | | Claude (claude.ai, app) | Personnaliser › Connecteurs › Ajouter un connecteur personnalisé, nom `Rustine`, URL `https://mcp.rustine.io` | [Centre d'aide](https://rustine.io/aide/claude-app) | | ChatGPT | Plugins › + › serveur MCP personnalisé, authentification OAuth | [Centre d'aide](https://rustine.io/aide/chatgpt) | | Claude Code | `claude mcp add --transport http rustine https://mcp.rustine.io`, puis `/mcp` | [Centre d'aide](https://rustine.io/aide/claude-code) | | Cursor | `~/.cursor/mcp.json` : `{ "mcpServers": { "rustine": { "url": "https://mcp.rustine.io" } } }` | | | VS Code | `.vscode/mcp.json` : `{ "servers": { "rustine": { "type": "http", "url": "https://mcp.rustine.io" } } }` | | Dans l'app, **Réglages › Agents connectés** copie l'adresse et ouvre la bonne page pour Claude et ChatGPT, et liste chaque agent branché avec sa dernière utilisation. ## Avec une clé d'API Pour un agent qui ne sait pas faire OAuth. La clé va dans l'en-tête `Authorization`, jamais dans l'adresse. | Agent | Configuration | | --- | --- | | Claude Code | `claude mcp add --transport http rustine https://mcp.rustine.io --header "Authorization: Bearer $RUSTINE_API_KEY"` | | Cursor | `{ "mcpServers": { "rustine": { "url": "https://mcp.rustine.io", "headers": { "Authorization": "Bearer ${env:RUSTINE_API_KEY}" } } } }` | | VS Code | `"headers": { "Authorization": "Bearer ${input:rustine-key}" }`, avec un `inputs` de type `promptString` et `password: true` | OAuth ou clé ? Le [centre d'aide](https://rustine.io/aide/oauth-ou-cle) compare les deux. Pour appeler le serveur depuis un script, sans agent : [Depuis un script](https://docs.rustine.io/mcp/scripts.md). ## Que lui demander 1. « Liste mes projets Rustine et leurs scores. » 2. « Lance un scan de monsaas.fr avec Rustine. » 3. « Quels sont les problèmes de sécurité de monsaas.fr, du plus grave au moins grave ? » 4. « Récupère le correctif du premier et applique-le. » 5. « Revérifie ce problème avec Rustine » (après ton déploiement). Les outils qu'il appelle, et ce que chacun coûte : [Les outils](https://docs.rustine.io/mcp/tools.md). ## Exemples ### Claude Code ```bash claude mcp add --transport http rustine https://mcp.rustine.io # puis /mcp dans Claude Code pour autoriser ``` ### Cursor ```json { "mcpServers": { "rustine": { "url": "https://mcp.rustine.io" } } } ``` ### VS Code ```json { "servers": { "rustine": { "type": "http", "url": "https://mcp.rustine.io" } } } ``` --- # Les outils > Les six outils du serveur MCP, leurs paramètres et ce qu'ils coûtent. Lire est gratuit, débloquer un correctif décompte un correctif. Chaque description d'outil rappelle les règles à l'agent : Rustine ne scanne que les sites des projets de l'utilisateur, en lecture seule, jamais un site qui n'est pas à lui, et ne modifie jamais le code. ## list_projects Tes projets et leurs sites : vérifié ou non, score sur 100, note de A à F, dernier scan, problèmes ouverts. **Gratuit.** Permission `scans:read`. Aucun paramètre. ## run_scan Scanne un site **déjà dans tes projets** et attend le résultat (jusqu'à une minute environ) : score, note et problèmes par gravité. Si le scan tourne encore, l'agent rappelle l'outil avec la même cible pour continuer d'attendre. **Un scan de ton quota** ; un scan du même type dans l'heure renvoie le résultat récent sans rien décompter. Permission `scans:run`. | Paramètre | Type | | Description | | --- | --- | --- | --- | | `target` | string | obligatoire | id de site, id de projet (s'il n'a qu'un site) ou URL d'un site de tes projets | | `kind` | `quick` \| `audit` | défaut `quick` | `audit` seulement sur un site prouvé | ## list_issues Les problèmes, du plus grave au moins grave, avec leur confiance ; pour un dépôt, le fichier, la ligne et le commit. Les problèmes d'un site non prouvé restent masqués (seul leur nombre apparaît). **Gratuit.** Permission `scans:read`. | Paramètre | Type | | Description | | --- | --- | --- | --- | | `project` | string | optionnel | id de projet | | `target` | string | optionnel | id ou URL d'un site | | `status` | `open` \| `all` | défaut `open` | `all` ajoute les corrigés, faux positifs et risques acceptés, avec la raison et l'agent qui les a marqués | ## get_fix Renvoie le correctif d'un problème : des instructions écrites pour un agent de code. **Un correctif de ton offre au premier appel** pour ce problème, gratuit ensuite. L'agent est invité à te demander avant d'en débloquer beaucoup. Permission `fixes:unlock`. | Paramètre | Type | | Description | | --- | --- | --- | --- | | `issue_id` | string (UUID) | obligatoire | id venu de `list_issues` | ## recheck_issue Revérifie un problème : sur le site en ligne, ou pour un dépôt en relançant une analyse de la branche par défaut (au plus une par quart d'heure). Répond `fixed`, `stillOpen` avec la preuve, ou `pending` (analyse pas finie : `run_scan` sur le dépôt un peu plus tard). **Gratuit**, dans la limite de 60 par projet et par jour. Permission `scans:run`. | Paramètre | Type | | Description | | --- | --- | --- | --- | | `issue_id` | string (UUID) | obligatoire | id venu de `list_issues` | ## set_issue_status Marque un problème comme faux positif (il sort du score) ou risque accepté (il y reste), ou le rouvre. La raison s'affiche dans l'app avec le nom de l'agent. L'agent ne doit marquer un faux positif qu'après avoir vérifié que la valeur est fausse ou injoignable, et passer par `recheck_issue` pour un problème corrigé. **Gratuit.** Permission `issues:write`. | Paramètre | Type | | Description | | --- | --- | --- | --- | | `issue` | string (UUID) | obligatoire | id venu de `list_issues` | | `status` | `falsePositive` \| `acceptedRisk` \| `open` | obligatoire | `open` annule un marquage | | `reason` | string | obligatoire | 10 à 500 caractères : ce qui a été vérifié (fichier, ligne, pourquoi la valeur est fausse) | Une connexion OAuth autorisée avant le 13/10/2026 n'a pas `issues:write` : `set_issue_status` lui demande de déconnecter puis reconnecter Rustine. ## Quand l'offre ne suit pas Un quota épuisé ou une offre sans MCP ne donne jamais un `403` : l'outil répond par une erreur lisible (`isError: true`), dans la langue de l'utilisateur, avec le lien pour changer d'offre. ## Exemples ### tools/call ```json { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "run_scan", "arguments": { "target": "https://monsaas.fr", "kind": "quick" } } } ``` --- # Depuis un script > Appelle le serveur MCP sans agent, avec curl et une clé d'API. Sans état, en POST, en SSE ou en JSON simple. Pour un script, l'[API REST](https://docs.rustine.io/api/projects/list.md) est plus simple. Mais tu peux aussi parler au serveur MCP directement, par exemple pour tester ta clé ou reproduire ce que voit ton agent. ## La poignée de main Le serveur est **sans état** : pas d'`initialize`, pas d'en-tête de session. Chaque requête est un `POST` sur `https://mcp.rustine.io` avec : - `Authorization: Bearer rst_...` ; - `Content-Type: application/json` ; - `Accept: application/json, text/event-stream` (sinon `406`). Sans en-tête `MCP-Protocol-Version`, la réponse arrive en SSE : une ligne `event: message`, puis `data: {…}` avec la réponse JSON-RPC. ## En JSON simple Avec la révision `2026-07-28` du protocole, la réponse est du JSON simple. Ajoute les en-têtes `MCP-Protocol-Version`, `Mcp-Method` et `Mcp-Name`, et les clés `_meta` dans `params` : ```bash curl -s https://mcp.rustine.io \ -H "Authorization: Bearer $RUSTINE_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/call" \ -H "Mcp-Name: list_projects" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_projects","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"mon-script","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}' ``` ## Les réponses à connaître - **401** `invalid_token` avec un en-tête `WWW-Authenticate` : clé absente, révoquée ou mal écrite. - **406** : il manque `text/event-stream` ou `application/json` dans `Accept`. - **Quota épuisé ou offre sans MCP** : jamais un `403`. La réponse est un résultat d'outil avec `isError: true` et un texte lisible, avec le lien pour changer d'offre. - **Débit** : 120 requêtes par minute et par clé, comme l'API. Les paramètres de chaque outil : [Les outils](https://docs.rustine.io/mcp/tools.md). ## Exemples ### tools/list ```bash curl -s https://mcp.rustine.io \ -H "Authorization: Bearer $RUSTINE_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` ### list_projects ```bash curl -s https://mcp.rustine.io \ -H "Authorization: Bearer $RUSTINE_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_projects","arguments":{}}}' ``` ### Réponse : 200 OK ```text event: message data: {"result":{"content":[{"type":"text","text":"…"}]},"jsonrpc":"2.0","id":1} ``` --- # Changelog > Ce qui change dans l'API, le serveur MCP et cette doc. Les ajouts arrivent sans casser la v1. ## 13 octobre 2026 : l'API v1 et cette doc - L'API REST v1 sur `https://api.rustine.io/v1` : projets, sites, scans, problèmes, correctifs, revérifications et ton offre. Clés `rst_` dans l'en-tête `Authorization`. - La doc sur `docs.rustine.io`, en français et en anglais, avec sa version Markdown pour les agents (`/llms.txt`, `/llms-full.txt`, une page `.md` pour chaque page) et la spécification OpenAPI [`/openapi.json`](https://docs.rustine.io/openapi.json). - Le serveur MCP accepte aussi les appels directs d'un script, avec une clé : [Depuis un script](https://docs.rustine.io/mcp/scripts.md).