# REST API

> Every /api/v1 route with its method, path, authentication, request, response and errors.

Source: https://retoque.xyz/docs/api
Markdown: https://retoque.xyz/docs/md/api

Version 1 of the registry API. The CLI uses these same routes. Request and response bodies are JSON unless a route says otherwise.

## Conventions

- **Registry keys** go in the `Authorization: Bearer rtq_...` header. A key without the scope a route needs, or a revoked key, is treated as missing and returns `401`.
- **Browser routes** use the session cookie set at sign-in. They are listed separately and are not meant for scripts.
- **Errors** always have the shape `{ "error": "code", "message": "text" }`. Codes are listed in [Errors and troubleshooting](https://retoque.xyz/docs/errors#api-error-codes).
- **Validation errors** return `400 invalid_request` with a message naming each bad field as `path: problem`.
- **Visibility.** Private, draft and unpublished patches return `404` on public routes, the same as names that don't exist.
- **Base URL.** Every route is relative to the site, for example `https://<registry host>/api/v1/patches`.

```sh
curl -s "$RETOQUE_API_URL/api/v1/patches?baseModel=qwen2.5-1.5b-instruct&limit=10"
curl -s -X POST -H "Authorization: Bearer $RETOQUE_KEY" \
  "$RETOQUE_API_URL/api/v1/patches/<slug>/versions/<version>/install-tickets"
```

## Public routes

No authentication.

### GET /api/v1/patches

Search published, public patches. Filters, sort and pagination run on the server.

| Query parameter | Meaning |
| --- | --- |
| `query` | Case-insensitive match on title and problem text. Up to 200 characters. |
| `baseModel` | Model slug, such as `qwen2.5-1.5b-instruct`. Not an `org/name@commit` reference. |
| `category` | Problem category, such as `structured-output`. |
| `verified` | `true` returns only patches with a published `VERIFIED_FOR_SCOPE` version. |
| `license` | `free` or `paid`. |
| `sort` | `newest` (default), `oldest` or `title`. |
| `page` | 1 to 10000. Default 1. |
| `limit` | 10, 25 or 50. Any other value falls back to 10. |

```json
{
  "total": 1,
  "page": 1,
  "pageSize": 10,
  "pageCount": 1,
  "items": [
    {
      "slug": "string",
      "title": "string",
      "category": "string",
      "publisher": "string",
      "baseModel": "org/name@<commit>",
      "latestVersion": "1.0.0",
      "evidenceStatus": "IMPROVED_ON_SCOPE"
    }
  ]
}
```

`latestVersion` and `evidenceStatus` are `null` when no version is in the `PUBLISHED` state. Errors: `400 invalid_query`.

### GET /api/v1/patches/{slug}

A public patch with every published, deprecated or withdrawn version.

```json
{
  "slug": "string",
  "title": "string",
  "status": "PUBLISHED",
  "baseModel": { "repository": "string", "revision": "string" },
  "versions": [
    {
      "semver": "1.0.0",
      "state": "PUBLISHED",
      "evidenceStatus": "IMPROVED_ON_SCOPE",
      "manifestSha256": "<64 hex>",
      "artifactSha256": "<64 hex>",
      "packageSha256": "<64 hex>",
      "publishedAt": "<ISO 8601 or null>"
    }
  ]
}
```

`state` is `PUBLISHED`, `DEPRECATED` or `WITHDRAWN`. `artifactSha256` is the adapter weights digest. Errors: `404 not_found`.

### GET /api/v1/patches/{slug}/versions/{version}

The full signed manifest of one version, for verification before download.

```json
{
  "semver": "1.0.0",
  "state": "PUBLISHED",
  "manifest": { "...": "the parsed retoque.manifest.json" },
  "manifestSha256": "<64 hex>",
  "signature": { "...": "the contents of signature.json" },
  "packageSha256": "<64 hex>"
}
```

The manifest digest is the sha256 of the manifest serialized as canonical JSON (sorted keys, no spaces, UTF-8), which `retoque verify` recomputes. Errors: `404 not_found` for unknown, private or unpublished versions.

### GET /api/v1/models

Supported base model revisions.

```json
{
  "items": [
    { "slug": "string", "repository": "string", "revision": "string", "license": "string", "publishedPatches": 0 }
  ]
}
```

### GET /api/v1/status

Live probe results for each component, checked on every request. A component without a real probe reports `unknown`, never healthy.

```json
{
  "components": [
    {
      "id": "api",
      "name": "Website and API",
      "state": "operational",
      "detail": "Database reachable",
      "checkedAt": "<ISO 8601>"
    }
  ]
}
```

`id` is one of `api`, `artifacts`, `chain`, `workers`, `replay`. `state` is one of `operational`, `degraded`, `down`, `unknown`, `not_deployed`.

### GET /api/v1/chain/config

The chain this environment uses and its contract addresses.

```json
{
  "chainId": 31337,
  "name": "string",
  "rpcUrl": "string",
  "explorerUrl": null,
  "isTestnet": true,
  "registry": null,
  "market": null,
  "paymentToken": null,
  "confirmations": 2,
  "deployed": false
}
```

`deployed` is `true` only when both the registry and market addresses are configured. Addresses are `null` when not set.

## Registry key routes

### GET /api/v1/me

Check a registry key. Used by `retoque login`. Needs the `registry:read` scope.

```json
{ "email": "string", "org": "string", "scopes": ["registry:read", "artifacts:download"] }
```

Errors: `401 unauthenticated`.

### POST /api/v1/patches/{slug}/versions/{version}/install-tickets

Issue a single-use download link for a version you hold an active license for. Authenticate with a key that has the `artifacts:download` scope, or a signed-in session. The body is ignored. The CLI sends `{}`.

`201`:

```json
{
  "downloadUrl": "https://<host>/api/v1/downloads/<token>",
  "expiresAt": "<ISO 8601, five minutes from now>",
  "packageSha256": "<64 hex>",
  "manifestSha256": "<64 hex>"
}
```

Withdrawn versions still issue tickets to existing license holders. Errors: `401 unauthenticated`, `403 no_entitlement`, `404 not_found`.

### GET /api/v1/downloads/{token}

Stream the package tar. The token in the path is the credential, so don't send your registry key. The token is consumed on first use.

```text
200 OK
content-type: application/x-tar
content-length: <bytes>
content-disposition: attachment; filename="<slug>-<version>.tar"
cache-control: no-store
```

Errors: `410 download_expired` when the token was already used, is older than five minutes, or doesn't exist.

### POST /api/v1/install-events

Opt-in CLI check-in. Needs the `installs:report` scope. A download on its own is never counted as an install.

```json
{
  "slug": "string, up to 100 characters",
  "semver": "string, up to 40 characters",
  "artifactSha256": "<64 lowercase hex>",
  "baseModelRef": "org/name@<commit>",
  "runtime": "transformers-peft",
  "state": "INSTALLED",
  "clientVersion": "0.1.0"
}
```

`state` is one of `INSTALLED`, `ACTIVE`, `DISABLED`, `ROLLED_BACK`, `FAILED`. Returns `201` with `{ "recorded": true }`. Errors: `400 invalid_request`, `401 unauthenticated`, `404 not_found`, `422 digest_mismatch` when `artifactSha256` isn't the version's adapter digest.

## Browser session routes

These back the website and rely on the session cookie.

### POST /api/v1/auth/magic-link

Send a sign-in link by email. The body is form-encoded: `email=<address>&next=<optional path>`, with the email up to 254 characters and `next` up to 512. Always answers `303`, to `/sign-in?sent=1` (with `next` when given), or to `/sign-in?error=invalid_email`. Links expire after 15 minutes.

### GET /api/v1/auth/callback

`?token=<emailed token>`. Consumes a sign-in link, sets the session cookie and answers `303` to the requested page, or to `/sign-in?error=link_invalid` when the link is unknown, used or expired.

### POST /api/v1/auth/sign-out

Ends the session. Answers `303` to `/`.

### POST /api/v1/auth/wallet/challenge

Get a message to sign that binds this site, the chain ID, your address, a nonce and a 10-minute expiry.

```json
{ "address": "0x..." }
```

`200` with `{ "nonce": "string", "message": "string" }`. Errors: `400 invalid_request` when the address isn't an EVM address, `401 unauthenticated`.

### POST /api/v1/auth/wallet/verify

Submit the signed challenge to link the wallet. Signing costs no gas.

```json
{ "nonce": "string", "signature": "0x..." }
```

`200` with `{ "address": "0x...", "chainId": 31337 }`. Errors: `400 invalid_request`, `400 wallet_link_failed`, `401 unauthenticated`.

### POST /api/v1/chain/reconcile-purchase

Ask the server to check a purchase transaction. The hash is only a hint. Every fact is read from the chain, and the call is safe to repeat.

```json
{ "txHash": "0x<64 hex>" }
```

`200` with one of:

```json
{ "status": "not_found" }
{ "status": "confirming", "confirmations": 1, "required": 2 }
{ "status": "confirmed", "purchases": [
    { "id": "<uuid>", "entitled": true },
    { "id": "<uuid>", "entitled": false, "reason": "buyer_wallet_not_linked" }
] }
```

The simple statuses are `not_found`, `pending`, `failed`, `wrong_contract` and `no_license_event`. The meaning of each, and of every `reason`, is in [Purchase reconciliation results](https://retoque.xyz/docs/errors#purchase-reconciliation-results). Errors: `400 invalid_request`, `401 unauthenticated`.

### GET /api/v1/me/compare/{runId}

Raw JSON export of a recorded comparison: the patch and base model digests, the evaluation run with target and regression counts and the rollback result, the public case summary, how many private cases were withheld, and the visible cases with inputs, expected outputs, both outputs and per-field errors. Visible to the comparison's owner, or to any signed-in user once it is shared. Sent with `Cache-Control: private, no-store`. Errors: `401 unauthenticated`, `404 not_found`.
