# API — AgentReady Monitor

> Everything the UI does, the API does too.

## Authentication

Every request needs a bearer token:

```http
Authorization: Bearer ark_live_...
```

You create API keys in the dashboard under Account. The plaintext is shown exactly once; only a SHA-256 hash is stored.

## Conventions

- Base URL: `https://ai-agent-ready.com/api/v1`
- Response format: JSON, always with a `meta` field.
- `meta.pricing` is currently `{"model":"free"}`. The field exists so agents can handle a 402 flow later without the schema changing.
- Errors come back as `{ "error": { "code", "message" }, "meta": {...} }`.

## Endpoints

### GET /api/v1/cases

Returns the cases belonging to the account behind the API key, including the current overall score and the time of the last run. A key carrying the monitor:admin scope reads every tenant instead; in that case each entry also carries owner_email and the response is marked scope: all_tenants.

Required scope: `monitor:read`

MCP tool: `list_cases`

Parameters:

- `q` (query): Filters over case name and domain.

Response:

```json
{
  "cases": [
    {
      "id": "4f1e9b2c-0d8a-4f7b-9d21-1b7c0c9a55e1",
      "user_id": "6b3d1a70-25c8-4f0e-9a3d-77a1f2b4c8d9",
      "name": "Example CRM",
      "domain": "example-crm.com",
      "category": "CRM software for trade businesses",
      "competitors": [
        "Craftnote",
        "Meisterwerk",
        "ToolTime"
      ],
      "status": "active",
      "latest_score": 44,
      "last_run_at": "2026-08-01T03:12:00.000Z"
    }
  ],
  "meta": {
    "pricing": {
      "model": "free"
    },
    "generated_at": "2026-08-25T09:12:44.000Z",
    "docs": "https://ai-agent-ready.com/docs/api"
  }
}
```

Errors:

- `401 unauthorized` — Bearer token missing or unknown.
- `403 no_account` — No Monitor account exists for this key.

### GET /api/v1/cases/{id}

Everything the monitor knows about the brand itself: fact sheet, ICP, competitors, use cases, region and languages. The fact sheet is the ground truth the accuracy scoring runs against, which makes this the endpoint an agent reads before writing or answering about the customer. Resolves across all tenants for a key with the monitor:admin scope, otherwise only within the key holder's own cases.

Required scope: `monitor:read`

MCP tool: `get_case`

Parameters:

- `id` (path, required): Case id (UUID).

Response:

```json
{
  "case": {
    "id": "4f1e9b2c-0d8a-4f7b-9d21-1b7c0c9a55e1",
    "name": "Example CRM",
    "domain": "example-crm.com",
    "category": "CRM software for trade businesses",
    "icp": "Owners of trade businesses with 5 to 50 employees",
    "competitors": [
      "Craftnote",
      "Meisterwerk",
      "ToolTime"
    ],
    "use_cases": [
      "Quoting",
      "Time tracking",
      "Invoicing"
    ],
    "region": "Germany",
    "languages": [
      "en"
    ],
    "fact_sheet": {
      "Price": "From 29 EUR per user and month",
      "Trial": "30 days, no credit card"
    },
    "status": "active",
    "created_at": "2026-07-14T08:02:00.000Z",
    "updated_at": "2026-08-02T11:20:00.000Z"
  },
  "meta": {
    "pricing": {
      "model": "free"
    },
    "generated_at": "2026-08-25T09:12:44.000Z",
    "docs": "https://ai-agent-ready.com/docs/api"
  }
}
```

Errors:

- `401 unauthorized` — Bearer token missing or unknown.
- `403 no_account` — No Monitor account exists for this key.
- `404 not_found` — No case with this id belongs to the key holder.

### GET /api/v1/cases/{id}/summary

The result of the most recent finished run: overall score, score per model, the three score dimensions, share of answer against every competitor, top gaps, the domains the models cited as sources, and the delta to the previous run.

Required scope: `monitor:read`

MCP tool: `get_case_summary`

Parameters:

- `id` (path, required): Case id (UUID).

Response:

```json
{
  "case": {
    "id": "4f1e9b2c-0d8a-4f7b-9d21-1b7c0c9a55e1",
    "name": "Example CRM"
  },
  "run": {
    "id": "9a0c...",
    "label": "Aug 26",
    "kind": "monthly",
    "finished_at": "2026-08-01T03:41:00.000Z"
  },
  "summary": {
    "total_score": 44,
    "per_model": {
      "openai": 48,
      "anthropic": 39,
      "perplexity": 51,
      "google": 38
    },
    "dimensions": {
      "visibility": 1.1,
      "accuracy": 1.4,
      "sentiment": 0.2
    },
    "share_of_answer": [
      {
        "name": "Craftnote",
        "share": 0.8,
        "mentions": 7,
        "judge_mentions": 1,
        "total": 10,
        "is_self": false
      },
      {
        "name": "Example CRM",
        "share": 0.4,
        "mentions": 4,
        "judge_mentions": 0,
        "total": 10,
        "is_self": true
      }
    ],
    "top_gaps": [
      "No prices are named",
      "Nothing is said about a trial"
    ],
    "delta": {
      "total_score": -6,
      "per_model": {
        "openai": -4
      },
      "previous_run_id": "7c21..."
    },
    "top_cited_domains": [
      {
        "domain": "craftnote.de",
        "citations": 9,
        "answers": 6,
        "share": 0.75,
        "is_self": false,
        "competitor": "Craftnote",
        "by_model": {
          "perplexity": 4,
          "openai": 2
        },
        "by_block": {
          "A": 2,
          "E": 4
        }
      },
      {
        "domain": "example-crm.com",
        "citations": 3,
        "answers": 2,
        "share": 0.25,
        "is_self": true,
        "competitor": null,
        "by_model": {
          "perplexity": 2
        },
        "by_block": {
          "B": 2
        }
      }
    ],
    "own_citation_rate": 0.25,
    "competitor_citation_rate": {
      "Craftnote": 0.75,
      "Meisterwerk": 0,
      "ToolTime": 0
    },
    "sources_meta": {
      "answers_total": 10,
      "answers_with_sources": 8,
      "training_recall": {
        "raw_mentions": 1,
        "raw_total": 6,
        "online_mentions": 4,
        "online_total": 6,
        "excluded_models": [
          "perplexity"
        ]
      }
    }
  },
  "meta": {
    "pricing": {
      "model": "free"
    },
    "generated_at": "2026-08-25T09:12:44.000Z",
    "docs": "https://ai-agent-ready.com/docs/api"
  }
}
```

Errors:

- `404 not_found` — Case does not exist or belongs to another account.
- `409 no_run` — No run has finished for this case yet.

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

All measurement runs of a case, newest first, with status, overall score and the delta to the preceding run.

Required scope: `monitor:read`

MCP tool: `list_runs`

Parameters:

- `id` (path, required): Case id (UUID).
- `limit` (query): Maximum number of runs (1–100).

Response:

```json
{
  "runs": [
    {
      "id": "9a0c...",
      "label": "Aug 26",
      "kind": "monthly",
      "status": "done",
      "scheduled_for": "2026-08-01T03:00:00.000Z",
      "finished_at": "2026-08-01T03:41:00.000Z",
      "total_score": 44,
      "delta": -6
    }
  ],
  "meta": {
    "pricing": {
      "model": "free"
    },
    "generated_at": "2026-08-25T09:12:44.000Z",
    "docs": "https://ai-agent-ready.com/docs/api"
  }
}
```

Errors:

- `404 not_found` — Case does not exist or belongs to another account.

### POST /api/v1/cases/{id}/runs

Queues a run. The worker picks it up within seconds. Pro plan only; a run that is already queued or in progress for the same case is not duplicated.

Required scope: `monitor:write`

REST only — not exposed over MCP.

Parameters:

- `id` (path, required): Case id (UUID).

Body:

```json
{
  "label": "After relaunch"
}
```

Response:

```json
{
  "run": {
    "id": "b71f...",
    "label": "After relaunch",
    "kind": "manual",
    "status": "queued"
  },
  "meta": {
    "pricing": {
      "model": "free"
    },
    "generated_at": "2026-08-25T09:12:44.000Z",
    "docs": "https://ai-agent-ready.com/docs/api"
  }
}
```

Errors:

- `402 plan_required` — The plan does not allow manual re-runs.
- `409 run_pending` — A run is already queued or in progress for this case.

### POST /api/v1/scans

Scans a domain live against nine technical criteria in four pillars (Retrievable, Explainable, Actionable, Distributed) and returns a score from 0 to 100. The score is a percentage of the APPLICABLE points, not a plain sum: areas that do not apply to a domain are removed from numerator and denominator, and signals that cannot be verified from outside count as half. A check may therefore report state "unknown" with max 0. Takes 5-30 seconds. Free; without a bearer key you get 3 scans per day per IP.

Required scope: `none — bearer optional`

MCP tool: `scan_domain`

Body:

```json
{
  "domain": "example.com"
}
```

Response:

```json
{
  "scan": {
    "id": "0e2f8c11-6b4a-4a7e-9f10-2c3d4e5f6a7b",
    "domain": "example.com",
    "score": 62,
    "scoring_version": 2,
    "created_at": "2026-09-14T08:31:12.000Z",
    "url": "https://ai-agent-ready.com/check/scan/0e2f8c11-6b4a-4a7e-9f10-2c3d4e5f6a7b",
    "checks": [
      {
        "id": "crawler",
        "pillar": "R",
        "title": "AI crawler access",
        "state": "good",
        "score": 18,
        "max": 18,
        "findings": [
          {
            "state": "good",
            "text": "GPTBot, ClaudeBot and PerplexityBot all receive 200."
          }
        ]
      }
    ]
  },
  "meta": {
    "pricing": {
      "model": "free"
    },
    "generated_at": "2026-08-25T09:12:44.000Z",
    "docs": "https://ai-agent-ready.com/docs/api"
  }
}
```

Errors:

- `400 invalid_domain` — The domain is missing or cannot be parsed.
- `401 invalid_key` — A bearer token was sent but is unknown.
- `503 auth_unavailable` — A bearer token was sent but could not be verified right now.
- `429 rate_limited` — Anonymous daily scan limit reached.
- `500 scan_failed` — The scan could not be completed.

### GET /api/v1/scans/{id}

Returns a previously created scan with all nine checks and their findings. Results are public: the same payload backs the shareable result page.

Required scope: `none — bearer optional`

MCP tool: `get_scan`

Parameters:

- `id` (path, required): Scan id (UUID).

Response:

```json
{
  "scan": {
    "id": "0e2f8c11-6b4a-4a7e-9f10-2c3d4e5f6a7b",
    "domain": "example.com",
    "score": 62,
    "scoring_version": 2,
    "created_at": "2026-09-14T08:31:12.000Z",
    "url": "https://ai-agent-ready.com/check/scan/0e2f8c11-6b4a-4a7e-9f10-2c3d4e5f6a7b",
    "checks": [
      {
        "id": "crawler",
        "pillar": "R",
        "title": "AI crawler access",
        "state": "good",
        "score": 18,
        "max": 18,
        "findings": [
          {
            "state": "good",
            "text": "GPTBot, ClaudeBot and PerplexityBot all receive 200."
          }
        ]
      }
    ]
  },
  "meta": {
    "pricing": {
      "model": "free"
    },
    "generated_at": "2026-08-25T09:12:44.000Z",
    "docs": "https://ai-agent-ready.com/docs/api"
  }
}
```

Errors:

- `401 invalid_key` — A bearer token was sent but is unknown.
- `503 auth_unavailable` — A bearer token was sent but could not be verified right now.
- `404 not_found` — There is no result for this id.

### GET /api/v1/stats

Anonymous aggregates over every scan ever run at the current scoring version: number of domains, score distribution and how often each check fails. No domain names, no personal data. This is the data behind the public report.

Required scope: `none — bearer optional`

MCP tool: `get_stats`

Response:

```json
{
  "stats": {
    "domains": 1284,
    "scans": 1731,
    "scoring_version": 2,
    "median_score": 54,
    "updated_at": "2026-09-14T08:00:00.000Z"
  },
  "meta": {
    "pricing": {
      "model": "free"
    },
    "generated_at": "2026-08-25T09:12:44.000Z",
    "docs": "https://ai-agent-ready.com/docs/api"
  }
}
```

Errors:

- `401 invalid_key` — A bearer token was sent but is unknown.
- `503 auth_unavailable` — A bearer token was sent but could not be verified right now.

### POST /api/v1/scans/batch

Queues a technical scan for each domain and returns a job id straight away. A single scan takes about a minute, so the work runs through a durable queue rather than inside the request. Poll GET /api/v1/scans/batch/{id} for status and per-domain results. Quantity limits depend on the plan behind the key and are enforced atomically, so two simultaneous requests cannot exceed them together. The free single scan on POST /api/v1/scans is unaffected.

Required scope: `scans:write`

REST only — not exposed over MCP.

Body:

```json
{
  "domains": [
    "example-crm.com",
    "craftnote.de",
    "not a domain"
  ]
}
```

Response:

```json
{
  "job": {
    "id": "b3d0f6a1-77c2-4c2e-9a1f-9f2c3d4e5f60",
    "status": "queued",
    "total": 2,
    "created_at": "2026-09-16T10:04:00.000Z",
    "url": "https://ai-agent-ready.com/api/v1/scans/batch/b3d0f6a1-77c2-4c2e-9a1f-9f2c3d4e5f60"
  },
  "accepted": [
    "example-crm.com",
    "craftnote.de"
  ],
  "rejected": [
    {
      "domain": "not a domain",
      "reason": "That is not a valid domain."
    }
  ],
  "limits": {
    "plan": "Starter",
    "per_batch": 10,
    "per_day": 25
  },
  "meta": {
    "pricing": {
      "model": "free"
    },
    "generated_at": "2026-08-25T09:12:44.000Z",
    "docs": "https://ai-agent-ready.com/docs/api"
  }
}
```

Errors:

- `400 invalid_request` — The body carries no usable domains array.
- `402 plan_required` — The subscription has ended. Existing results stay readable; new measurements do not.
- `403 insufficient_scope` — The key does not carry scans:write.
- `429 quota_exceeded` — The batch or daily limit of the plan is reached.

### GET /api/v1/scans/batch/{id}

Returns the job counters plus one row per domain: its score and scan id once done, or the reason it failed. A partial failure is a result, not an abort — the remaining domains keep running. Jobs belonging to another account read as not found.

Required scope: `scans:read`

REST only — not exposed over MCP.

Parameters:

- `id` (path, required): The job id.

Response:

```json
{
  "job": {
    "id": "b3d0f6a1-77c2-4c2e-9a1f-9f2c3d4e5f60",
    "status": "running",
    "created_at": "2026-09-16T10:04:00.000Z",
    "finished_at": null,
    "counts": {
      "total": 2,
      "queued": 1,
      "done": 1,
      "failed": 0
    }
  },
  "results": [
    {
      "domain": "example-crm.com",
      "status": "done",
      "score": 61,
      "scan_id": "4a1c34f1-fce3-4674-bbfb-d7da43265095",
      "error": null,
      "finished_at": "2026-09-16T10:05:07.000Z"
    },
    {
      "domain": "craftnote.de",
      "status": "queued",
      "score": null,
      "scan_id": null,
      "error": null,
      "finished_at": null
    }
  ],
  "meta": {
    "pricing": {
      "model": "free"
    },
    "generated_at": "2026-08-25T09:12:44.000Z",
    "docs": "https://ai-agent-ready.com/docs/api"
  }
}
```

Errors:

- `403 insufficient_scope` — The key does not carry scans:read.
- `404 not_found` — No such job for this account.

### GET /api/v1/webhooks

Lists the webhook endpoints of the account together with the last deliveries, including status code, attempt count and error text. The signing secret is never returned — it goes over the wire exactly once, when the endpoint is created.

Required scope: `webhooks:manage`

REST only — not exposed over MCP.

Response:

```json
{
  "events": [
    "scan.completed",
    "run.completed",
    "score.changed"
  ],
  "endpoints": [
    {
      "id": "0d0a5b1f-2c3d-4e5f-8a9b-0c1d2e3f4a5b",
      "url": "https://example.com/hooks/agentready",
      "label": "Shopify app",
      "events": [
        "scan.completed",
        "run.completed"
      ],
      "active": true,
      "created_at": "2026-09-16T09:00:00.000Z",
      "last_delivery_at": "2026-09-16T10:05:09.000Z"
    }
  ],
  "recent_deliveries": [
    {
      "id": "9f8e7d6c-5b4a-4938-2716-0f1e2d3c4b5a",
      "endpoint_id": "0d0a5b1f-2c3d-4e5f-8a9b-0c1d2e3f4a5b",
      "event_id": "7c6b5a49-3827-4615-a0f1-e2d3c4b5a697",
      "event": "scan.completed",
      "status": "delivered",
      "attempts": 1,
      "last_status": 200,
      "last_error": null,
      "created_at": "2026-09-16T10:05:08.000Z",
      "delivered_at": "2026-09-16T10:05:09.000Z"
    }
  ],
  "meta": {
    "pricing": {
      "model": "free"
    },
    "generated_at": "2026-08-25T09:12:44.000Z",
    "docs": "https://ai-agent-ready.com/docs/api"
  }
}
```

Errors:

- `403 insufficient_scope` — The key does not carry webhooks:manage.

### POST /api/v1/webhooks

Registers an https endpoint and returns its signing secret once. Each delivery carries x-agentready-event-id, x-agentready-timestamp and x-agentready-signature, the latter being v1=HMAC-SHA256(secret, `${timestamp}.${rawBody}`). Verify both the signature and the timestamp age; the event id stays stable across retries so you can deduplicate. URLs must be https and must not point into a private network — this is checked when you register and again against the resolved address before every delivery.

Required scope: `webhooks:manage`

REST only — not exposed over MCP.

Body:

```json
{
  "url": "https://example.com/hooks/agentready",
  "events": [
    "scan.completed",
    "run.completed"
  ],
  "label": "Shopify app"
}
```

Response:

```json
{
  "endpoint": {
    "id": "0d0a5b1f-2c3d-4e5f-8a9b-0c1d2e3f4a5b",
    "url": "https://example.com/hooks/agentready",
    "label": "Shopify app",
    "events": [
      "scan.completed",
      "run.completed"
    ],
    "active": true,
    "created_at": "2026-09-16T09:00:00.000Z"
  },
  "secret": "whsec_9f1c...",
  "signature": {
    "header": "x-agentready-signature",
    "scheme": "v1=HMAC-SHA256(secret, `${timestamp}.${rawBody}`)",
    "timestamp_header": "x-agentready-timestamp",
    "event_id_header": "x-agentready-event-id"
  },
  "meta": {
    "pricing": {
      "model": "free"
    },
    "generated_at": "2026-08-25T09:12:44.000Z",
    "docs": "https://ai-agent-ready.com/docs/api"
  }
}
```

Errors:

- `400 invalid_url` — Not https, carries credentials, or points at a private host.
- `403 insufficient_scope` — The key does not carry webhooks:manage.
- `409 duplicate_endpoint` — That URL is already registered for this account.

### PATCH /api/v1/webhooks/{id}

Changes the subscribed events, the label, or pauses delivery. The URL cannot be changed: together with the secret it is the contract with one receiver, so a different target gets its own secret.

Required scope: `webhooks:manage`

REST only — not exposed over MCP.

Parameters:

- `id` (path, required): The endpoint id.

Body:

```json
{
  "events": [
    "score.changed"
  ],
  "active": false
}
```

Response:

```json
{
  "endpoint": {
    "id": "0d0a5b1f-2c3d-4e5f-8a9b-0c1d2e3f4a5b",
    "url": "https://example.com/hooks/agentready",
    "label": "Shopify app",
    "events": [
      "score.changed"
    ],
    "active": false
  },
  "meta": {
    "pricing": {
      "model": "free"
    },
    "generated_at": "2026-08-25T09:12:44.000Z",
    "docs": "https://ai-agent-ready.com/docs/api"
  }
}
```

Errors:

- `400 invalid_event` — An unknown event name was sent.
- `404 not_found` — No such endpoint for this account.

### DELETE /api/v1/webhooks/{id}

Removes the endpoint and every delivery still waiting for it.

Required scope: `webhooks:manage`

REST only — not exposed over MCP.

Parameters:

- `id` (path, required): The endpoint id.

Response:

```json
{
  "deleted": "0d0a5b1f-2c3d-4e5f-8a9b-0c1d2e3f4a5b",
  "meta": {
    "pricing": {
      "model": "free"
    },
    "generated_at": "2026-08-25T09:12:44.000Z",
    "docs": "https://ai-agent-ready.com/docs/api"
  }
}
```

Errors:

- `404 not_found` — No such endpoint for this account.

## OpenAPI

- Spec: https://ai-agent-ready.com/api/v1/openapi.json

## Scopes

Every route checks the scope it needs. A key that lacks it gets `403 insufficient_scope` — it is never silently downgraded to an anonymous call.

- `monitor:read` — read cases, runs and summaries.
- `monitor:write` — start manual runs (Pro).
- `scans:read` — read technical scans and batch jobs.
- `scans:write` — start technical scans, single or batched.
- `webhooks:manage` — register, change and revoke delivery targets.
- `monitor:admin` — read every tenant. Only for a key created by an address in `ADMIN_EMAILS`.
- `plugin` — marks a key issued to a marketplace plugin. A plugin key never carries `monitor:admin`.

## Webhooks

Deliveries are `POST`ed as JSON with these headers:

```http
x-agentready-event: run.completed
x-agentready-event-id: 7c6b5a49-3827-4615-a0f1-e2d3c4b5a697
x-agentready-timestamp: 2026-09-16T10:05:08.000Z
x-agentready-signature: v1=<hex>
```

The signature is `HMAC-SHA256(secret, timestamp + "." + rawBody)`, hex-encoded. Verify it against the raw body before parsing, and reject deliveries whose timestamp is more than five minutes old. Retries reuse the same `x-agentready-event-id`, so deduplicate on it. Failed deliveries are retried with growing delays and stay visible under Account → Webhooks.

## MCP

MCP stays read-only. Every tool maps one to one onto a `GET` endpoint here; the operations that change state exist over REST only.

- `list_cases` → `GET /api/v1/cases`
- `get_case` → `GET /api/v1/cases/{id}`
- `get_case_summary` → `GET /api/v1/cases/{id}/summary`
- `list_runs` → `GET /api/v1/cases/{id}/runs`
- `scan_domain` → `POST /api/v1/scans`
- `get_scan` → `GET /api/v1/scans/{id}`
- `get_stats` → `GET /api/v1/stats`

A product by The Autopilot — https://the-autopilot.com
