# 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
