# REST API

> Read your Prefera data and run every MCP tool over plain REST with an API key. Endpoints, authentication, errors and limits.

Source: https://prefera.si/docs/api
Docs index for agents: https://prefera.si/llms.txt

The REST API gives scripts and automations the same data as the app. Reading uses simple `GET` endpoints. Everything an [MCP agent](/docs/agents) can do is also a `POST` to `/api/v1/tools/{name}`. The API is included with every paid plan.

## Authentication

Create an API key in the app:

1. Open **API & MCP**.
2. Under **API keys, for scripts and automations**, click **Create key**. Only an owner or editor can.
3. Name it after the tool that will use it. Optionally limit its areas under **Advanced: limit to areas**. Click **Create key**.
4. Copy the key. It is shown once. Prefera keeps only a one-way fingerprint, so a lost key cannot be shown again. Revoke it and make another.

Send the key on every request as a Bearer token:

```bash
export PREFERA_KEY=your_key_here
curl -s -H "Authorization: Bearer $PREFERA_KEY" https://prefera.si/api/v1/brands
```

- A key reaches one workspace and acts as the person who made it. If that person leaves the workspace, the key stops working.
- A workspace can have 10 active keys. Revoking takes effect on the next request.
- If the workspace is on the Free plan, requests answer `403 plan_required`.
- The tools endpoints also accept an OAuth access token from Sign in with Prefera.

## Base URL

```text
https://prefera.si/api/v1
```

Responses are JSON with `Cache-Control: no-store`. Field names are snake_case and timestamps are ISO 8601 UTC.

## Read endpoints

These accept `GET` only. Any other method answers `405 method_not_allowed`.

| Endpoint | Returns | Query |
|---|---|---|
| `/brands` | Brands in the workspace | none |
| `/questions` | Tracked questions with the latest outcome | `brand`, `status` (`active`, `suggested`, `archived`), `limit`, `cursor` |
| `/runs` | Runs, newest first | `brand`, `limit`, `cursor` |
| `/answers` | Every answer of a run | `brand`, `run` (`latest` or a run id), `question`, `engine`, `limit`, `cursor` |
| `/sources` | Domains the engines cite | `brand`, `run`, `limit`, `cursor` |
| `/competitors` | Tracked competitors and your own share | `brand` |
| `/fixes` | Fixes, newest first | `brand`, `status` (`draft`, `ready`, `published`, `rechecking`, `done`, `abandoned`, or `live`), `kind`, `group`, `search`, `limit`, `cursor` |
| `/proof` | Published fixes with the Proof verdict | `brand` |
| `/results` | Fixes with their verdict and the traffic they brought afterwards | `brand`, `limit` |
| `/facts` | Approved facts with source and verification state | `brand` |
| `/moves` | All moves, ranked | `brand`, `type`, `limit` |
| `/opportunities` | Search opportunities | `brand`, `state`, `limit` |
| `/rankings` | Your searches, tracked keywords or the keyword gap | `brand`, `view` (`searches`, `tracked`, `gap`), `limit`, `only_winnable` (`true`, gap only) |
| `/authority` | Domain Rating, links and the link gap | `brand`, `limit` |
| `/pitch-list` | Third-party pages to pitch, ranked | `brand`, `limit` |

`brand` is a brand id or a domain. You can leave it out when the workspace tracks one brand. With several brands the API answers `400 brand_required`.

List endpoints with paging return `{ "data": [...], "next_cursor": "..." | null }`. Pass `next_cursor` back as `cursor` to get the next page. Cursors are opaque.

```bash
export PREFERA=https://prefera.si

# Which brands does this key see?
curl -s -H "Authorization: Bearer $PREFERA_KEY" $PREFERA/api/v1/brands

# Questions, 100 at a time
curl -s -H "Authorization: Bearer $PREFERA_KEY" "$PREFERA/api/v1/questions?brand=example.com&limit=100"

# Answers of the latest run for one engine
curl -s -H "Authorization: Bearer $PREFERA_KEY" "$PREFERA/api/v1/answers?brand=example.com&run=latest&engine=perplexity"

# What the published fixes did
curl -s -H "Authorization: Bearer $PREFERA_KEY" "$PREFERA/api/v1/proof?brand=example.com"
```

## Every MCP tool over REST

| Endpoint | What it does |
|---|---|
| `GET /api/v1/tools` | The catalog: `{ "data": [{ name, title, description, input_schema, class, costly, annotations }] }`. `class` is `read`, `write` or `publish`. |
| `POST /api/v1/tools/{name}` | Run one tool. The JSON body is the tool's arguments. Send `{}` or an empty body for none. |

A call goes through the same code as MCP. The roles, areas, access levels, rate limits, costly-action limit and approvals are the same, and a call spends exactly what it would spend over MCP. Tool names and arguments are in the [MCP tool reference](/docs/agents/tools).

The body must be a JSON object, sent with `Content-Type: application/json`, up to 256 KB. Success answers `200` with `{ "data": { ... } }`. If the key has [Ask me in Prefera first](/docs/agents/permissions#ask-me-in-prefera-first) and the tool is a publish tool, the answer is `202` with `status: "pending_approval"` and an `approval_id`. Poll it with the `get_approval_status` tool.

```bash
# What can I call?
curl -s https://prefera.si/api/v1/tools -H "Authorization: Bearer $PREFERA_KEY"

# Read: the questions you are losing
curl -s -X POST https://prefera.si/api/v1/tools/losing_questions \
  -H "Authorization: Bearer $PREFERA_KEY" -H "Content-Type: application/json" \
  -d '{"limit": 5}'

# Write: rename a fix
curl -s -X POST https://prefera.si/api/v1/tools/update_fix_details \
  -H "Authorization: Bearer $PREFERA_KEY" -H "Content-Type: application/json" \
  -d '{"fix_number": 2, "title": "Pricing page rewrite"}'
```

## Errors

Errors always have this shape:

```json
{ "error": { "code": "not_found", "message": "No fix number 99 on this brand. List them with list_fixes." } }
```

On the tools endpoints the error object can carry extra fields, for example `allowed_areas`.

| Status | Codes |
|---|---|
| 400 | `bad_request`, `invalid`, `invalid_cursor`, `brand_required`, `brand_ambiguous` and other input errors |
| 401 | `invalid_api_key`, `invalid_token` |
| 403 | `plan_required`, `forbidden`, `publish_required`, `area_not_allowed` |
| 404 | `not_found`, `no_brand` |
| 405 | `method_not_allowed` |
| 409 | `state` |
| 413 | `payload_too_large` |
| 415 | `unsupported_media_type` |
| 429 | `rate_limited`, `limit` (with a `Retry-After` header) |
| 500 | `internal_error` |

What each code means and what to do is in [Permissions, approvals and limits](/docs/agents/permissions#error-codes).

## Limits

- 60 requests a minute per key. REST and MCP share the count.
- 10 changes a minute per key on write and publish tools.
- 20 costly tool calls an hour per key.
- 30 requests a minute from one address with a missing or invalid key.

Over a limit you get `429` and a `Retry-After` header in seconds.
