# API

Everything the CLI does goes through this HTTP API, and the CLI's `--json` output is the API's response bodies, so the two never disagree.

- **Base URL:** `https://dromad.dev/api/v1`
- **Format:** JSON in and out.
- **Schema:** [`/api/v1/openapi.json`](https://dromad.dev/api/v1/openapi.json), with an [interactive explorer](https://dromad.dev/api/v1/docs). The reference below is generated from the same schema.

## Authentication

Send a bearer token on every request:

```sh
export DROMAD_API_KEY=dmk_…   # Account › API keys; shown once
curl -s https://dromad.dev/api/v1/me -H "Authorization: Bearer $DROMAD_API_KEY"
```

API keys (`dmk_…`) are for servers, CI and agents; revoke them at Account › API keys. **A key acts in one workspace**, the one it was made for, and spends that workspace's credit: everything outside it is `404` to the key. The token `dromad login` stores (`dct_…`) is a per-device credential for a person, and reaches every workspace they belong to. `GET /me` lists the workspaces a credential can act in.

Keys can also be managed over the API: `GET /api-keys` lists them, with `last_used_at` and `revoked_at`; `POST /api-keys` with `{"name": "…", "workspace_id": "ws_…"}` creates one (for your own workspace if `workspace_id` is omitted) and returns the secret once, in `key`; `DELETE /api-keys/{id}` revokes one.

## Errors

Every non-2xx response has one shape:

```json
{"error": {"code": "conflict", "message": "Only a finished run can be shared; dr_… is queued.", "engine": null, "retryable": false}}
```

`code` is stable and machine-readable; `message` is for people; `engine` names the answer engine when one is at fault; `retryable` says whether the same request could succeed later. Treat an unknown `code` as its HTTP status.

| Code | Status | Meaning |
|---|---|---|
| `invalid_request` | 400 | The request is malformed or asks for something impossible. |
| `unauthenticated` | 401 | No credential, or a revoked or unknown one. |
| `insufficient_credit` | 402 | The account's credit does not cover the request. Nothing was created; `message` gives what it needs, what is available and where to add credit. `retryable` is false. |
| `permission_denied` | 403 | The credential cannot do this. |
| `not_found` | 404 | No such object, or it is not yours. |
| `conflict` | 409 | The object is not in a state that allows this (sharing an unfinished run). |
| `rate_limited` | 429 | The account has too many runs queued or running at once. `message` gives the numbers; `retryable` is true. |
| `engine_error` | 502 | An answer engine failed; `engine` says which. |
| `internal_error` | 500 | Dromad failed. |
| `cli_outdated` | 426 | The request came from a `dromad` CLI older than the server supports. Nothing was run. See below. |

An engine failing inside a prompt does not fail the request: that run is `failed` with its own error, and the batch is `partial`.

**CLI versions.** `GET /version` needs no credential and returns `{"min_cli_version": "0.2.0"}`, the oldest `dromad` CLI the server answers. The CLI names its version in `User-Agent` (`dromad-cli/0.2.2`), and a released version below the minimum gets `cli_outdated` from every other endpoint, before authentication and before anything is created or charged, because commands have changed meaning between versions. Only that `User-Agent` is checked: your own client is never refused for its version.

## IDs and objects

IDs are `<prefix>_<ULID>` and sort by creation time: `ws_` workspace, `pr_` project, `dr_` run, `rq_` batch, `fo_` fanout, `fa_` questions analysis, `ca_` citation analysis, `sh_` share, `ak_` API key. A run is one engine execution or one page audit; a batch is the runs one prompt request started; a fanout is the prompt runs of a set of prompts on several engines, repeated, with what each searched; an analysis is derived from runs. Finished runs and analyses never change.

**Work is filed under a project**, and a project belongs to a workspace ([Teams](https://dromad.dev/docs/teams)). `POST /prompts`, `/fanouts`, `/questions` and `/audits` take `"project": "pr_…"` and answer `400` without it; `GET /projects` lists the ones a credential can use and `POST /projects` starts one. A citation analysis names no project: it is filed with its runs, which must all be in one. Runs and analyses report `project_id` and `created_by`; lists take `?project=` and `?workspace=`. Anything in a workspace the credential cannot act in is `404`, the same as something that does not exist; `403` (`permission_denied`) means you are in the workspace and only an admin may do that.

## Starting work and waiting for it

Work that calls an engine or fetches a page is queued, and the request returns at once:

| Request | Returns |
|---|---|
| `POST /prompts` | `202` · the batch and its queued runs |
| `POST /fanouts` | `202` · the queued fanout, its runs queued |
| `POST /fanouts/estimate` | `200` · what that fanout would run and its estimated cost; informational, starts and authorizes nothing |
| `POST /questions` | `202` · the queued questions analysis |
| `POST /audits` | `202` · the queued audit run |
| `POST /citation-analyses` | `201` · the finished analysis (it calls no model) |

To wait, fetch the object with `?wait=N` (0–30 seconds): the request returns as soon as the object finishes, or after `N` seconds with it still running. `GET /runs/{id}`, `GET /batches/{id}`, `GET /fanouts/{id}`, `GET /analyses/{id}` and `GET /runs?batch=…` all accept it.

```sh
AUTH="Authorization: Bearer $DROMAD_API_KEY"
batch=$(curl -s -X POST https://dromad.dev/api/v1/prompts -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"prompt": "best search APIs for AI agents", "engines": ["chatgpt", "claude", "gemini"], "project": "pr_…"}' | jq -r .batch.id)
curl -s "https://dromad.dev/api/v1/batches/$batch?wait=30" -H "$AUTH" | jq '.batch.status'
```

Run statuses are `queued`, `running`, `completed` and `failed`. A batch is `completed` when every run completed, `partial` when some failed, and `failed` when all did.

## Pagination

`GET /runs` and `GET /analyses` return `{"items": [...], "next_cursor": "…"}`, newest first. Pass `limit` (1–200, default 50) and, for the next page, `cursor=<next_cursor>`. `next_cursor` is `null` on the last page.

## Sharing

A finished run or analysis can have one public, read-only link. The same four requests exist under `/runs/{id}/share` and `/analyses/{id}/share`:

| Request | Does |
|---|---|
| `GET …/share` | `{"shared": true/false, "share": …}`: whether a link is active, and which. |
| `POST …/share` | Creates the link, or returns the active one. `409 conflict` if the object has not finished. |
| `POST …/share/rotate` | Replaces the link; the old one stops working. |
| `DELETE …/share` | Stops sharing; the link stops working at once. |

The share object's `url` is the page to hand out. Shared pages need no account and ask search engines not to index them.

## Examples

```sh
# What engines search for a set of prompts. The estimate is informational: what
# decides whether the fanout starts is the workspace's available credit (402 if short).
BODY='{"prompts": [{"text": "free web search MCP server for Claude Code"}], "runs": 3, "project": "pr_…"}'
curl -s -X POST https://dromad.dev/api/v1/fanouts/estimate -H "$AUTH" -H 'Content-Type: application/json' -d "$BODY" \
  | jq '{executions, total_usd, available_usd, sufficient}'
fanout=$(curl -s -X POST https://dromad.dev/api/v1/fanouts -H "$AUTH" -H 'Content-Type: application/json' -d "$BODY" | jq -r .id)
curl -s "https://dromad.dev/api/v1/fanouts/$fanout?wait=30" -H "$AUTH" \
  | jq '.prompts[].engines[] | {engine, runs: [.runs[] | [.queries[].text]]}'

# Questions for a topic
curl -s -X POST https://dromad.dev/api/v1/questions -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"topic": "AI search API", "count": 30, "project": "pr_…"}'

# Count what a set of runs and batches cited
curl -s -X POST https://dromad.dev/api/v1/citation-analyses -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"ids": ["rq_…", "rq_…"], "domains": ["exa.ai"], "brands": ["Exa"]}'

# Audit a page
curl -s -X POST https://dromad.dev/api/v1/audits -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"url": "https://exa.ai/docs/reference/search", "project": "pr_…"}'

# Share a finished analysis
curl -s -X POST https://dromad.dev/api/v1/analyses/ca_…/share -H "$AUTH" | jq -r .url
```
