# 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
