# Frontmost AI API

Use this API to read how AI apps answer your buyers' questions about your business, and to change what Frontmost AI tracks.
It gives a coding agent the same numbers as the Frontmost AI app and the MCP server.

- Base URL: `https://app.frontmost.ai/api/v1`
- OpenAPI 3.1 file: https://app.frontmost.ai/api/v1/openapi.json
- This page as Markdown: https://app.frontmost.ai/docs/api.md

## Get a key

1. Open https://app.frontmost.ai/team and go to API Keys.
2. Name the key and choose what it can do. A Read Only key calls every GET route. A Read and Write key calls every route.
3. Copy the key when it appears. Frontmost AI shows it once.

Every plan includes API keys. Every member of a workspace can create keys, see every key, and revoke any key.
A revoked key stops working on the next request.

## Authenticate

Send the key in the `Authorization` header of every request:

```bash
curl https://app.frontmost.ai/api/v1/workspace \
  -H "Authorization: Bearer $FRONTMOST_API_KEY"
```

Keep the key in an environment variable such as `FRONTMOST_API_KEY`. Do not put it in code that you commit.

## First call

Call `GET /workspace` first. It returns the plan, its limits, and what is in use. Then call `GET /projects` to find project IDs.

```bash
curl https://app.frontmost.ai/api/v1/projects \
  -H "Authorization: Bearer $FRONTMOST_API_KEY"
```

## Routes

| Method and path | Key | What it does |
| --- | --- | --- |
| `GET /workspace` | Read Only | The plan, its limits, and use. `profiles.max_per_project` is the most buyer profiles one project holds. |
| `GET /projects` | Read Only | List projects. `q` filters by name or domain. |
| `GET /projects/{id}` | Read Only | One project |
| `GET /projects/{id}/scans` | Read Only | Delivered scans, newest first. `delivered_after` takes a date. |
| `GET /projects/{id}/metrics` | Read Only | Visibility, Win Rate, Position, and Sentiment for one scope. `group_by` is `app` or `scan`. |
| `GET /projects/{id}/rivals` | Read Only | Rivals that the answers named, most named first |
| `GET /projects/{id}/answers` | Read Only | Answers in scope, without their text. Filters: `question_id`, `rival_id`, `named`, `cited_domain`. |
| `GET /answers/{id}` | Read Only | One answer with its text and citations |
| `GET /projects/{id}/sources` | Read Only | Sites that the answers cited, most cited first. Each site has a `channel`, such as `editorial`, `reddit`, or `own_website`. `domain` filters to one site, and `channel` filters to one channel. |
| `GET /projects/{id}/questions` | Read Only | The project's questions. `active` filters. |
| `GET /projects/{id}/recommendations` | Read Only | Recommendations, open first. `status` is `open`, `done`, or `dismissed`. |
| `GET /projects/{id}/profiles` | Read Only | The project's buyer profiles, with each `name` and `who` |
| `POST /projects` | Read and Write | Create a project from a website, or continue a setup |
| `PATCH /projects/{id}` | Read and Write | Rename a project (`name`), or start or stop its scans (`scanning`) |
| `POST /projects/{id}/questions` | Read and Write | Add 1 to 25 questions: `{"questions": [{"text": "…", "topic": "…"}]}` |
| `PATCH /projects/{id}/questions/{question_id}` | Read and Write | Reword (`text`), move (`topic`), pause, or resume (`active`) a question. A paused question keeps its history and does not count toward the plan's questions. No route deletes a question. |
| `POST /projects/{id}/profiles` | Read and Write | Add a buyer profile: `name` and an optional `who` |
| `PATCH /profiles/{id}` | Read and Write | Rename or describe a buyer profile |
| `DELETE /profiles/{id}` | Read and Write | Remove a buyer profile. Its questions stay. |
| `PATCH /recommendations/{id}` | Read and Write | Set `status` to `done`, `dismissed`, or `open` |

The OpenAPI file has every parameter and response field.

## Create a project

A new project starts from a website. Frontmost AI reads the site, then writes the first questions.

1. Send `POST /projects` with `domain` and an `Idempotency-Key` header. Use a new random key, for example a UUID, for each new project.
2. A ready project returns 201 with the `project` object.
3. A setup that needs more time returns 202 with a `project_setup` object and a `Retry-After: 30` header. Its `next` field holds the exact next call.
4. Wait `Retry-After` seconds. Send `POST /projects` with `{"setup_id": "…"}` and no `Idempotency-Key`. Repeat steps 2 to 4.

A repeated call never makes a second project. A site that Frontmost AI cannot read returns 422 `site_unreadable`.

```bash
curl -X POST https://app.frontmost.ai/api/v1/projects \
  -H "Authorization: Bearer $FRONTMOST_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com"}'
```

The same `Idempotency-Key` with the same body returns the first reply with `Idempotent-Replayed: true`. The same key with another body returns 409 `idempotency_conflict`.
Frontmost AI keeps each key for 24 hours.

## Scope

The metrics, rivals, answers, and sources routes take the same scope parameters:

| Parameter | Values | Default |
| --- | --- | --- |
| `scan_id` | a delivered scan of the project | the newest delivered scan |
| `period` | `scan`, `week`, `month` | `scan` |
| `app` | one AI app of the project | every app of the project |
| `questions` | `all`, `branded`, `unbranded` | `all` |
| `compare_to` | `previous`, `none` | `previous` |

- A `week` or `month` is the calendar week or month, in US Eastern time, that holds the anchor scan. It does not end at `scan_id`.
- `previous` is the same kind of period that ends at the last delivered scan before this one starts.
- Each rate comes from pooled counts: `{"value", "numerator", "denominator", "previous"}`. Report the counts with the rate, for example "22% · 8 of 36 answers".
- Every response states its scope: the anchor scan, the dates, the scans counted, the apps, the question set, and the comparison.
- `scope.definitions_version` changes when a counting rule changes.
- A project with no delivered scan returns `"scope": null` and empty lists.

## Lists

A list returns `{"object": "list", "data": [...], "has_more": true, "next_cursor": "…"}`.
`limit` is 1 to 100, and the default is 25. To get the next page, send the same request with `cursor` set to `next_cursor`.
A cursor works only with the list and parameters that made it.

## Errors

Every error has this shape. The `message` says what to do next. Quote `request_id` when you report a problem.

```json
{
  "error": {
    "code": "invalid_param",
    "message": "period must be scan, week, or month.",
    "param": "period",
    "request_id": "req_…"
  }
}
```

| Status | Code | What to do |
| --- | --- | --- |
| 401 | `invalid_key` | The key is missing or unknown. Ask the person for a key from the Team page. |
| 401 | `key_revoked` | The key was revoked. Ask the person for a new key. |
| 403 | `plan_no_api` | The workspace has no active plan. The person chooses a plan on the Billing page. |
| 403 | `needs_plan` | The plan is not running, so changes fail. Reads still work. |
| 403 | `key_read_only` | A Read Only key called a change route. Ask for a Read and Write key. |
| 404 | `not_found` | Nothing with this ID in this workspace, or no such route. |
| 409 | `idempotency_conflict` | This `Idempotency-Key` was used with another body. Use a new key. |
| 409 | `request_in_progress` | A request with this `Idempotency-Key` is still running. Wait `Retry-After` seconds. |
| 422 | `invalid_param` | A parameter is not valid. `param` names it. |
| 422 | `invalid_cursor` | The cursor belongs to another list. Start the list again. |
| 422 | `question_limit` | The change goes over the plan's questions. `details` has `used`, `max`, and `adding`. Pause a question to make room. |
| 422 | `project_limit` | The plan's projects are all in use. `details` has `used` and `max`. |
| 422 | `profile_limit` | The project has the most profiles it can hold. `details` has `max`. Remove a profile first. |
| 422 | `site_unreadable` | Frontmost AI could not read the website. Check the domain. |
| 429 | `rate_limited` | Too many requests. Wait `Retry-After` seconds, then retry. |
| 500 | `internal_error` | Retry with backoff. Quote `request_id` if it keeps failing. |

A route that exists with another method returns 404, and the message names the methods it takes.

## Rate limits

Each key has one budget for reads and one for changes. A browser sign-in has the same two budgets for each person. A month scope costs more than a scan or a week.
Read the headers on every response. Do not hard-code the numbers, because they can change.

```http
HTTP/2 429
Retry-After: 3
RateLimit-Limit: 60
RateLimit-Remaining: 0
RateLimit-Reset: 3
```

On a 429, wait `Retry-After` seconds plus a short random delay, then send the request again.

## Compatibility

- New fields can appear in any response. Ignore fields that you do not know.
- Lists of values, such as AI apps and codes, can grow. Treat an unknown value as "other".
- IDs are strings. They match the IDs in app URLs, for example the `16` in `/projects/16`.
- A change that removes or renames a field goes to `/v2`.

## Command line

The Frontmost AI CLI calls this API from a terminal. Each route on this page is a command.

```bash
brew install frontmost-ai/tap/frontmost
frontmost login
frontmost metrics --period week
```

Without Homebrew, run `npm install -g github:frontmost-ai/cli`. It needs Node.js 22.12 or later.
Add `--json` to any command to get the API response, unchanged. Source: https://github.com/frontmost-ai/cli

## Use with a coding agent

Give the agent the key and this page:

```bash
export FRONTMOST_API_KEY=fmk_…
# Docs: https://app.frontmost.ai/docs/api.md
```

To connect an AI app such as Claude or ChatGPT instead, use the MCP server: https://frontmost.ai/connect
