# Folio Agent API

Base path: /api/v1 on the Folio server serving this document. OpenAPI: /api/openapi.json. Human reference and key management: /docs/api.

Authenticate with Authorization: Bearer <FOLIO_API_KEY>. Create an owner-scoped key in the signed-in workspace; the provider's API key is never used by callers. A read key can retrieve saved results. The evaluate permission additionally allows explicit work requests and cancellation.

## MCP and SEO

Streamable HTTP endpoint: /api/mcp, using the same bearer key. Connect Codex with url and bearer_token_env_var=FOLIO_API_KEY; Claude Code supports an Authorization header from an environment variable. Tool discovery exposes only permitted scopes. read opens saved evidence; evaluate starts observations; seo authorizes fresh DataForSEO overview lookups. Existing evaluate keys do not gain seo. POST /seo also requires confirmSpend:true and Idempotency-Key. Twenty lookups per hour are shared across browser, REST and MCP. For keyword runs, useSeoTools:true authorizes one selected-domain sandbox lookup and requires evaluate+seo. Its short-lived capability exposes only that tool, and repeats reuse the same report. Enhanced runs have separate harness identities.

## Request a recent result

Default maxAgeSeconds is 86400 (24 hours); range 0–604800. First enumerate owned websites or keyword cases. GET /observations reports saved data and freshness only. POST /observations/ensure with an Idempotency-Key (8–128 letters, digits, dots, underscores, colons or hyphens) reuses a fresh matching result or matching active run, otherwise starts one new run. This POST can incur usage charges. Use the same key and identical body to recover a request; do not change keys to retry unknown creation. A frozen website replay retains its original capture age.

200 fresh_saved means the saved observation met the age/configuration requirements. 202 started/existing_active means follow run.pollUrl. GET reads local saved state; POST the corresponding /reconcile endpoint to retrieve existing provider work. No polling request creates new inference or submits a pending tool answer. Stop on completed, failed, or cancelled. requires_action needs inspection, not a replacement task. Account allowlists, active-run reservations, and quotas apply to API keys too. Keys are limited to 60 requests/minute; honor Retry-After when a rate_limited response supplies it. A reserved request denied as run_limit or owner_active_elsewhere retains that outcome for its idempotency key. After capacity is resolved, a separately authorized request needs a new key.

## Visibility reports

GET /visibility/current, /overview, /citations, /prompts and /recommendations require websiteId from /sites. Optional startDate and endDate are inclusive UTC YYYY-MM-DD dates; model filters an exact saved model. platform supports openai only. citations accepts sourceType=all|owned|external and limit=1–200 (default 50). All reads are private and start no work. The newest 100 matching attempts are considered; coverage.truncated reports additional history. Metrics use the latest completed answer per question in that window, preserving completed evidence when later attempts fail. visibilityScore is 100 × target-listed answers / answers with known target identity. Unknown identities are excluded and counted separately. shareOfVoice divides target domain appearances by all identified domain appearances, deduplicating each domain within each answer. averagePosition averages the first target position only when listed; absent measurements are null. Daily samples can differ in questions and models; do not interpret them as controlled trends. Recommendations are deterministic suggestions tied to run IDs, not promises of rank improvement.

## Endpoints

### GET /api/v1/seo

List saved DataForSEO reports; pass reportId to read an owned report. Never starts a lookup. Permission: read.

### POST /api/v1/seo

One paid domain overview with two provider endpoints. Requires confirmSpend and a stable Idempotency-Key. Replays return saved pending or complete state without retrying provider work. Permission: seo.

JSON body:

```json
{
  "domain": "example.com",
  "confirmSpend": true
}
```

### GET /api/v1/visibility/current

Read saved open-web observations for one owned website. Never starts a task. See date filters and metric definitions below. Permission: read.

### GET /api/v1/visibility/overview

Read saved open-web observations for one owned website. Never starts a task. See date filters and metric definitions below. Permission: read.

### GET /api/v1/visibility/citations

Read saved open-web observations for one owned website. Never starts a task. See date filters and metric definitions below. Permission: read.

### GET /api/v1/visibility/prompts

Read saved open-web observations for one owned website. Never starts a task. See date filters and metric definitions below. Permission: read.

### GET /api/v1/visibility/recommendations

Read saved open-web observations for one owned website. Never starts a task. See date filters and metric definitions below. Permission: read.

### GET /api/v1/auth/status

Confirm the key is valid and inspect its permissions. Permission: read.

### GET /api/v1/sites

List websites saved in the key owner’s workspace. Use the returned website ID for a page evaluation. Permission: read.

### GET /api/v1/benchmark-cases

List saved keyword cases. Each ID preserves its query, target, locale, and search configuration. Permission: read.

### GET /api/v1/observations

Read a matching result and its age. Missing or stale data does not start a task. Permission: read.

### POST /api/v1/observations/ensure

Reuse a sufficiently recent result or matching active run; otherwise reserve and start one new evaluation. This request can incur usage charges. Permission: evaluate.

JSON body:

```json
{
  "kind": "keyword",
  "caseId": "case_example",
  "maxAgeSeconds": 86400
}
```

### GET /api/v1/runs/{kind}/{id}

Retrieve one private run’s status, result, and usage. This reads Folio’s saved state without contacting the model. Permission: read.

### POST /api/v1/runs/{kind}/{id}/reconcile

Retrieve the existing remote task and save its returned progress. Creates no new task and submits no pending tool answers. A keyword deadline may request cancellation. Permission: evaluate.

JSON body:

```json
{}
```

### POST /api/v1/runs/{kind}/{id}/cancel

Request that existing work stop. Keep reading the saved run until its terminal outcome is confirmed; an acknowledgement alone is not completion. Permission: evaluate.

JSON body:

```json
{}
```

## Reading results

Website checks measure captured-page evidence, not search rank. Keyword results include text, mentions, citations, and limitations. Each mentions entry contains name, url, reason, and citationUrls; its one-based array index is its returned recommendation position. Preserve this order. Keyword recommendations record ordered Astra/OpenAI search answers and their citations. A recommendation position belongs to that saved answer; it is not a universal search-engine position or a measurement of the consumer ChatGPT website. Older reviewed-domain searches retain their restricted scope and are not comparable to open-web runs. Missing data and unknown cost stay null. Results are private to the key's owner. The v1 projection excludes private page bodies, reference answers, hidden reasoning, and provider secrets.

## Errors

Responses contain {error:{code,message}}. A 503 persistence_unconfirmed may also include recovery:{runId,sessionId}; preserve those IDs and inspect the existing attempt before any new request. 401 means authenticate again with a valid key; 403 means insufficient permission/account access; 404 means no owned record; 409 means conflicting or unresolved work; 429 means quota/rate limits. Do not retry uncertain creation with a new idempotency key.
