---
title: Coordinator REST
description: Agent lifecycle, delegation graph, and V1 façade endpoints — complete request and response reference for the Coordinator service.
pageType: api
---

The Coordinator manages agent sessions and delegation edges. It enforces depth and child limits, prevents delegation cycles, and cascades revocation through the agent subtree and delegation graph. It runs on port `4000`.

## Authentication

All routes except `/health`, `/ready`, and `POST /v1/verify` require a bearer token issued by the STS:

```
Authorization: Bearer <sts-issued-mandate>
```

The token must carry the `zone_id` claim matching the URL's `{zoneId}` parameter, and must include one of the coordinator scopes for the target operation:

| Scope | Permits |
| --- | --- |
| `coordinator.admin` | All operations |
| `coordinator.spawn_for:{appId}` | Spawn agents for the specified application |
| `coordinator.spawn_under:{appId}` | Spawn agents as children of the specified application's agents |
| `coordinator.delegate_from:{appId}` | Create delegation edges from the specified application |
| `coordinator.delegate_to:{appId}` | Accept delegation edges to the specified application |

## Error format

```json
{
  "error": "error_code",
  "message": "human-readable description"
}
```

---

## Agent lifecycle — `/v1/zones/{zoneId}/agents`

### `POST /v1/zones/{zoneId}/agents`

Spawn a new agent session. Equivalent to `POST /v1/begin`.

**Request body:**

| Field | Type | Required | Default |
| --- | --- | --- | --- |
| `application_id` | string | Yes | — |
| `subject_session_id` | string | No | STS session ID from the bearer token |
| `parent_id` | string \| null | No | `null` (root agent) |
| `kind` | `"service" \| "instance" \| "ephemeral"` | No | `"ephemeral"` |
| `capabilities` | string[] | No | `[]` |
| `ttl_seconds` | integer | No | `3600` | 1–86400 |
| `metadata` | object | No | `{}` |

**Limits:**

| Constraint | Value |
| --- | --- |
| Max depth | 10 |
| Max children per agent | 10 |
| Max agents per zone | 50 |
| Max agents per application | 200 |
| Max capabilities per agent | 32 |
| Max capability length | 64 characters |

**Validation:** `subject_session_id` must reference an active session. If `parent_id` is set, the parent must be active and the caller must own the parent's application or hold `coordinator.spawn_under:{parentAppId}`.

**Response 201:**

```json
{
  "agent_session_id": "agent-session-id",
  "zone_id": "zone1",
  "application_id": "app1",
  "parent_id": null,
  "subject_session_id": "sts-session-id",
  "kind": "ephemeral",
  "capabilities": ["browser", "code"],
  "status": "active",
  "depth": 0,
  "spawned_at": "2026-05-11T20:00:00Z",
  "terminated_at": null
}
```

**Idempotency:** If `Idempotency-Key` is provided, returns the existing agent session if one already matches `(zone, application_id, subject_session_id, parent_id)`.

**Errors:**

| Code | Status | Condition |
| --- | --- | --- |
| `subject_session_id_required` | 400 | No resolvable subject session ID |
| `application_not_found` | 404 | `application_id` does not exist |
| `session_not_found` | 404 | `subject_session_id` does not reference an active session |
| `parent_not_found` | 404 | `parent_id` does not exist |
| `application_ownership_required` | 403 | Caller does not own `application_id` and lacks spawn scope |
| `agent_zone_limit_exceeded` | 429 | 50-agent zone limit reached |
| `agent_limit_exceeded` | 429 | 200-agent per-application limit reached |
| `agent_children_limit_exceeded` | 429 | Parent has 10 children |
| `agent_depth_limit_exceeded` | 429 | Nesting depth would exceed 10 |

### `GET /v1/zones/{zoneId}/agents`

List agent sessions. Paginated.

**Query parameters:** `limit` (1–500, default 100), `cursor` (agent ID)

**Response 200:** `{ "items": [...], "next_cursor": "..." }`

### `GET /v1/zones/{zoneId}/agents/{id}`

Get a single agent session.

**Response 200:** Agent session object.  
**Error 404:** `agent_not_found`

### `GET /v1/zones/{zoneId}/agents/{id}/children`

List direct children of an agent session.

**Response 200:** `{ "items": [...], "next_cursor": "..." }`

### `PATCH /v1/zones/{zoneId}/agents/{id}/suspend`

Suspend the agent and all its descendants. Active operations may continue to completion; new operations are blocked.

**Response 200:** `{ "suspended": true }`

### `PATCH /v1/zones/{zoneId}/agents/{id}/resume`

Resume a suspended agent and its descendants.

**Response 200:** `{ "resumed": true }`

### `DELETE /v1/zones/{zoneId}/agents/{id}`

Terminate the agent and its entire subtree (all children, grandchildren, and so on). Cascades revocation to all delegation edges whose source or target is in the subtree and enqueues agent lifecycle events for each terminated session.

**Query parameters:** `reason` (string, 1–256 characters; default `"requested"`)

**Response 204.**

**Errors:**

| Code | Status | Condition |
| --- | --- | --- |
| `agent_not_found` | 404 | Agent session does not exist |
| `application_ownership_required` | 403 | Caller does not own the agent's application |

---

## Delegation graph — `/v1/zones/{zoneId}/delegations`

### `POST /v1/zones/{zoneId}/delegations`

Create a delegation edge from a source agent session to a target agent session. Equivalent to `POST /v1/exchange`.

**Request body:**

| Field | Type | Required |
| --- | --- | --- |
| `source_session_id` | string | Yes |
| `target_session_id` | string | Yes |
| `issuer_application_id` | string | Yes |
| `receiver_application_id` | string | Yes |
| `resource_id` | string \| null | No |
| `scopes` | string[] | No |
| `expires_at` | ISO 8601 datetime | Cond. — required unless `ttl_seconds` set |
| `ttl_seconds` | integer | Cond. — 1–86400; alternative to `expires_at` |
| `constraints_json` | object | No |

**`constraints_json` fields:**

| Field | Type | Description |
| --- | --- | --- |
| `ttl_seconds` | integer | Maximum lifetime of tokens issued under this edge; issued mandates are capped by this value |
| `max_hops` | integer | Maximum delegation chain depth, 1–10; default 1 |
| `max_depth` | integer | Compatibility alias for `max_hops`, 1–10 |
| `budget` | integer | Maximum scope count per token exchange |
| `resources` | string[] | Optional resource labels for policy and audit metadata |
| `policy_approved` | boolean | Optional policy marker consumed by STS policy logic |
| `expires_at` | ISO 8601 datetime | Optional constraint-side expiry used only when top-level `expires_at` is not provided |

**Validation:** `source_session_id ≠ target_session_id`. The issuer application must own the source session; the receiver application must own the target session. If `resource_id` is set, the requested `scopes` must be a subset of the resource's declared scopes. Constraint keys are closed and typed; unknown keys are rejected. The edge must not create a cycle in the delegation graph (checked via recursive CTE before insertion).

**Response 201:**

```json
{
  "id": "edge-id",
  "zone_id": "zone1",
  "source_session_id": "agent1",
  "target_session_id": "agent2",
  "issuer_application_id": "app1",
  "receiver_application_id": "app2",
  "resource_id": "resource-uuid",
  "scopes": ["read"],
  "constraints_json": { "max_hops": 1 },
  "status": "active",
  "expires_at": "2026-05-11T21:00:00Z",
  "edge_version": 0,
  "revoked_at": null,
  "created_at": "2026-05-11T20:00:00Z"
}
```

**Errors:**

| Code | Status | Condition |
| --- | --- | --- |
| `self_delegation_denied` | 400 | Source and target are the same session |
| `delegation_expiry_required` | 400 | Neither `expires_at` nor `ttl_seconds` provided |
| `delegation_expired` | 400 | `expires_at` is in the past |
| `invalid_delegation_constraint` | 400 | `constraints_json` or `constraints` contains an unknown key |
| `invalid_max_hops` | 400 | `max_hops` is outside the supported 1–10 range |
| `invalid_max_depth` | 400 | `max_depth` is outside the supported 1–10 range |
| `invalid_delegation_resources` | 400 | `resources` is not an array of non-empty strings |
| `issuer_ownership_required` | 403 | Caller does not own the issuer application |
| `resource_ownership_required` | 403 | Resource not accessible to issuer application |
| `delegation_scopes_exceed_resource` | 403 | Requested scopes exceed resource's scopes |
| `delegation_endpoint_not_found` | 404 | Source or target agent session does not exist |
| `resource_not_found` | 404 | `resource_id` does not exist |
| `delegation_cycle_denied` | 409 | Edge would create a cycle in the delegation graph |
| `delegation_application_mismatch` | 409 | Issuer or receiver application does not match the session's owning application |

### `GET /v1/zones/{zoneId}/delegations/inbound/{sessionId}`

List delegation edges where `target_session_id` equals `sessionId`.

**Query parameters:** `limit` (1–500, default 100), `cursor` (edge ID)

**Response 200:** `{ "items": [DelegationEdge, ...], "next_cursor": "..." }`

### `GET /v1/zones/{zoneId}/delegations/outbound/{sessionId}`

List delegation edges where `source_session_id` equals `sessionId`.

**Response 200:** Same structure as inbound.

### `GET /v1/zones/{zoneId}/delegations/{id}/traverse`

Traverse the delegation graph reachable from the given edge, up to 10 hops.

**Response 200:** Array of reachable edges with depth:

```json
[
  {
    "id": "edge-id",
    "source_session_id": "agent1",
    "target_session_id": "agent2",
    "depth": 1
  }
]
```

### `PATCH /v1/zones/{zoneId}/delegations/{id}/revoke`

Cascade-revoke the edge and all downstream edges reachable from it. Terminates the target agent subtrees of all revoked edges. Increments the delegation graph epoch.

**Response 200:**

```json
{
  "revoked_edges": 3,
  "affected_sessions": 5,
  "terminated_agents": 2
}
```

**Errors:**

| Code | Status | Condition |
| --- | --- | --- |
| `delegation_not_found` | 404 | Edge does not exist |
| `issuer_ownership_required` | 403 | Caller does not own the issuer application |

---

## V1 façade

These flat-path routes are aliases for the zone-scoped routes above. They accept all the same fields, with `zone_id` moved into the request body.

### `POST /v1/begin`

Alias for `POST /v1/zones/{zoneId}/agents`. Include `zone_id` in the request body.

**Additional required field:** `zone_id` (string)

**Response:** Identical to the agent spawn response.

### `POST /v1/end`

Alias for `DELETE /v1/zones/{zoneId}/agents/{id}`.

**Request body:**

| Field | Type | Required |
| --- | --- | --- |
| `zone_id` | string | Yes |
| `agent_session_id` | string | Yes | Agent session ID to terminate |
| `reason` | string | No |

**Response 204.**

### `POST /v1/exchange`

Alias for `POST /v1/zones/{zoneId}/delegations`. Include `zone_id` in the request body.

**Additional required field:** `zone_id` (string)

**Response:** Identical to the delegation edge creation response.

---

## Token verification — `POST /v1/verify`

Verify a Caracal mandate without requiring authentication. Useful for debugging and for services that want to validate a token before forwarding it.

**Request body:**

| Field | Type | Description |
| --- | --- | --- |
| `token` | string | JWT to verify directly |
| `authorization` | string | `"Bearer <token>"` — alternative to `token` |
| `zone_id` | string | If set, verify the token's `zone_id` claim matches |
| `required_scope` | string | If set, verify this scope is present in the `scope` claim |
| `require_agent` | boolean | If true, verify `agent_session_id` claim is present |
| `require_delegation` | boolean | If true, verify `delegation_edge_id` claim is present |

**Response 200 (valid):**

```json
{
  "valid": true,
  "claims": { "sub": "...", "zone_id": "...", "scope": "..." }
}
```

**Response 401 (invalid):**

```json
{
  "valid": false,
  "error": "token_expired",
  "message": "token has expired"
}
```

**Response 429 (rate limited):**

```json
{
  "valid": false,
  "error": "rate_limited"
}
```

---

## Health and readiness

| Method | Path | Description |
| --- | --- | --- |
| `GET` | `/health` | Liveness; always `200 {"ok": true}` |
| `GET` | `/ready` | Readiness; `200` when database and Redis are healthy |
| `GET` | `/metrics` | JSON counters for agent lifecycle and delegation operations |
