# FYEO API (v1)

Run FYEO scans from scripts and CI. API scans use your account's credits (trial credits
included), show up in the portal like any other scan, and send the normal emails.

## Authentication

Create a key in **Account → API access** (account owners only; team members ask their account
owner). The key is shown once. Send it on every request:

```
Authorization: Bearer fyeo_user_…
```

Base URL: `https://xlkajfrijcxyllklojkd.supabase.co/functions/v1/user-api`

Requests and responses are JSON. Errors look like `{ "error": "message", "code": "machine_code" }`.

## Endpoints

| Method | Path | What it does |
|---|---|---|
| GET | `/v1/usage` | Credits and plan: `available`, `reserved`, `expiring_30d`, `trial_ends_at`, `plan_id`, `is_paid` |
| GET | `/v1/repos` | Your active repositories: `id`, `full_name`, `default_branch`, `visibility`, `source_kind`, `pr_scanning` |
| POST | `/v1/repos/pr-scanning` | Turn automatic PR audits on or off for a GitHub App repository: `{ "repo": "owner/name", "enabled": true }` |
| POST | `/v1/credit-codes/redeem` | Redeem a credit code for your account: `{ "code": "…" }` → `credits_added`, `expires_at` |
| POST | `/v1/scans/estimate` | Price, balance and a `quote_id` for a scan (nothing is queued) |
| POST | `/v1/scans` | Queue a scan (scan fields, or `{ "quote_id": … }`); credits are reserved immediately. Returns `202` with the scan |
| GET | `/v1/scans/{id}` (or `/status`) | Scan status |
| GET | `/v1/scans/{id}/findings` | Findings (rejected findings are left out) |
| GET | `/v1/scans/{id}/report` | Executive summary, threat model, severity counts (`404 report_not_ready` until done) |
| POST | `/v1/scans/{id}/cancel` | Cancel a scan that is still `queued`; the credits are refunded in full |
| GET | `/v1/scans/{id}/watch?token=…` | Status only, **no key needed**: the `status_url` returned when the scan was queued |

### Estimate and submit

Body (both endpoints; unknown fields are rejected):

| Field | Required | Notes |
|---|---|---|
| `repo` | yes | `owner/name`: a public GitHub repository, or one of yours with the FYEO GitHub App installed |
| `scan_type` | yes | `full_audit` or `pr_audit` |
| `ref` | no | Branch, tag or commit to scan; defaults to the default branch |
| `base_ref` | `pr_audit` | The branch the changes are compared against; must differ from `ref` |
| `max_credits` | no (submit) | Refuse the scan if it would cost more (`422 price_above_limit`); nothing is reserved |

```bash
curl -X POST "$BASE/v1/scans/estimate" -H "Authorization: Bearer $FYEO_API_KEY" \
  -H "Content-Type: application/json" -d '{"repo": "octocat/Hello-World", "scan_type": "full_audit"}'
```

```json
{ "repo": "octocat/Hello-World", "scan_type": "full_audit", "ref": "master", "estimated_credits": 35,
  "size_band": "<= 5 KLOC", "loc_estimate": 120, "balance": 50, "sufficient_balance": true,
  "credits_needed": 0, "trial_ends_at": "2026-11-15T10:00:00Z" }
```

```bash
curl -X POST "$BASE/v1/scans" -H "Authorization: Bearer $FYEO_API_KEY" -H "Idempotency-Key: ci-run-4711" \
  -H "Content-Type: application/json" -d '{"repo": "octocat/Hello-World", "scan_type": "pr_audit", "ref": "feature/x", "base_ref": "master"}'
```

Scan view (submit, status and cancel):

```json
{ "id": "…", "scan_type": "pr_audit", "repo": "octocat/Hello-World", "ref": "feature/x", "base_ref": "master",
  "status": "queued", "phase": null, "error": null, "commit_sha": null, "credits_reserved": 3,
  "credits_charged": null, "created_at": "…", "started_at": null, "finished_at": null,
  "portal_url": "https://scanner.fyeo.io/scans/…" }
```

The submit response also carries `status_url`, `wait_command`, `email_on_completion` and `next_steps`
(see **Waiting for results**).

`status` is `queued`, `running`, `completed`, `failed` or `cancelled`. A scan reports `running` as soon as
the worker picks it up, which is usually within seconds.

- **Public repositories**: the repository and refs are checked with GitHub, and the repository is
  added to your account if it is not there yet.
- **GitHub App repositories**: refs are only checked for format. A ref that does not exist makes
  the scan fail, and failed scans are refunded.
- Private repositories outside the FYEO GitHub App are not supported yet.

### Quotes

Every estimate also returns `quote_id` and `quote_expires_at` (valid 30 minutes). Submitting exactly
`{ "quote_id": "…" }` queues that scan and caps it at the quoted price. If the price went up since the
quote (for example the repository grew), nothing is reserved and you get `409 price_changed` with
`quoted_credits`, `current_credits` and a fresh `quote_id` to confirm. A lower price simply applies.
One quote queues at most one scan: submitting it again returns the same scan. An unknown or expired
quote gives `422 quote_expired`.

### Waiting for results

PR scans of small diffs often finish within minutes; full audits take about 35 minutes for smaller
repositories and over an hour for large ones. There are no webhooks yet. Instead:

- **Email**: FYEO emails the account when a scan finishes (unless scan emails or the key's "email me"
  switch are off; `email_on_completion` says which). The email links to the results.
- **`status_url`**: `GET` it without any key; it returns `{ "status", "done", "finished_at" }` for this
  scan only, at most 6 times a minute.
- **`wait_command`**: a ready-made shell loop that checks `status_url` every 5 minutes and exits when
  `done` is true (gives up after 12 hours). Run it in the background, then fetch the report.

Don't poll the status in a tight loop. For AI agents, `next_steps` puts the same as two options to offer the user: wait now, or come back after the email.

### Reading results

Start with `/report` (summary and severity counts), then `/findings`, most severe first. Rejected
findings are already left out; triage happens in the portal (`portal_url`).

### Idempotency

Send `Idempotency-Key` (1-255 printable characters) on `POST /v1/scans`. Retrying with the same key
and body returns the same scan and never reserves credits twice. The same key with a different
body is rejected with `422 idempotency_mismatch`.

### Cancel

Only a scan that is still `queued` can be cancelled, and it is refunded in full. The worker usually
picks scans up within seconds, so cancelling mostly helps when scans are waiting in line. Once a scan has
been picked up it cannot be stopped yet: `409 not_cancellable`.

### PR scanning

While PR scanning is on for a repository, every pull request into its default branch (except from forks)
is audited automatically and billed like a `pr_audit`. Only repositories connected through the FYEO GitHub
App can have it (`422 pr_scanning_unavailable` otherwise). It is on by default for newly connected repositories.

### Credit codes

Each code can be redeemed once per account. Redeeming needs a contact email: an account without one (for example
only a GitHub no-reply address) gets `409 email_required`. The user adds one on the
[Account page](https://scanner.fyeo.io/account) (Notifications → Contact email) and tries again.

## Limits

- 3 active (queued or running) scans per account: `429 too_many_active_scans`
- 60 requests per minute per key (or per connected AI app): `429 rate_limited` with `Retry-After` (seconds)
- 5 active keys per account
- 100 KB request body: `413 body_too_large`

## AI apps (MCP)

Claude, ChatGPT and other MCP clients connect to the MCP server shown in **Account → API access**
(OAuth sign-in, no key needed). They can always read, turn PR scanning off and redeem codes you give
them. Queueing and cancelling scans and turning PR scanning on also require **Let connected AI apps queue
and cancel scans and turn on PR scanning** in the same place (off by default); otherwise those calls fail
with `403 ai_apps_queue_disabled`. AI apps queue scans from a quote, so a
scan never costs more than the price you were shown. Revoke an app under **Connected apps**.

MCP tools: `list_repos`, `list_scans`, `get_scan`, `list_findings`, `get_finding`, `get_report`, `get_usage`
and `estimate_scan` (read), plus `queue_scan` (from a `quote_id`), `cancel_scan` (queued scans only),
`set_pr_scanning` and `redeem_credit_code`, which your AI app asks you to confirm.

These apps call this API with their OAuth access token instead of a key. Plain portal session tokens
are rejected (`401 oauth_token_required`).

## Notifications

You get the normal scan-completed email, with a link to the results, unless the key was created with
"Email me when a scan completes" switched off. Failure emails are always sent.

## Error codes

| Status | `code` | Meaning |
|---|---|---|
| 401 | `missing_api_key`, `invalid_api_key` | No key, unknown key or revoked key |
| 401 | `invalid_token`, `oauth_token_required` | Expired OAuth token, or a token not issued to an AI app |
| 403 | `terms_not_accepted` | Accept the FYEO terms in the portal first |
| 403 | `not_account_owner` | The caller now belongs to another account's team |
| 403 | `ai_apps_queue_disabled` | An AI app tried to queue, cancel or turn PR scanning on while that is switched off |
| 402 | `trial_ended` | The free trial has ended and the account has no active plan |
| 402 | `insufficient_credits` | Not enough credits for the scan; includes `balance` and `needed` |
| 404 | `repo_not_available` | Not public and not covered by your FYEO GitHub App installation |
| 404 | `scan_not_found`, `report_not_ready`, `not_found` | Unknown scan (or not yours), report not ready, unknown route |
| 409 | `not_cancellable` | The scan is no longer queued |
| 409 | `code_already_redeemed`, `code_exhausted`, `email_required` | Code already used by this account, fully used, or the account needs a contact email first (Account page) |
| 409 | `price_changed` | The price rose since the quote; includes a fresh `quote_id` |
| 413 | `body_too_large` | Request body over 100 KB |
| 422 | `invalid_request`, `invalid_json`, `invalid_idempotency_key` | Bad or unknown fields (see `details`) |
| 422 | `scan_type_not_available` | `third_party_audit` is not offered through the API |
| 422 | `ref_not_found`, `repo_inactive`, `idempotency_mismatch` | Ref missing on GitHub, repository deactivated, reused key |
| 422 | `price_above_limit`, `quote_expired` | Scan costs more than `max_credits`; unknown or expired quote |
| 422 | `pr_scanning_unavailable` | PR scanning needs a repository connected through the FYEO GitHub App |
| 422 | `code_invalid`, `code_expired`, `code_inactive` | Unknown, expired or deactivated credit code |
| 429 | `too_many_active_scans`, `rate_limited`, `too_many_attempts` | See Limits; more than 10 code attempts in an hour |
| 502 | `github_unavailable` | GitHub could not be reached; retry later |

## Privacy

FYEO never stores your source code after a scan, and never uses it to train models.
