# AIronClaw REST API — full reference

All paths are relative to `${AIRONCLAW_BASE_URL}` (default `https://dashboard.aironclaw.com`). Every request requires `Authorization: Bearer ${AIRONCLAW_TOKEN}`.

Conventions used below:

- `BODY` blocks are JSON request bodies; field types are TypeScript-ish.
- `RESPONSE` blocks show the success-case shape only. Error responses are always `{ "error": "<message>" }` with the appropriate status code.
- A field marked `(safe)` means it has been scrubbed of secrets and internals before being returned. Do not assume the same shape exists in Redis or the gateway.

---

## Profile

### `GET /api/profile`

Caller identity.

```json
RESPONSE 200
{
  "name": "string | null",
  "email": "string",
  "consumerId": "uuid | null",
  "consumerUsername": "string | null",
  "tags": ["private:permissions:user", "private:permissions:mcp:limit:10", ...]
}
```

The `tags` array on the consumer encodes per-user quotas:
- `private:permissions:mcp:limit:N` — max MCP servers
- `private:permissions:apikey:limit:N` — max API keys (PAT excluded)
- `private:permissions:resource:limit:N` — max resources in the user's repository
- `private:permissions:mcp:max_resp_body_kb:N` — MCP response-body buffer cap (KB, default 512); past it the plugin returns a "Response too large" JSON-RPC error
- `private:permissions:llm:max_resp_body_kb:N` — LLM upstream-body buffer cap (KB, default 2048); only affects token-usage parsing, never the streamed response

### `GET /api/profile/token`

PAT-forbidden (session only). Returns `{"hasToken": boolean}`.

### `POST /api/profile/token`

PAT-forbidden (session only). Issues or rotates the PAT. Returns `{"token": "<plaintext>", "rotated": boolean}` exactly once.

---

## API keys (for downstream clients)

These are the keys end-clients use to call MCP/LLM proxies. They are NOT the PAT.

### `GET /api/keys`

```json
RESPONSE 200
{
  "keys": [
    {
      "id": "uuid",
      "key": "abcdef...wxyz",      // masked: first 6 + "..." + last 4 of the SHA-256 hash
      "tags": ["name:my-key", "private:permissions:mcp", "mcp:<uuid>:tool:*", ...],
      "created_at": 1714000000,
      "ttl": null
    }
  ]
}
```

### `POST /api/keys`

```json
BODY
{
  "name": "string",                 // required, [a-zA-Z0-9_-]+
  "mcpPermissions": [
    { "id": "<mcp-uuid>", "tools": ["search", "fetch"], "resources": ["<rid>"] }
  ],
  "llmPermissions": [
    { "id": "<llm-uuid>", "models": ["gpt-4o-mini"] }
  ],
  "customTags": ["string"]          // optional, free-form passthrough
}
```

The server cross-checks every UUID against the caller's owned set — forging another user's tags is rejected. Use `["*"]` for tools/models/resources to grant unrestricted access on that proxy.

```json
RESPONSE 201
{
  "key": {
    "id": "uuid",
    "key": "<plaintext>",          // shown ONCE — store it client-side
    "tags": [...]
  },
  "message": "Save this key now — it will not be shown in plain text again."
}
```

### `PATCH /api/keys/:id`

Same body shape as POST (minus `name`, which is pinned at creation). Replaces the entire tag set computed from the input.

### `DELETE /api/keys/:id`

Returns `{"message": "Key deleted"}`.

---

## MCP servers

### `GET /api/mcp`

```json
RESPONSE 200
{
  "servers": [
    {
      "id": "uuid",
      "name": "string",
      "url": "https://upstream-mcp.example.com",
      "authType": "none | bearer",   // for upstream auth (AIronClaw → MCP)
      "auth": {                      // for inbound auth (client → AIronClaw)
        "mode": "aifw_api_key | jwt",
        "issuer": "string?",
        "audience": "string?",
        "jwksJson": "string?",
        "clockSkewS": 30
      } | null,
      "tools": [{"name": "string", "description": "string", "inputSchema": {...}}, ...],
      "resources": [...],
      "createdAt": 1714000000,
      "proxyHost": "<uuid>.aifirewall.aironclaw.com"
    }
  ]
}
```

### `GET /api/mcp/:id`

Same shape as the array element above, plus `"pin": { hostname, ip, port } | null` reflecting the current DNS pin the gateway is using for the upstream.

### `POST /api/mcp`

```json
BODY
{
  "name": "string",                 // required
  "url": "https://...",             // required, must resolve to a public IP (SSRF-blocked)
  "authType": "none | bearer",
  "authToken": "string?",           // required iff authType="bearer"; encrypted at rest
  "auth": {                         // optional inbound auth
    "mode": "jwt",
    "issuer": "https://idp.example.com",
    "audience": "my-mcp",
    "jwksJson": "{ \"keys\": [...] }",
    "clockSkewS": 30
  }
}
```

`authType` controls the *upstream* bearer (AIronClaw → real MCP). `auth.mode` controls how *inbound* requests authenticate to AIronClaw (default = AIronClaw API key; alt = user-supplied JWT verified against the pasted JWKS).

If `auth.mode = "jwt"`, validate the JWKS first with `POST /api/mcp/jwks/test`.

```json
RESPONSE 201
{ "server": { ... } }    // same shape as GET, with id assigned
```

Side effects: a gateway service is created, plus two routes (one host-based at `<id>.aifirewall.<DOMAIN>`, one path-based at `/<id>`), plus an `aifw` plugin instance. The MCP becomes immediately reachable at `proxyHost`.

### `PATCH /api/mcp/:id`

Any subset of `{ name, url, authType, authToken, auth }`. URL changes trigger a gateway upstream/target reconciliation (DNS pin is refreshed). Auth changes are pushed to the gateway plugin instance.

### `DELETE /api/mcp/:id`

Tears down the the gateway resources, strips `mcp:<id>:tool:*` and `mcp:<id>:resource:*` tags from every API key, then deletes the Redis record. Not undoable.

### `POST /api/mcp/:id/tools`

Connects to the upstream MCP via `tools/list`, persists the tool catalog. Use after the user reports the upstream MCP changed its tools, or after creating an MCP for the first time.

```json
RESPONSE 200
{ "tools": [...] }
```

### `POST /api/mcp/:id/re-resolve`

Force-refreshes the DNS pin the gateway uses for the upstream. Useful after a known IP change.

### `DELETE /api/mcp/:id/cache?tool=<toolName>`

Drops every cached response for the given tool on this MCP. Does not affect other tools.

### `POST /api/mcp/jwks/test`

```json
BODY  { "jwksJson": "{ \"keys\": [...] }" }
RESPONSE 200
  on valid:    { "ok": true,  "keys": [{ "kid", "kty", "alg", "use" }, ...] }
  on invalid:  { "ok": false, "error": "string" }
```

---

## MCP resources (synthetic)

A "resource" here is a static text payload AIronClaw exposes to clients via the MCP `resources/list` and `resources/read` flows, without ever touching the upstream MCP. Useful for serving instructions, prompts, or prepared context.

### `GET /api/mcp/:id/resources`

```json
RESPONSE 200
{ "resources": [{ "id", "uri", "name", "description?", "mimeType", "createdAt" }, ...] }
```

### `POST /api/mcp/:id/resources`

```json
BODY
{
  "uri": "string",                  // required, unique within the MCP
  "name": "string",                 // required
  "description": "string?",
  "mimeType": "text/plain",         // default
  "content": "string"               // required; the payload served to clients
}
```

### `GET /api/mcp/:id/resources/:rid`

Returns the full record including `content`.

### `PATCH /api/mcp/:id/resources/:rid`

Any subset of the create body.

### `DELETE /api/mcp/:id/resources/:rid`

`{"deleted": true}`.

---

## MCP firewall rules

### `GET /api/mcp/:id/rules`

```json
RESPONSE 200
{ "rules": [...AifwRule] }
```

### `PUT /api/mcp/:id/rules`

Replaces the **entire** rule array. To add a single rule, GET first, append, then PUT.

```json
BODY  { "rules": [...AifwRule] }
RESPONSE 200  { "rules": [...stored rules] }
```

`AifwRule` is a discriminated union on `rule_type`. See [rules-and-dlp.md](rules-and-dlp.md) for the full schema and worked examples per type. Valid types for MCP proxies:

- `ip_acl`
- `rate_limit`
- `tool_description_inject`
- `response_replace` (DLP)
- `lambda` (phase: `access` | `response`)
- `static_cache`
- `mcp_resource`

---

## LLM proxies

### `GET /api/llm`

```json
RESPONSE 200
{ "proxies": [
  {
    "id": "uuid",
    "name": "string",
    "provider": "openai | anthropic | google | mistral",
    "upstreamUrl": "string",        // derived from provider; not user-settable
    "allowedModels": ["string"],    // empty = all provider models allowed
    "defaultModel": "string | null",
    "logConversations": false,
    "auth": { ... } | null,         // same shape as MCP inbound auth
    "budget": {
      "period": "fixed | daily | weekly | monthly",
      "capUsd": 0,
      "hardBlock": false
    } | null,
    "createdAt": 1714000000,
    "proxyHost": "<uuid>.aifirewall.aironclaw.com"
  }
] }
```

### `GET /api/llm/:id`

Element shape above plus `"pin": {...} | null`.

### `POST /api/llm`

```json
BODY
{
  "name": "string",                          // required
  "provider": "openai | anthropic | google | mistral",   // required
  "allowedModels": ["string"],
  "defaultModel": "string?",
  "providerKey": "string?",                  // upstream provider key; encrypted at rest
  "logConversations": false,
  "auth": { ... },                           // same as MCP
  "budget": { "period", "capUsd", "hardBlock" }
}
```

`upstreamUrl` is derived from `provider` (`openai` → `https://api.openai.com`, etc.) and is never user-settable.

### `PATCH /api/llm/:id`

Any subset of the create body. Provider changes swap the upstream URL automatically.

### `DELETE /api/llm/:id`

Same teardown semantics as MCP.

### `POST /api/llm/:id/re-resolve`

Refreshes the DNS pin for the provider host.

---

## LLM rules

`PUT /api/llm/:id/rules` — same shape as MCP rules. Valid types for LLM proxies (the schema rejects others at gateway level):

- `ip_acl`
- `rate_limit` (including `match_key=tokens_per_minute` for token-rate limits)
- `prompt_replace` (LLM equivalent of `response_replace` — redacts the prompt)
- `prompt_guard` (regex-based prompt-injection / jailbreak detection)
- `model_route` (rewrite the requested model when the prompt matches a regex)
- `lambda` (phase: `access` only)
- `static_cache`

`response_replace` is **rejected** on LLM proxies — LLM responses are not mutated.

---

## LLM usage & budget

### `GET /api/llm/:id/usage?months=12`

Monthly series + per-key breakdown for the current month. Read-only.

### `GET /api/llm/:id/usage/daily?days=14`

Daily series + budget config + current-window running spend.

### `DELETE /api/llm/:id/usage/history?all=true|before=YYYYMMDD`

Removes daily history hashes. Budget counters untouched (use `/budget/reset` for those).

### `GET /api/llm/:id/budget`

(via `GET /api/llm/:id` — there is no separate GET endpoint; budget is a field on the proxy.)

### `POST /api/llm/:id/budget/reset`

Zeroes the current spend window for the proxy. Returns `{"reset": true}`.

### `GET /api/llm/:id/keys`

Per-API-key budget configurations and usage links for this proxy.

### `PUT /api/llm/:id/keys/:credId/budget`

```json
BODY  { "period": "monthly", "capUsd": 50, "hardBlock": true }
```

### `DELETE /api/llm/:id/keys/:credId/budget`

Removes the per-key cap (proxy cap still applies).

### `POST /api/llm/:id/keys/:credId/budget/reset`

Zeroes the per-key spend window.

### `GET /api/llm/:id/keys/:credId/usage/daily?days=14`

Per-key daily series.

### `DELETE /api/llm/:id/keys/:credId/usage/history?all=true|before=YYYYMMDD`

---

## LLM logs

Available only when the proxy has `logConversations: true`.

### `GET /api/llm/:id/logs?limit=50&before=<id>`

Returns most recent first. Each entry has `id`, `timestamp`, `model`, `requestSummary`, `responseSummary`, `tokens`, `costCents`.

### `GET /api/llm/:id/logs/:logId`

Full conversation (prompt + completion) — encrypted at rest, decrypted in this response.

### `DELETE /api/llm/:id/logs?before=<timestamp>` or `?all=true`

Bulk delete with the same params semantics as usage/history.

---

## Audit logs (firewall events)

### `GET /api/logs?page=1&limit=50&event_type=<type>&from=<ts>&to=<ts>`

Returns events emitted by the gateway plugin: blocks, allows, errors, ip_banned, dlp_match, prompt_guard_hit, lambda_error, etc.

```json
RESPONSE 200
{
  "logs": [
    {
      "timestamp": 1714000000,
      "event_type": "string",
      "client_ip": "string",
      "mcp_uuid": "uuid?",
      "llm_uuid": "uuid?",
      "tool": "string?",
      "model": "string?",
      "rule_name": "string?",
      "details": { ... }
    }
  ],
  "total": 1234,
  "page": 1,
  "limit": 50
}
```

### `GET /api/logs/chart?bucket=hour|day&from=<ts>&to=<ts>`

Time-series counts for the dashboard chart. Returns `{ "buckets": [{ts, allow, deny, error}, ...] }`.

---

## Overview

### `GET /api/overview`

Aggregate counts: total MCPs, total LLM proxies, active rules, last 24h events, ...

### `GET /api/overview/feed?limit=20`

Recent activity entries (the same as the dashboard's right-hand feed).

---

## Resources (global)

### `GET /api/resources`

Flat list of every synthetic MCP resource the user owns, across all MCPs. Each entry includes `mcpId` and `mcpName` for context. Useful when the user asks "show me all my resources" without narrowing to one MCP.

---

## Health

### `GET /api/health`

Composite probe of postgres + redis + gateway admin. Returns `{ "overall": "operational | degraded", "services": [...] }`.

Even though this endpoint is auth-gated, it does not leak useful infra detail beyond up/down + latency.
