# Rustine docs (English) # Introduction > Rustine's API and MCP server. Your code or your agent starts scans, reads issues and gets fixes, without going through the app. Rustine finds the leaks in your site or your GitHub repository, gives you a score out of 100 and, for each issue, a fix ready to paste into your coding agent. Everything the app does, you can do from here: - **The REST API** (`https://api.rustine.io/v1`) for your scripts, your CI, n8n or Make: scan after a deploy, block a release when a critical issue shows up, recheck after a fix. - **The MCP server** (`https://mcp.rustine.io`) for your agent: Claude, ChatGPT, Claude Code, Cursor or VS Code call Rustine themselves, and check before telling you "done". ## Where to start 1. [First call in 2 minutes](https://docs.rustine.io/en/quickstart.md): a key, one `curl` command, your first answer. 2. [Authentication](https://docs.rustine.io/en/authentication.md): where the key goes (never in the address). 3. [The API reference](https://docs.rustine.io/en/api.md): every route, its parameters and its samples in curl, Node and Python. Your agent prefers MCP? Go straight to [Connect your agent](https://docs.rustine.io/en/mcp/connect.md). ## What Rustine does not do Rustine only scans the sites of **your** projects, read-only. A full audit only runs on a site you proved is yours: the law forbids testing a site you do not control. And Rustine never touches your code: the fix is text, your agent or you apply it. ## For agents Every page exists in Markdown at the same address followed by `.md`. [`/llms.txt`](https://docs.rustine.io/llms.txt) is the index, [`/en/llms-full.txt`](https://docs.rustine.io/en/llms-full.txt) all the docs in one file, and [`/openapi.json`](https://docs.rustine.io/openapi.json) the API specification. ## Examples ### curl ```bash curl https://api.rustine.io/v1/account \ -H "Authorization: Bearer $RUSTINE_API_KEY" ``` ### Response: 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 } } } ``` --- # First call in 2 minutes > Create a key, keep it in an environment variable and call the API. Then start your first scan. ## 1. Create a key In the app, open [Settings › API keys](https://app.rustine.io/en/settings?section=apiKeys), name the key ("mysaas CI") and click **Create key**. You need the Pro plan, or the Solo plan's trial (see [Access](https://docs.rustine.io/en/access.md)), and two-factor authentication turned on. The key starts with `rst_` and is shown once. Copy it right away. ## 2. Keep it in a variable ```bash export RUSTINE_API_KEY="rst_..." ``` In your CI, put it in the secrets (GitHub Actions: **Settings › Secrets and variables › Actions**). Never in a committed file, never in an address. ## 3. Call the API The command on the right lists your projects. If you see your project and its score, everything works. A `401` means the key is missing or mistyped: see [Errors](https://docs.rustine.io/en/api/errors#invalid_api_key). ## 4. Start a scan Take the `id` of a site in `targets` and start a quick scan: ```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"}' ``` The scan runs in the background. Poll [Get a scan](https://docs.rustine.io/en/api/scans/get.md) every few seconds until `done`, then [list the project's issues](https://docs.rustine.io/en/api/issues/list.md). ## Next - The fix for an issue: [Unlock the fix](https://docs.rustine.io/en/api/issues/unlock-fix.md). Paste it into your agent, deploy, then [recheck](https://docs.rustine.io/en/api/issues/recheck.md). - Your agent can do all of this by itself: [Connect your agent](https://docs.rustine.io/en/mcp/connect.md). ## Examples ### 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"]]) ``` ### Response: 200 OK ```json { "object": "list", "data": [ { "id": "4f9c2a7e-1b3d-4e5f-8a6b-7c8d9e0f1a2b", "object": "project", "name": "My SaaS", "score": 62, "grade": "C", "open_issues": 3, "targets": [ { "id": "8d2e4f6a-9b1c-4d3e-a5f7-0b2c4d6e8f1a", "kind": "domain", "address": "app.mysaas.com", "verified": true } ] } ], "has_more": false } ``` --- # Authentication > An rst_ key in the Authorization header for your scripts, OAuth for agents. Never a key in an address. ## API keys A key is created in [Settings › API keys](https://app.rustine.io/en/settings?section=apiKeys). It starts with `rst_`, works for your whole workspace and is shown only once: Rustine only keeps a fingerprint of it. It goes **only** in the header: ```http Authorization: Bearer rst_... ``` Never in the address (`?key=…`): a URL ends up in server logs, browser history and screenshots. - One key per use ("CI", "n8n", "deploy script"): you know which one to revoke. - A key has every permission of your workspace: read, scan, unlock a fix, mark an issue. - Error messages and issue texts come in the language of the key's owner. - If a key leaks, click **Revoke**: it stops working at once. Create another one. ## OAuth, for agents Claude, ChatGPT, Claude Code, Cursor and VS Code connect to the [MCP server](https://docs.rustine.io/en/mcp/connect.md) with OAuth: no key to copy, you click **Allow** and the agent renews its access by itself. You see every connected agent in **Settings › Connected agents**, and disconnect it in one click. The OAuth access token (`rat_…`) also works on the API, in the same header. It only has the permissions granted to the agent: every reference page says which one the route needs (`scans:read`, `scans:run`, `fixes:unlock` or `issues:write`). Without it, the answer is `403 scope_missing`. ## Requirements - A plan that opens the API and MCP: Pro, or the Solo plan's trial ([Access](https://docs.rustine.io/en/access.md)). - Two-factor authentication turned on in your Rustine account, to create a key or allow an agent. ## Refusals | Status | `error.code` | What happens | | --- | --- | --- | | 401 | [`authentication_required`](https://docs.rustine.io/en/api/errors#authentication_required) | No key in the request. | | 401 | [`invalid_api_key`](https://docs.rustine.io/en/api/errors#invalid_api_key) | Wrong, revoked or expired key. | | 403 | [`scope_missing`](https://docs.rustine.io/en/api/errors#scope_missing) | The OAuth token lacks the permission. | | 402 | [`api_plan_required`](https://docs.rustine.io/en/api/errors#api_plan_required) | The plan does not open the API. | ## Examples ### curl ```bash curl https://api.rustine.io/v1/account \ -H "Authorization: Bearer $RUSTINE_API_KEY" ``` ### Response: 401 Unauthorized ```json { "error": { "type": "authentication", "code": "invalid_api_key", "message": "This API key or token is invalid, revoked or expired.", "doc_url": "https://docs.rustine.io/api/errors#invalid_api_key" } } ``` --- # Errors > The error format, when to retry, and every code the API can return. An error always has the same shape, whatever the call: ```json { "error": { "type": "invalid_request", "code": "parameter_invalid", "message": "Invalid parameter: limit.", "param": "limit", "doc_url": "https://docs.rustine.io/api/errors#parameter_invalid" } } ``` - `type`: the category, to know who must act (you, your plan, or Rustine). - `code`: the exact case. This is what your code should test. - `message`: a sentence for a human, in the language of the key's owner. Log it, do not test it: its wording may change. - `param`: with `invalid_request`, the parameter at fault. - `doc_url`: the line of this page that explains the code. - `upgrade_url`: with `plan_limit`, where to change plans or buy a pack. Every response also carries a `Request-Id` header: give it to support if an error keeps coming back. ## When to retry - `429`: yes, after the number of seconds in the `Retry-After` header. - `500`, `502` and `503`: yes, a little later. For a write, send the same [`Idempotency-Key`](https://docs.rustine.io/en/idempotency.md) again: the action will not run twice. - `409 idempotency_in_progress`: yes, in a moment. - Other `4xx`: no. The same request will get the same answer; fix it first, or change plans for a `402`. ## Every code | error.code | HTTP | error.type | What it means | | --- | --- | --- | --- | | `body_invalid` | 400 | `invalid_request` | The request body is not valid JSON. Check the `Content-Type: application/json` header. | | `cursor_invalid` | 400 | `invalid_request` | `starting_after` is not the id of an item of this list. | | `idempotency_key_invalid` | 400 | `invalid_request` | The `Idempotency-Key` header must be 1 to 255 printable characters. | | `parameter_invalid` | 400 | `invalid_request` | A parameter is missing or invalid; `param` names it. | | `reason_required` | 400 | `invalid_request` | Marking a false positive or an accepted risk needs a reason, 10 to 500 characters. | | `target_kind_invalid` | 400 | `invalid_request` | The scan kind does not fit the target: `repository` for a GitHub repository, `quick` or `audit` for a site. | | `url_invalid` | 400 | `invalid_request` | The address is not the valid URL of a public site. | | `url_private` | 400 | `invalid_request` | Rustine only scans public sites, never a local or private address. | | `authentication_required` | 401 | `authentication` | No key in the request. Pass it in the `Authorization: Bearer rst_…` header, never in the address. | | `invalid_api_key` | 401 | `authentication` | The key or OAuth token is wrong, revoked or expired. Check it is copied in full, or create a new key. | | `address_limit_reached` | 402 | `plan_limit` | The project already follows as many addresses as your plan allows. | | `api_plan_required` | 402 | `plan_limit` | The workspace is on the Free plan. The API comes with Pro, or on a 14-day trial with Solo. | | `api_trial_ended` | 402 | `plan_limit` | Solo's 14-day trial is over. Move to Pro: your keys work again at once. | | `audit_not_in_plan` | 402 | `plan_limit` | The Free plan runs quick scans only; the full audit comes with Solo and Pro. | | `audit_quota_exceeded` | 402 | `plan_limit` | This period's full audits are used up, and no pack credit is left. | | `domain_taken` | 402 | `plan_limit` | On the Free plan, a domain another free account already follows cannot be added or scanned. | | `fix_quota_exceeded` | 402 | `plan_limit` | This period's fixes are used up, and no pack credit is left. A fix already unlocked stays readable. | | `plan_limit` | 402 | `plan_limit` | Your plan does not include this action. `upgrade_url` leads to the plan that allows it. | | `project_limit_reached` | 402 | `plan_limit` | All your plan's projects are in use. Add an extra project or change plans. | | `project_paused` | 402 | `plan_limit` | The project is paused: your plan covers fewer projects than your workspace has. Its issues stay readable, but nothing new runs. | | `repositories_not_in_plan` | 402 | `plan_limit` | GitHub repository analyses come with Solo and Pro. | | `scan_quota_exceeded` | 402 | `plan_limit` | This period's quick scans are used up, and no pack credit is left. | | `scope_missing` | 403 | `authentication` | The OAuth token lacks the permission this route needs (shown on its page). Reconnect the agent to grant it. An API key has every permission. | | `fix_locked` | 404 | `not_found` | The fix of this issue is not unlocked yet: `POST /issues/{issue_id}/fix` unlocks it. | | `resource_not_found` | 404 | `not_found` | Nothing with this id in the key's workspace. An object of another workspace answers the same: its existence is never revealed. | | `route_not_found` | 404 | `not_found` | This route does not exist. Check the method and the path in the reference. | | `conflict` | 409 | `conflict` | The request does not fit the object's current state. Read it again, then retry. | | `github_unavailable` | 409 | `conflict` | GitHub repository analyses are not available right now. | | `idempotency_conflict` | 409 | `conflict` | This `Idempotency-Key` was already used for another request (method, path or body). A new action takes a new key. | | `idempotency_in_progress` | 409 | `conflict` | A request with this `Idempotency-Key` is still running. Retry in a moment. | | `issue_already_fixed` | 409 | `conflict` | The issue is fixed: there is nothing to mark. | | `proof_required` | 409 | `conflict` | The site must first be proved yours (full audit, fix, marking). Prove it in the app: DNS, file or meta tag. | | `site_excluded` | 409 | `conflict` | The owner of this site asked not to be scanned. Rustine will not scan it. | | `target_exists` | 409 | `conflict` | The project already follows this address. | | `rate_limited` | 429 | `rate_limited` | Too many requests for this key (120 per minute). Wait `Retry-After` seconds. | | `recheck_limit_reached` | 429 | `rate_limited` | Too many rechecks on this project today (60 per day). Try again tomorrow. | | `repository_analyses_limit_reached` | 429 | `rate_limited` | The workspace started many repository analyses in the last 24 hours. Try again later. | | `internal_error` | 500 | `api_error` | An error on our side, reported automatically. Retry later; give the `Request-Id` to support if it comes back. | | `recheck_failed` | 502 | `api_error` | The recheck could not complete (site unreachable?). Check the site answers, then retry. | | `platform_busy` | 503 | `rate_limited` | Rustine is not starting new scans for a few minutes. Retry later; nothing was charged. | | `service_unavailable` | 503 | `api_error` | Rustine is unavailable for a moment. Retry with the same `Idempotency-Key`. | ## Types Test `type` to decide what to do, `code` to know exactly what happened. | error.type | What it means | | --- | --- | | `invalid_request` | The request itself needs fixing. | | `authentication` | The key or token is not accepted, or lacks the permission. | | `plan_limit` | The plan or the quota does not allow it; `upgrade_url` says what to do. | | `not_found` | The object or the route does not exist for this key. | | `rate_limited` | Too many requests, or a ceiling reached: wait a little. | | `conflict` | The action does not fit the current state (unproved site, key already used…). | | `api_error` | A problem on Rustine's side: retry. | ## Examples ### Node ```js const res = await fetch(url, { headers }) if (!res.ok) { const { error } = await res.json() if (error.type === "plan_limit") { console.log(`Plan or 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)) // ...then retry, with the same Idempotency-Key } else { throw new Error(`${error.code}: ${error.message}`) } } ``` ### Response: 402 Payment Required ```json { "error": { "type": "plan_limit", "code": "audit_quota_exceeded", "message": "This period's full audits are used up.", "doc_url": "https://docs.rustine.io/api/errors#audit_quota_exceeded", "upgrade_url": "https://app.rustine.io/settings?section=offer" } } ``` --- # Pagination > Lists come in pages. limit sets the size, starting_after resumes after the last item, has_more says whether more are left. Every route that returns several items answers with the same envelope: ```json { "object": "list", "data": [...], "has_more": true } ``` ## Parameters - `limit`: how many items per page, from 1 to 100. 20 by default. - `starting_after`: the `id` of the last item of the previous page. The next page starts right after it. When `has_more` is `false`, you have everything. The order stays the same from page to page: issues worst first, scans newest first. The cursor follows an item, not a page number: if a new scan arrives while you go through the list, you never see the same item twice. A `starting_after` that is not in the list answers `400 cursor_invalid`. ## Examples ### 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"] ``` ### Response: 200 OK ```json { "object": "list", "data": [ { "id": "a3c5e7f9-1b2d-4c6e-8f0a-2b4d6f8a1c3e", "object": "issue" } ], "has_more": true } ``` --- # Rate limits and quotas > 120 requests per minute per key, your plan's quotas for each period, and the few ceilings that protect Rustine and the scanned sites. ## Rate: 120 requests per minute per key Every response says so in its headers: `X-RateLimit-Limit` (the ceiling), `X-RateLimit-Remaining` (what is left this minute) and `X-RateLimit-Reset` (when the window restarts, in Unix seconds). Past that, the API answers `429 rate_limited` with `Retry-After`: wait that long and carry on. The MCP server follows the same limit. To follow a scan, one request every 3 to 5 seconds is plenty. ## Your plan's quotas Your plan gives you quick scans, full audits and fixes **per project**, over a 30-day period; each extra project adds its share. Once the period's quota is used up, Rustine takes from your pack credits if you have any. Otherwise the action answers `402` (`scan_quota_exceeded`, `audit_quota_exceeded` or `fix_quota_exceeded`) with `upgrade_url`. [Your plan and quotas](https://docs.rustine.io/en/api/account.md) tells you where you stand. What costs nothing: - reading your projects, scans and issues; - reading a fix you already unlocked; - scanning the same site with the same kind again within the hour: Rustine returns the recent result (`reused: true`) without charging anything; - analysing a GitHub repository; - rechecking an issue, up to 60 rechecks per project and per day. ## Rustine's ceilings A few ceilings protect the platform and the scanned sites. You should not meet them, but your code should know what to do: - **Too many scans at once on Rustine**, or the platform's daily ceiling reached: a new scan gets `503 platform_busy`. Retry in a few minutes; nothing was charged. - **Rechecks**: 60 per project and per day, then `429 recheck_limit_reached`. - **Repository analyses**: a limited number per workspace over 24 hours, pushes included, then `429 repository_analyses_limit_reached`. A recheck on a repository starts at most one analysis per quarter of an hour. These ceilings may be tightened in case of abuse, without notice: always treat `429` and `503` as "retry later". | Plan | Projects included | Quick scans | Full audits | Fixes | Addresses per project | API and MCP | | --- | --- | --- | --- | --- | --- | --- | | Free | 1 | 10 | 0 | 3 given once | 1 | no | | Solo | 1 | 30 | 60 | 15 | 5 | 14-day trial | | Pro | 3 | 100 | 60 | 30 | 20 | included | Quick scans, full audits, fixes: per project and per month. ## Examples ### curl ```bash curl -i https://api.rustine.io/v1/projects \ -H "Authorization: Bearer $RUSTINE_API_KEY" ``` ### Response: 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": "Too many requests for this key (120 per minute).", "doc_url": "https://docs.rustine.io/api/errors#rate_limited" } } ``` --- # Idempotency > Send an Idempotency-Key with your POSTs. If the request is replayed (timeout, CI rerun), Rustine returns the first answer instead of doing the action again. The network drops, a CI job is rerun, your script times out: you do not know whether the action happened. With an idempotency key, you can send the same request again safely. ## How Add the `Idempotency-Key` header to your `POST` requests, with a value unique to the action (1 to 255 printable characters): your deploy id, your job id, or a UUID. ```http Idempotency-Key: deploy-4f9c2e1 ``` - **Same key, same request, within 24 hours**: Rustine returns the first answer, with the `Idempotent-Replayed: true` header. Nothing is done again, nothing is charged twice. - **Same key, another request** (method, path or body): `409 idempotency_conflict`. A new action takes a new key. - **Same key while the first request is still running**: `409 idempotency_in_progress`. Retry in a moment. - A malformed key: `400 idempotency_key_invalid`. `GET` requests and the `PATCH` of an issue already have no double effect: no key needed. ## Where it matters most - **Unlocking a fix**: replaying it never uses two fixes. - **Starting a scan** from CI: a rerun job does not start a second scan. (Even without a key, a scan of the same kind on the same site within the hour returns the recent result, with `reused: true`.) - **Creating a project or adding a site**: no duplicate if the first answer got lost. - **After a `500` or a `503`**: send the same request again with the same key. ## Examples ### 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" ``` ### Response: 200 OK (replayed) ```http HTTP/1.1 200 OK Idempotent-Replayed: true { "object": "fix", "issue_id": "a3c5e7f9-1b2d-4c6e-8f0a-2b4d6f8a1c3e", "charged": true, "fixes_left": 82 } ``` --- # Versions > The API is at v1. What may change without notice (additions), what will never change within v1, and the Rustine-Version header. The major version is in the address: `/v1`. Every response also carries the `Rustine-Version` header, the date of the contract that answered (`2026-10-13` today). Log it: if a behaviour surprises you, it tells which version of the contract produced it. ## What Rustine promises within v1 Nothing that works today breaks: - no field removed or renamed, no type changed; - no new required parameter; - no route removed; - the format of errors and lists stays the same, and an error `code` keeps its meaning. ## What may happen without notice Additions, which your code must be able to ignore. The date in `Rustine-Version` then moves forward: - new routes; - new fields in answers; - new optional parameters; - new values in a closed list (`status`, `severity`, `family`…): plan an "other" case; - new error codes: handle an unknown code by its `type`. ## A v2, some day A change that would break your code will come as a new version, `/v2`, announced in the [Changelog](https://docs.rustine.io/en/changelog.md) and by email to key holders. v1 will keep answering for **at least 12 months** after that announcement, while you migrate; its end date will be given in the announcement. ## Examples ### curl ```bash curl -sI https://api.rustine.io/v1/account \ -H "Authorization: Bearer $RUSTINE_API_KEY" | grep -i rustine-version ``` ### Response: 200 OK ```http Rustine-Version: 2026-10-13 ``` --- # Access > The API and the MCP server are included in the Pro plan, and on trial for 14 days on the Solo plan. | Plan | API and MCP server | | --- | --- | | Free | no (`402 api_plan_required`) | | Solo | 14-day trial, once | | Pro | included | ## The Solo trial On the Solo plan, the trial lasts 14 days, once per workspace. It starts with the first call from your script or your agent (API or MCP), not when you create the key: you can prepare your integration first. [Your plan and quotas](https://docs.rustine.io/en/api/account.md) gives its end date (`api_access.trial_ends_at`). When the trial ends, the API answers `402 api_trial_ended` with `upgrade_url`, and the MCP server answers with a readable tool error (never a `403`, which agents read as "reconnect"). Move to Pro: your keys and connected agents work again at once, nothing to set up again. ## Creating a key A key can only be created when your plan opens the API (Pro, or Solo while the trial has not started or is running), with two-factor authentication turned on. Keys created earlier stay listed and can be revoked. [Create a key](https://app.rustine.io/en/settings?section=apiKeys) · [Compare the plans](https://rustine.io/en) --- # API reference > The address, formats and conventions shared by every route of API v1, then each route one by one. **Address: `https://api.rustine.io/v1`** - **Authentication**: `Authorization: Bearer rst_…` ([Authentication](https://docs.rustine.io/en/authentication.md)). - **Format**: JSON in and out, `snake_case` fields, ISO 8601 dates (UTC), UUID ids. - **Lists**: `{ "object": "list", "data": [...], "has_more": false }`, with `limit` and `starting_after` ([Pagination](https://docs.rustine.io/en/pagination.md)). - **Errors**: `{ "error": { "type", "code", "message", "doc_url" } }` ([Errors](https://docs.rustine.io/en/api/errors.md)). - **Writes**: send an `Idempotency-Key` on `POST` requests ([Idempotency](https://docs.rustine.io/en/idempotency.md)). - **Language**: error messages and issue texts come in the language of the key's owner. Every response carries `Request-Id` (give it to support), `Rustine-Version` (the contract version that answered, see [Versions](https://docs.rustine.io/en/versioning.md)) and the `X-RateLimit-*` headers ([Rate limits and quotas](https://docs.rustine.io/en/rate-limits.md)). The full specification is served as-is at [`/openapi.json`](https://docs.rustine.io/openapi.json) (OpenAPI 3.1): import it into Postman, Insomnia or a client generator. ## Routes - `GET /projects`: [List projects](https://docs.rustine.io/en/api/projects/list.md) - `POST /projects`: [Create a project](https://docs.rustine.io/en/api/projects/create.md) - `GET /projects/{project_id}`: [Get a project](https://docs.rustine.io/en/api/projects/get.md) - `POST /projects/{project_id}/targets`: [Add a site](https://docs.rustine.io/en/api/projects/add-site.md) - `POST /targets/{target_id}/scans`: [Start a scan](https://docs.rustine.io/en/api/scans/create.md) - `GET /scans/{scan_id}`: [Get a scan](https://docs.rustine.io/en/api/scans/get.md) - `GET /projects/{project_id}/scans`: [List a project's scans](https://docs.rustine.io/en/api/scans/list.md) - `GET /projects/{project_id}/issues`: [List issues](https://docs.rustine.io/en/api/issues/list.md) - `GET /issues/{issue_id}`: [Get an issue](https://docs.rustine.io/en/api/issues/get.md) - `POST /issues/{issue_id}/fix`: [Unlock the fix](https://docs.rustine.io/en/api/issues/unlock-fix.md) - `GET /issues/{issue_id}/fix`: [Read a fix](https://docs.rustine.io/en/api/issues/fix.md) - `POST /issues/{issue_id}/recheck`: [Recheck an issue](https://docs.rustine.io/en/api/issues/recheck.md) - `PATCH /issues/{issue_id}`: [Mark an issue](https://docs.rustine.io/en/api/issues/update.md) - `GET /account`: [Your plan and quotas](https://docs.rustine.io/en/api/account.md) ## Examples ### curl ```bash curl -i https://api.rustine.io/v1/account \ -H "Authorization: Bearer $RUSTINE_API_KEY" ``` ### Response: 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 ``` --- # List projects Your projects, with their sites, their score out of 100 and their open issues. The site ids (`targets[].id`) are what you pass to [start a scan](https://docs.rustine.io/en/api/scans/create). ``` GET https://api.rustine.io/v1/projects ``` OAuth permission: `scans:read` · an API key has them all ## Query parameters - `limit` (integer) How many objects to return, 1 to 100 (default 20). - `starting_after` (string) Cursor: the id of the last object of the previous page. ## Response (200 OK) - `object` (string, required) - `data` (array of object, required) - `id` (string, required) Project id. - `object` (string, required) - `name` (string, required) Project name. - `score` (number | null, required) Score out of 100; null before the first finished scan. - `grade` (string | null, required) Grade from A to F; null before the first finished scan. (`A`, `B`, `C`, `D`, `F`) - `open_issues` (integer, required) Open issues across the project's targets, hidden ones included. - `paused` (boolean, required) Paused because the plan covers fewer projects: readable, but no scan, recheck, fix or new address. - `last_scan_at` (string | null, required) End of the last finished scan of any target. - `targets` (array of object, required) - `id` (string, required) Target id: a site (domain) or a GitHub repository of the project. - `object` (string, required) - `project_id` (string, required) Project the target belongs to. - `kind` (string, required) `domain` (a site) or `repository`. (`domain`, `server`, `repository`) - `address` (string, required) Hostname of a site (`app.example.com`) or `owner/repo` of a repository. - `verified` (boolean, required) Whether the workspace proved it controls this target. Without proof, issue details stay hidden and only quick scans run. - `score` (number | null, required) Score out of 100; null before the first finished scan. - `grade` (string | null, required) Grade from A to F; null before the first finished scan. (`A`, `B`, `C`, `D`, `F`) - `last_scan_at` (string | null, required) End of the last finished scan. - `hidden_issues` (object | null, required) Unverified site: what its hidden issues amount to, counts only (never which, nor where). Null when nothing is hidden. - `urgent` (integer, required) Open critical and high issues hidden until the site is verified. - `later` (integer, required) Open medium and low issues hidden until the site is verified. - `has_more` (boolean, required) Whether more objects follow: pass the last id as `starting_after`. ## Examples ### curl ```bash # The key stays in a variable, never in the address 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() ``` ### Response: 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 } ``` ## Errors | Status | error.code | What happens | | --- | --- | --- | | 400 | `parameter_invalid` | A parameter is missing or invalid; `param` names it. | | 400 | `cursor_invalid` | `starting_after` is not the id of an item of this list. | | 401 | `authentication_required` | No key in the request. Pass it in the `Authorization: Bearer rst_…` header, never in the address. | | 401 | `invalid_api_key` | The key or OAuth token is wrong, revoked or expired. Check it is copied in full, or create a new key. | | 402 | `api_plan_required` | The workspace is on the Free plan. The API comes with Pro, or on a 14-day trial with Solo. | | 402 | `api_trial_ended` | Solo's 14-day trial is over. Move to Pro: your keys work again at once. | | 403 | `scope_missing` | The OAuth token lacks the permission this route needs (shown on its page). Reconnect the agent to grant it. An API key has every permission. | | 429 | `rate_limited` | Too many requests for this key (120 per minute). Wait `Retry-After` seconds. | | 500 | `internal_error` | An error on our side, reported automatically. Retry later; give the `Request-Id` to support if it comes back. | | 503 | `service_unavailable` | Rustine is unavailable for a moment. Retry with the same `Idempotency-Key`. | https://docs.rustine.io/en/api/errors --- # Create a project Creates a project, with the address of its first site if you give one. The site is not proved yet: you can run a `quick` scan on it right away, the full audit waits for the proof. ``` POST https://api.rustine.io/v1/projects ``` OAuth permission: `scans:run` · an API key has them all > Only add a site you own or are allowed to test. A project counts in your plan (1 included in Solo, 3 in Pro, then extra projects): past that, `402 project_limit_reached`. ## Headers - `Idempotency-Key` (string) A unique key (1 to 255 printable characters, a UUID is fine) to retry this request safely: the first response is replayed for 24 hours; the same key with another request answers 409 `idempotency_conflict`. ## Request body - `name` (string, required) Project name. - `url` (string) The project's first site (optional). Only add a site you own or are allowed to test. ## Response (201 Created) - `id` (string, required) Project id. - `object` (string, required) - `name` (string, required) Project name. - `score` (number | null, required) Score out of 100; null before the first finished scan. - `grade` (string | null, required) Grade from A to F; null before the first finished scan. (`A`, `B`, `C`, `D`, `F`) - `open_issues` (integer, required) Open issues across the project's targets, hidden ones included. - `paused` (boolean, required) Paused because the plan covers fewer projects: readable, but no scan, recheck, fix or new address. - `last_scan_at` (string | null, required) End of the last finished scan of any target. - `targets` (array of object, required) - `id` (string, required) Target id: a site (domain) or a GitHub repository of the project. - `object` (string, required) - `project_id` (string, required) Project the target belongs to. - `kind` (string, required) `domain` (a site) or `repository`. (`domain`, `server`, `repository`) - `address` (string, required) Hostname of a site (`app.example.com`) or `owner/repo` of a repository. - `verified` (boolean, required) Whether the workspace proved it controls this target. Without proof, issue details stay hidden and only quick scans run. - `score` (number | null, required) Score out of 100; null before the first finished scan. - `grade` (string | null, required) Grade from A to F; null before the first finished scan. (`A`, `B`, `C`, `D`, `F`) - `last_scan_at` (string | null, required) End of the last finished scan. - `hidden_issues` (object | null, required) Unverified site: what its hidden issues amount to, counts only (never which, nor where). Null when nothing is hidden. - `urgent` (integer, required) Open critical and high issues hidden until the site is verified. - `later` (integer, required) Open medium and low issues hidden until the site is verified. ## Examples ### curl ```bash # The key stays in a variable, never in the address 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() ``` ### Response: 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 } ] } ``` ## Errors | Status | error.code | What happens | | --- | --- | --- | | 400 | `body_invalid` | The request body is not valid JSON. Check the `Content-Type: application/json` header. | | 400 | `parameter_invalid` | A parameter is missing or invalid; `param` names it. | | 400 | `url_invalid` | The address is not the valid URL of a public site. | | 400 | `url_private` | Rustine only scans public sites, never a local or private address. | | 400 | `idempotency_key_invalid` | The `Idempotency-Key` header must be 1 to 255 printable characters. | | 401 | `authentication_required` | No key in the request. Pass it in the `Authorization: Bearer rst_…` header, never in the address. | | 401 | `invalid_api_key` | The key or OAuth token is wrong, revoked or expired. Check it is copied in full, or create a new key. | | 402 | `api_plan_required` | The workspace is on the Free plan. The API comes with Pro, or on a 14-day trial with Solo. | | 402 | `api_trial_ended` | Solo's 14-day trial is over. Move to Pro: your keys work again at once. | | 402 | `project_limit_reached` | All your plan's projects are in use. Add an extra project or change plans. | | 402 | `domain_taken` | On the Free plan, a domain another free account already follows cannot be added or scanned. | | 403 | `scope_missing` | The OAuth token lacks the permission this route needs (shown on its page). Reconnect the agent to grant it. An API key has every permission. | | 409 | `site_excluded` | The owner of this site asked not to be scanned. Rustine will not scan it. | | 409 | `idempotency_conflict` | This `Idempotency-Key` was already used for another request (method, path or body). A new action takes a new key. | | 409 | `idempotency_in_progress` | A request with this `Idempotency-Key` is still running. Retry in a moment. | | 429 | `rate_limited` | Too many requests for this key (120 per minute). Wait `Retry-After` seconds. | | 500 | `internal_error` | An error on our side, reported automatically. Retry later; give the `Request-Id` to support if it comes back. | | 503 | `service_unavailable` | Rustine is unavailable for a moment. Retry with the same `Idempotency-Key`. | https://docs.rustine.io/en/api/errors --- # Get a project One project, its sites and its latest score. Handy after a scan to watch the grade move. ``` GET https://api.rustine.io/v1/projects/{project_id} ``` OAuth permission: `scans:read` · an API key has them all ## Path parameters - `project_id` (string, required) Project id. ## Response (200 OK) - `id` (string, required) Project id. - `object` (string, required) - `name` (string, required) Project name. - `score` (number | null, required) Score out of 100; null before the first finished scan. - `grade` (string | null, required) Grade from A to F; null before the first finished scan. (`A`, `B`, `C`, `D`, `F`) - `open_issues` (integer, required) Open issues across the project's targets, hidden ones included. - `paused` (boolean, required) Paused because the plan covers fewer projects: readable, but no scan, recheck, fix or new address. - `last_scan_at` (string | null, required) End of the last finished scan of any target. - `targets` (array of object, required) - `id` (string, required) Target id: a site (domain) or a GitHub repository of the project. - `object` (string, required) - `project_id` (string, required) Project the target belongs to. - `kind` (string, required) `domain` (a site) or `repository`. (`domain`, `server`, `repository`) - `address` (string, required) Hostname of a site (`app.example.com`) or `owner/repo` of a repository. - `verified` (boolean, required) Whether the workspace proved it controls this target. Without proof, issue details stay hidden and only quick scans run. - `score` (number | null, required) Score out of 100; null before the first finished scan. - `grade` (string | null, required) Grade from A to F; null before the first finished scan. (`A`, `B`, `C`, `D`, `F`) - `last_scan_at` (string | null, required) End of the last finished scan. - `hidden_issues` (object | null, required) Unverified site: what its hidden issues amount to, counts only (never which, nor where). Null when nothing is hidden. - `urgent` (integer, required) Open critical and high issues hidden until the site is verified. - `later` (integer, required) Open medium and low issues hidden until the site is verified. ## Examples ### curl ```bash # The key stays in a variable, never in the address 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() ``` ### Response: 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 } ] } ``` ## Errors | Status | error.code | What happens | | --- | --- | --- | | 401 | `authentication_required` | No key in the request. Pass it in the `Authorization: Bearer rst_…` header, never in the address. | | 401 | `invalid_api_key` | The key or OAuth token is wrong, revoked or expired. Check it is copied in full, or create a new key. | | 402 | `api_plan_required` | The workspace is on the Free plan. The API comes with Pro, or on a 14-day trial with Solo. | | 402 | `api_trial_ended` | Solo's 14-day trial is over. Move to Pro: your keys work again at once. | | 403 | `scope_missing` | The OAuth token lacks the permission this route needs (shown on its page). Reconnect the agent to grant it. An API key has every permission. | | 404 | `resource_not_found` | Nothing with this id in the key's workspace. An object of another workspace answers the same: its existence is never revealed. | | 429 | `rate_limited` | Too many requests for this key (120 per minute). Wait `Retry-After` seconds. | | 500 | `internal_error` | An error on our side, reported automatically. Retry later; give the `Request-Id` to support if it comes back. | | 503 | `service_unavailable` | Rustine is unavailable for a moment. Retry with the same `Idempotency-Key`. | https://docs.rustine.io/en/api/errors --- # Add a site Adds an address to a project: a subdomain (`app.`, `api.`) or another domain of the same product. ``` POST https://api.rustine.io/v1/projects/{project_id}/targets ``` OAuth permission: `scans:run` · an API key has them all > Only a site you own or are allowed to test. How many addresses a project holds depends on the plan (5 on Solo, 20 on Pro): past that, `402 address_limit_reached`. ## Path parameters - `project_id` (string, required) Project id. ## Headers - `Idempotency-Key` (string) A unique key (1 to 255 printable characters, a UUID is fine) to retry this request safely: the first response is replayed for 24 hours; the same key with another request answers 409 `idempotency_conflict`. ## Request body - `url` (string, required) Address of the site (`https://app.example.com`). Only a site you own or are allowed to test. ## Response (201 Created) - `id` (string, required) Target id: a site (domain) or a GitHub repository of the project. - `object` (string, required) - `project_id` (string, required) Project the target belongs to. - `kind` (string, required) `domain` (a site) or `repository`. (`domain`, `server`, `repository`) - `address` (string, required) Hostname of a site (`app.example.com`) or `owner/repo` of a repository. - `verified` (boolean, required) Whether the workspace proved it controls this target. Without proof, issue details stay hidden and only quick scans run. - `score` (number | null, required) Score out of 100; null before the first finished scan. - `grade` (string | null, required) Grade from A to F; null before the first finished scan. (`A`, `B`, `C`, `D`, `F`) - `last_scan_at` (string | null, required) End of the last finished scan. - `hidden_issues` (object | null, required) Unverified site: what its hidden issues amount to, counts only (never which, nor where). Null when nothing is hidden. - `urgent` (integer, required) Open critical and high issues hidden until the site is verified. - `later` (integer, required) Open medium and low issues hidden until the site is verified. ## Examples ### curl ```bash # The key stays in a variable, never in the address 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() ``` ### Response: 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 } ``` ## Errors | Status | error.code | What happens | | --- | --- | --- | | 400 | `body_invalid` | The request body is not valid JSON. Check the `Content-Type: application/json` header. | | 400 | `parameter_invalid` | A parameter is missing or invalid; `param` names it. | | 400 | `url_invalid` | The address is not the valid URL of a public site. | | 400 | `url_private` | Rustine only scans public sites, never a local or private address. | | 400 | `idempotency_key_invalid` | The `Idempotency-Key` header must be 1 to 255 printable characters. | | 401 | `authentication_required` | No key in the request. Pass it in the `Authorization: Bearer rst_…` header, never in the address. | | 401 | `invalid_api_key` | The key or OAuth token is wrong, revoked or expired. Check it is copied in full, or create a new key. | | 402 | `api_plan_required` | The workspace is on the Free plan. The API comes with Pro, or on a 14-day trial with Solo. | | 402 | `api_trial_ended` | Solo's 14-day trial is over. Move to Pro: your keys work again at once. | | 402 | `address_limit_reached` | The project already follows as many addresses as your plan allows. | | 402 | `domain_taken` | On the Free plan, a domain another free account already follows cannot be added or scanned. | | 402 | `project_paused` | The project is paused: your plan covers fewer projects than your workspace has. Its issues stay readable, but nothing new runs. | | 403 | `scope_missing` | The OAuth token lacks the permission this route needs (shown on its page). Reconnect the agent to grant it. An API key has every permission. | | 404 | `resource_not_found` | Nothing with this id in the key's workspace. An object of another workspace answers the same: its existence is never revealed. | | 409 | `site_excluded` | The owner of this site asked not to be scanned. Rustine will not scan it. | | 409 | `target_exists` | The project already follows this address. | | 409 | `idempotency_conflict` | This `Idempotency-Key` was already used for another request (method, path or body). A new action takes a new key. | | 409 | `idempotency_in_progress` | A request with this `Idempotency-Key` is still running. Retry in a moment. | | 429 | `rate_limited` | Too many requests for this key (120 per minute). Wait `Retry-After` seconds. | | 500 | `internal_error` | An error on our side, reported automatically. Retry later; give the `Request-Id` to support if it comes back. | | 503 | `service_unavailable` | Rustine is unavailable for a moment. Retry with the same `Idempotency-Key`. | https://docs.rustine.io/en/api/errors --- # Start a scan Starts a scan of one of your project's sites, or an analysis of a GitHub repository. The scan runs in the background: the answer comes back at once (`201`, status `queued`) and you follow it with [Get a scan](https://docs.rustine.io/en/api/scans/get). ``` POST https://api.rustine.io/v1/targets/{target_id}/scans ``` OAuth permission: `scans:run` · an API key has them all > A **full audit** only runs on a proved site (DNS, file or meta tag): the law forbids testing a site you do not control. Without a proof, ask for `quick`: Rustine only reads what any visitor sees. If a scan of the site is running, or one of the same kind finished within the hour, you get that one (`200`, `reused: true`) and nothing is counted. ## Path parameters - `target_id` (string, required) Target id (a site or a repository of a project). ## Headers - `Idempotency-Key` (string) A unique key (1 to 255 printable characters, a UUID is fine) to retry this request safely: the first response is replayed for 24 hours; the same key with another request answers 409 `idempotency_conflict`. ## Request body - `kind` (string) `quick` (default for a site), `audit` (verified sites, Solo and Pro) or `repository` (the only kind, and the default, for a repository). (`quick`, `audit`, `repository`) ## Response (201 Created) - `id` (string, required) Scan id. - `object` (string, required) - `target_id` (string, required) Target scanned. - `kind` (string, required) `quick` (what any visitor could see), `audit` (adds deeper read-only checks, verified sites only) or `repository` (analysis of a GitHub repository). (`quick`, `audit`, `repository`) - `status` (string, required) Scans run asynchronously: poll until `done` or `failed`. (`queued`, `running`, `done`, `failed`) - `score` (number | null, required) Score out of 100, once done. - `grade` (string | null, required) Grade, once done. (`A`, `B`, `C`, `D`, `F`) - `coverage` (number | null, required) Share of the checks that could run (0 to 1), once done. - `audit_fallback` (boolean, required) An audit was asked but the site's proof no longer held: a quick scan ran instead and no audit was counted. - `reused` (boolean, required) True when the request got an existing scan instead of a new one (one already running, or one of the same kind in the last hour); nothing was counted. - `next_real_scan_at` (string | null, required) When `reused` because of a recent scan: when a new scan of this kind may start. - `created_at` (string, required) When the scan was requested. - `finished_at` (string | null, required) When the scan finished. ## Examples ### curl ```bash # The key stays in a variable, never in the address 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() ``` ### Response: 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 } ``` ## Errors | Status | error.code | What happens | | --- | --- | --- | | 400 | `body_invalid` | The request body is not valid JSON. Check the `Content-Type: application/json` header. | | 400 | `parameter_invalid` | A parameter is missing or invalid; `param` names it. | | 400 | `target_kind_invalid` | The scan kind does not fit the target: `repository` for a GitHub repository, `quick` or `audit` for a site. | | 400 | `idempotency_key_invalid` | The `Idempotency-Key` header must be 1 to 255 printable characters. | | 401 | `authentication_required` | No key in the request. Pass it in the `Authorization: Bearer rst_…` header, never in the address. | | 401 | `invalid_api_key` | The key or OAuth token is wrong, revoked or expired. Check it is copied in full, or create a new key. | | 402 | `api_plan_required` | The workspace is on the Free plan. The API comes with Pro, or on a 14-day trial with Solo. | | 402 | `api_trial_ended` | Solo's 14-day trial is over. Move to Pro: your keys work again at once. | | 402 | `audit_not_in_plan` | The Free plan runs quick scans only; the full audit comes with Solo and Pro. | | 402 | `repositories_not_in_plan` | GitHub repository analyses come with Solo and Pro. | | 402 | `scan_quota_exceeded` | This period's quick scans are used up, and no pack credit is left. | | 402 | `audit_quota_exceeded` | This period's full audits are used up, and no pack credit is left. | | 402 | `project_paused` | The project is paused: your plan covers fewer projects than your workspace has. Its issues stay readable, but nothing new runs. | | 402 | `domain_taken` | On the Free plan, a domain another free account already follows cannot be added or scanned. | | 403 | `scope_missing` | The OAuth token lacks the permission this route needs (shown on its page). Reconnect the agent to grant it. An API key has every permission. | | 404 | `resource_not_found` | Nothing with this id in the key's workspace. An object of another workspace answers the same: its existence is never revealed. | | 409 | `proof_required` | The site must first be proved yours (full audit, fix, marking). Prove it in the app: DNS, file or meta tag. | | 409 | `site_excluded` | The owner of this site asked not to be scanned. Rustine will not scan it. | | 409 | `github_unavailable` | GitHub repository analyses are not available right now. | | 409 | `idempotency_conflict` | This `Idempotency-Key` was already used for another request (method, path or body). A new action takes a new key. | | 409 | `idempotency_in_progress` | A request with this `Idempotency-Key` is still running. Retry in a moment. | | 429 | `rate_limited` | Too many requests for this key (120 per minute). Wait `Retry-After` seconds. | | 429 | `repository_analyses_limit_reached` | The workspace started many repository analyses in the last 24 hours. Try again later. | | 500 | `internal_error` | An error on our side, reported automatically. Retry later; give the `Request-Id` to support if it comes back. | | 503 | `service_unavailable` | Rustine is unavailable for a moment. Retry with the same `Idempotency-Key`. | | 503 | `platform_busy` | Rustine is not starting new scans for a few minutes. Retry later; nothing was charged. | https://docs.rustine.io/en/api/errors --- # Get a scan Where a scan stands. Poll this route every 3 to 5 seconds until `done` or `failed`; a quick scan usually takes less than a minute. ``` GET https://api.rustine.io/v1/scans/{scan_id} ``` OAuth permission: `scans:read` · an API key has them all ## Path parameters - `scan_id` (string, required) Scan id. ## Response (200 OK) - `id` (string, required) Scan id. - `object` (string, required) - `target_id` (string, required) Target scanned. - `kind` (string, required) `quick` (what any visitor could see), `audit` (adds deeper read-only checks, verified sites only) or `repository` (analysis of a GitHub repository). (`quick`, `audit`, `repository`) - `status` (string, required) Scans run asynchronously: poll until `done` or `failed`. (`queued`, `running`, `done`, `failed`) - `score` (number | null, required) Score out of 100, once done. - `grade` (string | null, required) Grade, once done. (`A`, `B`, `C`, `D`, `F`) - `coverage` (number | null, required) Share of the checks that could run (0 to 1), once done. - `audit_fallback` (boolean, required) An audit was asked but the site's proof no longer held: a quick scan ran instead and no audit was counted. - `reused` (boolean, required) True when the request got an existing scan instead of a new one (one already running, or one of the same kind in the last hour); nothing was counted. - `next_real_scan_at` (string | null, required) When `reused` because of a recent scan: when a new scan of this kind may start. - `created_at` (string, required) When the scan was requested. - `finished_at` (string | null, required) When the scan finished. ## Examples ### curl ```bash # The key stays in a variable, never in the address 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() ``` ### Response: 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" } ``` ## Errors | Status | error.code | What happens | | --- | --- | --- | | 401 | `authentication_required` | No key in the request. Pass it in the `Authorization: Bearer rst_…` header, never in the address. | | 401 | `invalid_api_key` | The key or OAuth token is wrong, revoked or expired. Check it is copied in full, or create a new key. | | 402 | `api_plan_required` | The workspace is on the Free plan. The API comes with Pro, or on a 14-day trial with Solo. | | 402 | `api_trial_ended` | Solo's 14-day trial is over. Move to Pro: your keys work again at once. | | 403 | `scope_missing` | The OAuth token lacks the permission this route needs (shown on its page). Reconnect the agent to grant it. An API key has every permission. | | 404 | `resource_not_found` | Nothing with this id in the key's workspace. An object of another workspace answers the same: its existence is never revealed. | | 429 | `rate_limited` | Too many requests for this key (120 per minute). Wait `Retry-After` seconds. | | 500 | `internal_error` | An error on our side, reported automatically. Retry later; give the `Request-Id` to support if it comes back. | | 503 | `service_unavailable` | Rustine is unavailable for a moment. Retry with the same `Idempotency-Key`. | https://docs.rustine.io/en/api/errors --- # List a project's scans A project's scans, newest first. How far back it goes depends on your plan. ``` GET https://api.rustine.io/v1/projects/{project_id}/scans ``` OAuth permission: `scans:read` · an API key has them all ## Path parameters - `project_id` (string, required) Project id. ## Query parameters - `limit` (integer) How many objects to return, 1 to 100 (default 20). - `starting_after` (string) Cursor: the id of the last object of the previous page. ## Response (200 OK) - `object` (string, required) - `data` (array of object, required) - `id` (string, required) Scan id. - `object` (string, required) - `target_id` (string, required) Target scanned. - `kind` (string, required) `quick` (what any visitor could see), `audit` (adds deeper read-only checks, verified sites only) or `repository` (analysis of a GitHub repository). (`quick`, `audit`, `repository`) - `status` (string, required) Scans run asynchronously: poll until `done` or `failed`. (`queued`, `running`, `done`, `failed`) - `score` (number | null, required) Score out of 100, once done. - `grade` (string | null, required) Grade, once done. (`A`, `B`, `C`, `D`, `F`) - `coverage` (number | null, required) Share of the checks that could run (0 to 1), once done. - `audit_fallback` (boolean, required) An audit was asked but the site's proof no longer held: a quick scan ran instead and no audit was counted. - `reused` (boolean, required) True when the request got an existing scan instead of a new one (one already running, or one of the same kind in the last hour); nothing was counted. - `next_real_scan_at` (string | null, required) When `reused` because of a recent scan: when a new scan of this kind may start. - `created_at` (string, required) When the scan was requested. - `finished_at` (string | null, required) When the scan finished. - `has_more` (boolean, required) Whether more objects follow: pass the last id as `starting_after`. ## Examples ### curl ```bash # The key stays in a variable, never in the address 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() ``` ### Response: 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 } ``` ## Errors | Status | error.code | What happens | | --- | --- | --- | | 400 | `parameter_invalid` | A parameter is missing or invalid; `param` names it. | | 400 | `cursor_invalid` | `starting_after` is not the id of an item of this list. | | 401 | `authentication_required` | No key in the request. Pass it in the `Authorization: Bearer rst_…` header, never in the address. | | 401 | `invalid_api_key` | The key or OAuth token is wrong, revoked or expired. Check it is copied in full, or create a new key. | | 402 | `api_plan_required` | The workspace is on the Free plan. The API comes with Pro, or on a 14-day trial with Solo. | | 402 | `api_trial_ended` | Solo's 14-day trial is over. Move to Pro: your keys work again at once. | | 403 | `scope_missing` | The OAuth token lacks the permission this route needs (shown on its page). Reconnect the agent to grant it. An API key has every permission. | | 404 | `resource_not_found` | Nothing with this id in the key's workspace. An object of another workspace answers the same: its existence is never revealed. | | 429 | `rate_limited` | Too many requests for this key (120 per minute). Wait `Retry-After` seconds. | | 500 | `internal_error` | An error on our side, reported automatically. Retry later; give the `Request-Id` to support if it comes back. | | 503 | `service_unavailable` | Rustine is unavailable for a moment. Retry with the same `Idempotency-Key`. | https://docs.rustine.io/en/api/errors --- # List issues A project's issues, worst first, with how sure Rustine is. Filter by status, severity or site. ``` GET https://api.rustine.io/v1/projects/{project_id}/issues ``` OAuth permission: `scans:read` · an API key has them all > Issues of a site not proved yet stay hidden: the site only gives how many there are (`targets[].hidden_issues` in [Get a project](https://docs.rustine.io/en/api/projects/get)), until you prove it is yours. ## Path parameters - `project_id` (string, required) Project id. ## Query parameters - `limit` (integer) How many objects to return, 1 to 100 (default 20). - `starting_after` (string) Cursor: the id of the last object of the previous page. - `status` (string) Only issues in this status. (`open`, `fixed`, `false_positive`, `accepted_risk`) - `severity` (string) Only issues of this severity. (`critical`, `high`, `medium`, `low`, `info`) - `target_id` (string) Only the issues of this target. ## Response (200 OK) - `object` (string, required) - `data` (array of object, required) - `id` (string, required) Issue id; it keeps its identity from one scan to the next. - `object` (string, required) - `project_id` (string, required) Project of the issue. - `target_id` (string, required) Target where it was found. - `address` (string, required) Hostname of the site or `owner/repo`. - `control_id` (string, required) The check that found it (stable identifier). - `family` (string, required) Family of the check. (`database`, `secrets`, `exposedFiles`, `personalData`, `vulnerableVersions`, `configuration`, `compliance`, `server`, `code`) - `severity` (string, required) (`critical`, `high`, `medium`, `low`, `info`) - `confidence` (string, required) How sure the finding is. (`confirmed`, `likely`, `estimated`) - `status` (string, required) `open`; `fixed` (a later scan or recheck no longer finds it); `false_positive` (left out of the score); `accepted_risk` (kept in the score). (`open`, `fixed`, `false_positive`, `accepted_risk`) - `title` (string, required) One line, in the language of the key's owner. - `explanation` (string, required) The risk in plain words, in the key owner's language. - `detail` (string, required) Technical detail, in the key owner's language. - `location` (string, required) Where: a path, a table, or `file:line@commit` in a repository. - `file` (string | null, required) Repository issue: the file. - `line` (integer | null, required) Repository issue: the line. - `commit` (string | null, required) Repository issue: the commit analysed. - `evidence` (string, required) What the check observed, secrets masked. - `fix_unlocked` (boolean, required) Whether its fix is unlocked (readable for free). - `status_reason` (string | null, required) Why it was marked a false positive or an accepted risk. - `marked_by` (string | null, required) Who marked it: an agent, an API key's name, or `app`. - `first_seen_at` (string, required) First scan that found it. - `last_seen_at` (string, required) Last scan that found it. - `fixed_at` (string | null, required) When it was found fixed. - `has_more` (boolean, required) Whether more objects follow: pass the last id as `starting_after`. ## Examples ### curl ```bash # The key stays in a variable, never in the address 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() ``` ### Response: 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 } ``` ## Errors | Status | error.code | What happens | | --- | --- | --- | | 400 | `parameter_invalid` | A parameter is missing or invalid; `param` names it. | | 400 | `cursor_invalid` | `starting_after` is not the id of an item of this list. | | 401 | `authentication_required` | No key in the request. Pass it in the `Authorization: Bearer rst_…` header, never in the address. | | 401 | `invalid_api_key` | The key or OAuth token is wrong, revoked or expired. Check it is copied in full, or create a new key. | | 402 | `api_plan_required` | The workspace is on the Free plan. The API comes with Pro, or on a 14-day trial with Solo. | | 402 | `api_trial_ended` | Solo's 14-day trial is over. Move to Pro: your keys work again at once. | | 403 | `scope_missing` | The OAuth token lacks the permission this route needs (shown on its page). Reconnect the agent to grant it. An API key has every permission. | | 404 | `resource_not_found` | Nothing with this id in the key's workspace. An object of another workspace answers the same: its existence is never revealed. | | 429 | `rate_limited` | Too many requests for this key (120 per minute). Wait `Retry-After` seconds. | | 500 | `internal_error` | An error on our side, reported automatically. Retry later; give the `Request-Id` to support if it comes back. | | 503 | `service_unavailable` | Rustine is unavailable for a moment. Retry with the same `Idempotency-Key`. | https://docs.rustine.io/en/api/errors --- # Get an issue One issue with its severity, what Rustine observed and where it saw it. The fix is asked for separately, with [Unlock the fix](https://docs.rustine.io/en/api/issues/unlock-fix). ``` GET https://api.rustine.io/v1/issues/{issue_id} ``` OAuth permission: `scans:read` · an API key has them all ## Path parameters - `issue_id` (string, required) Issue id. ## Response (200 OK) - `id` (string, required) Issue id; it keeps its identity from one scan to the next. - `object` (string, required) - `project_id` (string, required) Project of the issue. - `target_id` (string, required) Target where it was found. - `address` (string, required) Hostname of the site or `owner/repo`. - `control_id` (string, required) The check that found it (stable identifier). - `family` (string, required) Family of the check. (`database`, `secrets`, `exposedFiles`, `personalData`, `vulnerableVersions`, `configuration`, `compliance`, `server`, `code`) - `severity` (string, required) (`critical`, `high`, `medium`, `low`, `info`) - `confidence` (string, required) How sure the finding is. (`confirmed`, `likely`, `estimated`) - `status` (string, required) `open`; `fixed` (a later scan or recheck no longer finds it); `false_positive` (left out of the score); `accepted_risk` (kept in the score). (`open`, `fixed`, `false_positive`, `accepted_risk`) - `title` (string, required) One line, in the language of the key's owner. - `explanation` (string, required) The risk in plain words, in the key owner's language. - `detail` (string, required) Technical detail, in the key owner's language. - `location` (string, required) Where: a path, a table, or `file:line@commit` in a repository. - `file` (string | null, required) Repository issue: the file. - `line` (integer | null, required) Repository issue: the line. - `commit` (string | null, required) Repository issue: the commit analysed. - `evidence` (string, required) What the check observed, secrets masked. - `fix_unlocked` (boolean, required) Whether its fix is unlocked (readable for free). - `status_reason` (string | null, required) Why it was marked a false positive or an accepted risk. - `marked_by` (string | null, required) Who marked it: an agent, an API key's name, or `app`. - `first_seen_at` (string, required) First scan that found it. - `last_seen_at` (string, required) Last scan that found it. - `fixed_at` (string | null, required) When it was found fixed. ## Examples ### curl ```bash # The key stays in a variable, never in the address 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() ``` ### Response: 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 } ``` ## Errors | Status | error.code | What happens | | --- | --- | --- | | 401 | `authentication_required` | No key in the request. Pass it in the `Authorization: Bearer rst_…` header, never in the address. | | 401 | `invalid_api_key` | The key or OAuth token is wrong, revoked or expired. Check it is copied in full, or create a new key. | | 402 | `api_plan_required` | The workspace is on the Free plan. The API comes with Pro, or on a 14-day trial with Solo. | | 402 | `api_trial_ended` | Solo's 14-day trial is over. Move to Pro: your keys work again at once. | | 403 | `scope_missing` | The OAuth token lacks the permission this route needs (shown on its page). Reconnect the agent to grant it. An API key has every permission. | | 404 | `resource_not_found` | Nothing with this id in the key's workspace. An object of another workspace answers the same: its existence is never revealed. | | 429 | `rate_limited` | Too many requests for this key (120 per minute). Wait `Retry-After` seconds. | | 500 | `internal_error` | An error on our side, reported automatically. Retry later; give the `Request-Id` to support if it comes back. | | 503 | `service_unavailable` | Rustine is unavailable for a moment. Retry with the same `Idempotency-Key`. | https://docs.rustine.io/en/api/errors --- # Unlock the fix Unlocks the fix of an issue and returns it: a prompt written for your coding agent, to paste into Cursor, Claude Code or Lovable. ``` POST https://api.rustine.io/v1/issues/{issue_id}/fix ``` OAuth permission: `fixes:unlock` · an API key has them all > The first call for an issue **uses one fix** from your plan (`charged: true`); later calls are free. Send an [`Idempotency-Key`](https://docs.rustine.io/en/idempotency): a replayed call never charges twice. Rustine never edits your code. ## Path parameters - `issue_id` (string, required) Issue id. ## Headers - `Idempotency-Key` (string) A unique key (1 to 255 printable characters, a UUID is fine) to retry this request safely: the first response is replayed for 24 hours; the same key with another request answers 409 `idempotency_conflict`. ## Response (200 OK) - `object` (string, required) - `issue_id` (string, required) The issue. - `prompt` (string, required) The fix as instructions for a coding agent. - `ai_fix` (string | null, required) A patch written by Rustine, if any. - `charged` (boolean, required) True when this call unlocked the fix and consumed one; false when it was already unlocked. - `fixes_left` (integer, required) Fixes left this period after this call (quota plus pack credits). ## Examples ### curl ```bash # The key stays in a variable, never in the address 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() ``` ### Response: 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 } ``` ## Errors | Status | error.code | What happens | | --- | --- | --- | | 400 | `idempotency_key_invalid` | The `Idempotency-Key` header must be 1 to 255 printable characters. | | 401 | `authentication_required` | No key in the request. Pass it in the `Authorization: Bearer rst_…` header, never in the address. | | 401 | `invalid_api_key` | The key or OAuth token is wrong, revoked or expired. Check it is copied in full, or create a new key. | | 402 | `api_plan_required` | The workspace is on the Free plan. The API comes with Pro, or on a 14-day trial with Solo. | | 402 | `api_trial_ended` | Solo's 14-day trial is over. Move to Pro: your keys work again at once. | | 402 | `fix_quota_exceeded` | This period's fixes are used up, and no pack credit is left. A fix already unlocked stays readable. | | 402 | `project_paused` | The project is paused: your plan covers fewer projects than your workspace has. Its issues stay readable, but nothing new runs. | | 403 | `scope_missing` | The OAuth token lacks the permission this route needs (shown on its page). Reconnect the agent to grant it. An API key has every permission. | | 404 | `resource_not_found` | Nothing with this id in the key's workspace. An object of another workspace answers the same: its existence is never revealed. | | 409 | `proof_required` | The site must first be proved yours (full audit, fix, marking). Prove it in the app: DNS, file or meta tag. | | 409 | `idempotency_conflict` | This `Idempotency-Key` was already used for another request (method, path or body). A new action takes a new key. | | 409 | `idempotency_in_progress` | A request with this `Idempotency-Key` is still running. Retry in a moment. | | 429 | `rate_limited` | Too many requests for this key (120 per minute). Wait `Retry-After` seconds. | | 500 | `internal_error` | An error on our side, reported automatically. Retry later; give the `Request-Id` to support if it comes back. | | 503 | `service_unavailable` | Rustine is unavailable for a moment. Retry with the same `Idempotency-Key`. | https://docs.rustine.io/en/api/errors --- # Read a fix Reads a fix you already unlocked, at no cost. If it is not unlocked yet, the answer is `404 fix_locked` and nothing is unlocked: use [Unlock the fix](https://docs.rustine.io/en/api/issues/unlock-fix). ``` GET https://api.rustine.io/v1/issues/{issue_id}/fix ``` OAuth permission: `scans:read` · an API key has them all ## Path parameters - `issue_id` (string, required) Issue id. ## Response (200 OK) - `object` (string, required) - `issue_id` (string, required) The issue. - `prompt` (string, required) The fix as instructions for a coding agent (Cursor, Claude Code…), to apply in the codebase. Rustine never edits code. - `ai_fix` (string | null, required) A patch written by Rustine (unified diff), when one was generated in the app. ## Examples ### curl ```bash # The key stays in a variable, never in the address 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() ``` ### Response: 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 } ``` ## Errors | Status | error.code | What happens | | --- | --- | --- | | 401 | `authentication_required` | No key in the request. Pass it in the `Authorization: Bearer rst_…` header, never in the address. | | 401 | `invalid_api_key` | The key or OAuth token is wrong, revoked or expired. Check it is copied in full, or create a new key. | | 402 | `api_plan_required` | The workspace is on the Free plan. The API comes with Pro, or on a 14-day trial with Solo. | | 402 | `api_trial_ended` | Solo's 14-day trial is over. Move to Pro: your keys work again at once. | | 403 | `scope_missing` | The OAuth token lacks the permission this route needs (shown on its page). Reconnect the agent to grant it. An API key has every permission. | | 404 | `resource_not_found` | Nothing with this id in the key's workspace. An object of another workspace answers the same: its existence is never revealed. | | 404 | `fix_locked` | The fix of this issue is not unlocked yet: `POST /issues/{issue_id}/fix` unlocks it. | | 429 | `rate_limited` | Too many requests for this key (120 per minute). Wait `Retry-After` seconds. | | 500 | `internal_error` | An error on our side, reported automatically. Retry later; give the `Request-Id` to support if it comes back. | | 503 | `service_unavailable` | Rustine is unavailable for a moment. Retry with the same `Idempotency-Key`. | https://docs.rustine.io/en/api/errors --- # Recheck an issue Runs the same check at the same place on your live site, right after your deploy. For a repository, Rustine starts a new analysis of the default branch (`pending`, with its `scan_id`). ``` POST https://api.rustine.io/v1/issues/{issue_id}/recheck ``` OAuth permission: `scans:run` · an API key has them all > Free, up to 60 rechecks per project and per day. Deploy (or push) the fix first: Rustine looks at what is live. ## Path parameters - `issue_id` (string, required) Issue id. ## Headers - `Idempotency-Key` (string) A unique key (1 to 255 printable characters, a UUID is fine) to retry this request safely: the first response is replayed for 24 hours; the same key with another request answers 409 `idempotency_conflict`. ## Response (200 OK) - `object` (string, required) - `issue_id` (string, required) The issue. - `status` (string, required) `fixed` (closed), `still_open`, or `pending` for a repository issue: a new analysis of the default branch was started (`scan_id`); poll it, then read the issue. (`fixed`, `still_open`, `pending`) - `checked_at` (string, required) When the check ran. - `evidence` (string, required) What the check saw this time. - `scan_id` (string | null, required) Repository issue: the analysis that settles it. ## Examples ### curl ```bash # The key stays in a variable, never in the address 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() ``` ### Response: 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 } ``` ## Errors | Status | error.code | What happens | | --- | --- | --- | | 400 | `idempotency_key_invalid` | The `Idempotency-Key` header must be 1 to 255 printable characters. | | 401 | `authentication_required` | No key in the request. Pass it in the `Authorization: Bearer rst_…` header, never in the address. | | 401 | `invalid_api_key` | The key or OAuth token is wrong, revoked or expired. Check it is copied in full, or create a new key. | | 402 | `api_plan_required` | The workspace is on the Free plan. The API comes with Pro, or on a 14-day trial with Solo. | | 402 | `api_trial_ended` | Solo's 14-day trial is over. Move to Pro: your keys work again at once. | | 402 | `project_paused` | The project is paused: your plan covers fewer projects than your workspace has. Its issues stay readable, but nothing new runs. | | 403 | `scope_missing` | The OAuth token lacks the permission this route needs (shown on its page). Reconnect the agent to grant it. An API key has every permission. | | 404 | `resource_not_found` | Nothing with this id in the key's workspace. An object of another workspace answers the same: its existence is never revealed. | | 409 | `proof_required` | The site must first be proved yours (full audit, fix, marking). Prove it in the app: DNS, file or meta tag. | | 409 | `idempotency_conflict` | This `Idempotency-Key` was already used for another request (method, path or body). A new action takes a new key. | | 409 | `idempotency_in_progress` | A request with this `Idempotency-Key` is still running. Retry in a moment. | | 429 | `rate_limited` | Too many requests for this key (120 per minute). Wait `Retry-After` seconds. | | 429 | `recheck_limit_reached` | Too many rechecks on this project today (60 per day). Try again tomorrow. | | 429 | `repository_analyses_limit_reached` | The workspace started many repository analyses in the last 24 hours. Try again later. | | 500 | `internal_error` | An error on our side, reported automatically. Retry later; give the `Request-Id` to support if it comes back. | | 502 | `recheck_failed` | The recheck could not complete (site unreachable?). Check the site answers, then retry. | | 503 | `service_unavailable` | Rustine is unavailable for a moment. Retry with the same `Idempotency-Key`. | https://docs.rustine.io/en/api/errors --- # Mark an issue Marks an issue as a false positive or an accepted risk, or opens it again. The reason shows in the app with your key's name. ``` PATCH https://api.rustine.io/v1/issues/{issue_id} ``` OAuth permission: `issues:write` · an API key has them all > A false positive leaves the score; an accepted risk stays in it. For a fixed issue, do not mark it: [recheck it](https://docs.rustine.io/en/api/issues/recheck) and Rustine closes it. ## Path parameters - `issue_id` (string, required) Issue id. ## Request body - `status` (string, required) `false_positive` (leaves the score), `accepted_risk` (stays in the score) or `open` to undo. (`open`, `false_positive`, `accepted_risk`) - `reason` (string) Why, with the evidence checked (10 to 500 characters). Required for `false_positive` and `accepted_risk`; shown in the app with the key's name. ## Response (200 OK) - `id` (string, required) Issue id; it keeps its identity from one scan to the next. - `object` (string, required) - `project_id` (string, required) Project of the issue. - `target_id` (string, required) Target where it was found. - `address` (string, required) Hostname of the site or `owner/repo`. - `control_id` (string, required) The check that found it (stable identifier). - `family` (string, required) Family of the check. (`database`, `secrets`, `exposedFiles`, `personalData`, `vulnerableVersions`, `configuration`, `compliance`, `server`, `code`) - `severity` (string, required) (`critical`, `high`, `medium`, `low`, `info`) - `confidence` (string, required) How sure the finding is. (`confirmed`, `likely`, `estimated`) - `status` (string, required) `open`; `fixed` (a later scan or recheck no longer finds it); `false_positive` (left out of the score); `accepted_risk` (kept in the score). (`open`, `fixed`, `false_positive`, `accepted_risk`) - `title` (string, required) One line, in the language of the key's owner. - `explanation` (string, required) The risk in plain words, in the key owner's language. - `detail` (string, required) Technical detail, in the key owner's language. - `location` (string, required) Where: a path, a table, or `file:line@commit` in a repository. - `file` (string | null, required) Repository issue: the file. - `line` (integer | null, required) Repository issue: the line. - `commit` (string | null, required) Repository issue: the commit analysed. - `evidence` (string, required) What the check observed, secrets masked. - `fix_unlocked` (boolean, required) Whether its fix is unlocked (readable for free). - `status_reason` (string | null, required) Why it was marked a false positive or an accepted risk. - `marked_by` (string | null, required) Who marked it: an agent, an API key's name, or `app`. - `first_seen_at` (string, required) First scan that found it. - `last_seen_at` (string, required) Last scan that found it. - `fixed_at` (string | null, required) When it was found fixed. ## Examples ### curl ```bash # The key stays in a variable, never in the address 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() ``` ### Response: 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 } ``` ## Errors | Status | error.code | What happens | | --- | --- | --- | | 400 | `body_invalid` | The request body is not valid JSON. Check the `Content-Type: application/json` header. | | 400 | `parameter_invalid` | A parameter is missing or invalid; `param` names it. | | 400 | `reason_required` | Marking a false positive or an accepted risk needs a reason, 10 to 500 characters. | | 401 | `authentication_required` | No key in the request. Pass it in the `Authorization: Bearer rst_…` header, never in the address. | | 401 | `invalid_api_key` | The key or OAuth token is wrong, revoked or expired. Check it is copied in full, or create a new key. | | 402 | `api_plan_required` | The workspace is on the Free plan. The API comes with Pro, or on a 14-day trial with Solo. | | 402 | `api_trial_ended` | Solo's 14-day trial is over. Move to Pro: your keys work again at once. | | 403 | `scope_missing` | The OAuth token lacks the permission this route needs (shown on its page). Reconnect the agent to grant it. An API key has every permission. | | 404 | `resource_not_found` | Nothing with this id in the key's workspace. An object of another workspace answers the same: its existence is never revealed. | | 409 | `issue_already_fixed` | The issue is fixed: there is nothing to mark. | | 409 | `proof_required` | The site must first be proved yours (full audit, fix, marking). Prove it in the app: DNS, file or meta tag. | | 429 | `rate_limited` | Too many requests for this key (120 per minute). Wait `Retry-After` seconds. | | 500 | `internal_error` | An error on our side, reported automatically. Retry later; give the `Request-Id` to support if it comes back. | | 503 | `service_unavailable` | Rustine is unavailable for a moment. Retry with the same `Idempotency-Key`. | https://docs.rustine.io/en/api/errors --- # Your plan and quotas Your plan, your API access, and what you have left this period in quick scans, audits and fixes. Call it before a loop of scans so you do not hit a `402`. ``` GET https://api.rustine.io/v1/account ``` OAuth permission: `scans:read` · an API key has them all ## Response (200 OK) - `object` (string, required) - `workspace` (object, required) - `id` (string, required) Workspace id. - `name` (string, required) Workspace name. - `plan` (string, required) The plan in force (Free, Solo or Pro). (`free`, `solo`, `pro`) - `api_access` (object, required) - `allowed` (boolean, required) Whether the API and the MCP server answer now. - `trial_ends_at` (string | null, required) Solo only: end of the 14-day trial once started; null on Pro, or before the first call. - `period` (object, required) - `start` (string, required) Start of the current 30-day quota period. - `end` (string, required) When the counters reset. - `quotas` (object, required) - `quick_scans` (object, required) - `limit` (integer, required) Units included this period (all the projects the workspace pays for). - `used` (integer, required) Units used this period. - `remaining` (integer, required) Units left this period, before pack credits. - `audits` (object, required) - `limit` (integer, required) Units included this period (all the projects the workspace pays for). - `used` (integer, required) Units used this period. - `remaining` (integer, required) Units left this period, before pack credits. - `fixes` (object, required) - `limit` (integer, required) Units included this period (all the projects the workspace pays for). - `used` (integer, required) Units used this period. - `remaining` (integer, required) Units left this period, before pack credits. - `free_fixes_left` (integer, required) Fixes given once to a Free workspace, still unused. - `pack_credits` (object, required) - `scans` (integer, required) Scan credits from packs, used once the period's quota is spent. - `fixes` (integer, required) Fix credits from packs, used once the period's quota is spent. - `projects` (object, required) - `limit` (integer, required) Open projects allowed by the plan (included and extra). - `used` (integer, required) Projects in the workspace, paused ones included. - `paused` (integer, required) Projects paused because the plan covers fewer projects. - `addresses_per_project` (integer, required) Addresses (hostnames) a project may follow on this plan. - `features` (object, required) - `full_audit` (boolean, required) Full audits (Solo, Pro). - `repositories` (boolean, required) GitHub repository analyses (Solo, Pro). - `ai_fix` (boolean, required) AI Fix, Rustine writes the patch (Solo, Pro). - `alerts` (boolean, required) Score-drop and new-advisory alerts (Pro). ## Examples ### curl ```bash # The key stays in a variable, never in the address 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() ``` ### Response: 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 } } ``` ## Errors | Status | error.code | What happens | | --- | --- | --- | | 401 | `authentication_required` | No key in the request. Pass it in the `Authorization: Bearer rst_…` header, never in the address. | | 401 | `invalid_api_key` | The key or OAuth token is wrong, revoked or expired. Check it is copied in full, or create a new key. | | 402 | `api_plan_required` | The workspace is on the Free plan. The API comes with Pro, or on a 14-day trial with Solo. | | 402 | `api_trial_ended` | Solo's 14-day trial is over. Move to Pro: your keys work again at once. | | 403 | `scope_missing` | The OAuth token lacks the permission this route needs (shown on its page). Reconnect the agent to grant it. An API key has every permission. | | 429 | `rate_limited` | Too many requests for this key (120 per minute). Wait `Retry-After` seconds. | | 500 | `internal_error` | An error on our side, reported automatically. Retry later; give the `Request-Id` to support if it comes back. | | 503 | `service_unavailable` | Rustine is unavailable for a moment. Retry with the same `Idempotency-Key`. | https://docs.rustine.io/en/api/errors --- # Connect your agent > Claude, ChatGPT, Claude Code, Cursor or VS Code connect to https://mcp.rustine.io, with OAuth or an API key. Rustine's MCP server lets your agent list your projects, start a scan, read the issues, get a fix and ask for a recheck. Rustine never edits your code: your agent applies the fix, on your machine. **Server address: `https://mcp.rustine.io`** Before you start: a plan that opens MCP (Pro, or the 14-day trial on Solo, see [Access](https://docs.rustine.io/en/access.md)) and two-factor authentication turned on in Rustine. ## With OAuth (recommended) No key to copy. On the first call, your agent opens Rustine's authorization page: you see the account and plan it will act on, you click **Allow**, and it renews its access by itself. | Agent | Where to add it | Step by step | | --- | --- | --- | | Claude (claude.ai, app) | Customize › Connectors › Add custom connector, name `Rustine`, URL `https://mcp.rustine.io` | [Help center (French)](https://rustine.io/aide/claude-app) | | ChatGPT | Plugins › + › custom MCP server, OAuth authentication | [Help center (French)](https://rustine.io/aide/chatgpt) | | Claude Code | `claude mcp add --transport http rustine https://mcp.rustine.io`, then `/mcp` | [Help center (French)](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" } } }` | | In the app, **Settings › Connected agents** copies the address and opens the right page for Claude and ChatGPT, and lists every connected agent with its last use. ## With an API key For an agent that cannot do OAuth. The key goes in the `Authorization` header, never in the address. | 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}" }`, with an `inputs` entry of type `promptString` and `password: true` | OAuth or key? The [help center](https://rustine.io/aide/oauth-ou-cle) compares both (in French). To call the server from a script, without an agent: [From a script](https://docs.rustine.io/en/mcp/scripts.md). ## What to ask 1. "List my Rustine projects and their scores." 2. "Scan mysaas.com with Rustine." 3. "What are the security issues of mysaas.com, worst first?" 4. "Get the fix for the first one and apply it." 5. "Recheck this issue with Rustine" (after your deploy). The tools it calls, and what each one costs: [The tools](https://docs.rustine.io/en/mcp/tools.md). ## Examples ### Claude Code ```bash claude mcp add --transport http rustine https://mcp.rustine.io # then /mcp in Claude Code to allow it ``` ### Cursor ```json { "mcpServers": { "rustine": { "url": "https://mcp.rustine.io" } } } ``` ### VS Code ```json { "servers": { "rustine": { "type": "http", "url": "https://mcp.rustine.io" } } } ``` --- # The tools > The six tools of the MCP server, their parameters and what they cost. Reading is free, unlocking a fix uses one fix. Every tool description reminds the agent of the rules: Rustine only scans the sites of the user's projects, read-only, never a site that is not theirs, and never edits code. ## list_projects Your projects and their sites: verified or not, score out of 100, grade A to F, last scan, open issues. **Free.** Permission `scans:read`. No parameters. ## run_scan Scans a site **already in your projects** and waits for the result (up to about a minute): score, grade and issues by severity. If the scan is still running, the agent calls the tool again with the same target to keep waiting. **One scan from your quota**; a scan of the same kind within the hour returns the recent result without charging anything. Permission `scans:run`. | Parameter | Type | | Description | | --- | --- | --- | --- | | `target` | string | required | site id, project id (if it has a single site) or URL of a site in your projects | | `kind` | `quick` \| `audit` | default `quick` | `audit` only on a proved site | ## list_issues The issues, worst first, with their confidence; for a repository, the file, line and commit. Issues of an unproved site stay hidden (only their count shows). **Free.** Permission `scans:read`. | Parameter | Type | | Description | | --- | --- | --- | --- | | `project` | string | optional | project id | | `target` | string | optional | site id or URL | | `status` | `open` \| `all` | default `open` | `all` adds fixed issues, false positives and accepted risks, with the reason and the agent that marked them | ## get_fix Returns the fix of an issue: instructions written for a coding agent. **One fix from your plan on the first call** for that issue, free afterwards. The agent is asked to check with you before unlocking many. Permission `fixes:unlock`. | Parameter | Type | | Description | | --- | --- | --- | --- | | `issue_id` | string (UUID) | required | id from `list_issues` | ## recheck_issue Rechecks an issue: on the live site, or for a repository by starting an analysis of the default branch (at most one per quarter of an hour). Answers `fixed`, `stillOpen` with the evidence, or `pending` (analysis not finished: `run_scan` on the repository a little later). **Free**, up to 60 per project and per day. Permission `scans:run`. | Parameter | Type | | Description | | --- | --- | --- | --- | | `issue_id` | string (UUID) | required | id from `list_issues` | ## set_issue_status Marks an issue as a false positive (it leaves the score) or an accepted risk (it stays in it), or opens it again. The reason shows in the app with the agent's name. The agent must only mark a false positive after checking the value is fake or unreachable, and use `recheck_issue` for a fixed issue. **Free.** Permission `issues:write`. | Parameter | Type | | Description | | --- | --- | --- | --- | | `issue` | string (UUID) | required | id from `list_issues` | | `status` | `falsePositive` \| `acceptedRisk` \| `open` | required | `open` undoes a mark | | `reason` | string | required | 10 to 500 characters: what was checked (file, line, why the value is fake) | An OAuth connection allowed before 13/10/2026 lacks `issues:write`: `set_issue_status` asks it to disconnect and reconnect Rustine. ## When the plan does not allow it A used-up quota or a plan without MCP never gives a `403`: the tool answers with a readable error (`isError: true`), in the user's language, with the link to change plans. ## Examples ### tools/call ```json { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "run_scan", "arguments": { "target": "https://mysaas.com", "kind": "quick" } } } ``` --- # From a script > Call the MCP server without an agent, with curl and an API key. Stateless, over POST, as SSE or plain JSON. For a script, the [REST API](https://docs.rustine.io/en/api/projects/list.md) is simpler. But you can also talk to the MCP server directly, for example to test your key or reproduce what your agent sees. ## The handshake The server is **stateless**: no `initialize`, no session header. Every request is a `POST` to `https://mcp.rustine.io` with: - `Authorization: Bearer rst_...`; - `Content-Type: application/json`; - `Accept: application/json, text/event-stream` (otherwise `406`). Without an `MCP-Protocol-Version` header, the answer comes as SSE: an `event: message` line, then `data: {…}` with the JSON-RPC answer. ## As plain JSON With the `2026-07-28` revision of the protocol, the answer is plain JSON. Add the `MCP-Protocol-Version`, `Mcp-Method` and `Mcp-Name` headers, and the `_meta` keys in `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":"my-script","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}' ``` ## Answers to know - **401** `invalid_token` with a `WWW-Authenticate` header: missing, revoked or mistyped key. - **406**: `Accept` lacks `text/event-stream` or `application/json`. - **Quota used up or plan without MCP**: never a `403`. The answer is a tool result with `isError: true` and readable text, with the link to change plans. - **Rate**: 120 requests per minute per key, like the API. Each tool's parameters: [The tools](https://docs.rustine.io/en/mcp/tools.md). ## Examples ### 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":{}}}' ``` ### Response: 200 OK ```text event: message data: {"result":{"content":[{"type":"text","text":"…"}]},"jsonrpc":"2.0","id":1} ``` --- # Changelog > What changes in the API, the MCP server and these docs. Additions arrive without breaking v1. ## October 13, 2026: API v1 and these docs - The REST API v1 at `https://api.rustine.io/v1`: projects, sites, scans, issues, fixes, rechecks and your plan. `rst_` keys in the `Authorization` header. - The docs at `docs.rustine.io`, in French and English, with their Markdown version for agents (`/llms.txt`, `/en/llms-full.txt`, a `.md` page for every page) and the OpenAPI specification [`/openapi.json`](https://docs.rustine.io/openapi.json). - The MCP server also accepts direct calls from a script, with a key: [From a script](https://docs.rustine.io/en/mcp/scripts.md).