KovmereHow it worksPricingMethodologyBuy the Check — $29

Agency API (v1)

A small key-based JSON API for driving an Agency workspace from a script instead of the dashboard. Meant for the agency's own tooling (bulk scans, syncing rates into another system) — there is no self-serve API for the Check / Fix Pack / Subscription tiers.

Implemented in web/routes_api.py.

Auth

Every request needs the workspace's owner key (the same key used in the dashboard URL, /agency/{workspace_id}?key=ok_...) as a bearer token:

Authorization: Bearer ok_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Missing or wrong key → 401 {"error": "unauthorized"}.

There is no OAuth, no per-endpoint scopes, and no key rotation endpoint — if a key leaks, ask us to reissue the workspace's key.

Rate limit

60 requests / minute per key, enforced in memory by the running server process. Over the limit → 429 {"error": "rate_limited", "detail": "60 requests/min per key"}.

Endpoints

POST /api/v1/scans

Start a scan for one site in your workspace — the API equivalent of the dashboard's "add a site" form.

Request body:

{
  "site": "https://example.com",
  "brand": "Example",
  "category": "invoicing software for small construction companies",
  "competitors": [{"name": "Acme", "domain": "acme.com"}]
}

Responses:

GET /api/v1/scans/{job_id}

Poll a scan's status. Only returns jobs that belong to your workspace (a job id from another workspace, or an unknown id, is a 404 — never disclosed as "exists but not yours").

{
  "job_id": "260905-example-ab12",
  "status": "queued | running | done | error | cancelled",
  "stage": "sampling",
  "done": 24,
  "total": 80,
  "error": null,
  "site": "https://example.com",
  "summary": { ... }
}

summary is present once status is "done" and has the same shape as GET /r/{jid}/summary.json (schema version "1.0": rates.{recommended,mentioned,cited}, per_engine, per_prompt, share_of_voice, sources, shape, gaps — see that endpoint's own documentation block for field meanings).

GET /api/v1/sites

All sites in your workspace, each with its most recent report's headline rates.

{
  "sites": [
    {
      "id": "example.com|invoicing software...|acme.com,rival.io",
      "url": "https://example.com",
      "brand": "Example",
      "category": "invoicing software for small construction companies",
      "active": true,
      "last_job": "260905-example-ab12",
      "next_retest_at": "2026-09-19 00:00:00",
      "recommended_rate": 0.42,
      "mentioned_rate": 0.61,
      "cited_rate": 0.30,
      "n_samples": 80
    }
  ]
}

Rates are null until the site's first scan finishes.

POST /api/v1/webhooks

Register (or replace) a webhook URL for your workspace. One URL per workspace; posting again overwrites it.

Request body: {"url": "https://your-system.example.com/hooks/ai-visibility"}

Response: 200 {"ok": true, "webhook_url": "..."}, or 400 {"error": "invalid_url"}.

Delivery: whenever a scan belonging to your workspace finishes, we POST to your URL:

{
  "job_id": "260905-example-ab12",
  "site": "https://example.com",
  "recommended": 34,
  "n": 80,
  "report_url": "https://.../r/260905-example-ab12"
}

Delivery is best-effort: one attempt, 10-second timeout, plus a single retry 30 seconds later for a timeout or a 5xx response — never for a 4xx, and never more than once. No signature. Failures are logged on our side but never surfaced to you beyond that — if you need reliability, poll GET /api/v1/scans/{job_id} instead or in addition.