> ## Documentation Index
> Fetch the complete documentation index at: https://docs.visiqlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API Reference

> Complete REST API reference for the delegation-governance endpoints — the agent-to-agent grant lifecycle and per-action enforcement.

Delegation-governance endpoints are mounted under `/orchestrate/*`. The base URL is `https://api.visiqlabs.com`.

These endpoints govern agent-to-agent handoffs: a parent agent creates a scoped grant, the named child accepts it and receives a signed token, and the child evaluates each delegated action against that grant. The decision vocabulary here is `permit` and `deny` — a delegated action is either within the grant's intersected authority or it is not.

## Authentication

All endpoints require a Bearer credential: `Authorization: Bearer <key>`. Requests without a valid credential receive `401 Unauthorized`.

The whole `/orchestrate/*` surface is **management-audience**. A harness key (`vq_prod_...` / `vq_test_...` minted under **Settings → Harness Keys**, or an `allow_...` agent key) is confined to the SDK operational routes and receives `403 {"error": "harness_key_not_permitted"}` here — delegation is driven by a management key or a dashboard session. Management keys are **launching soon**: they are visible in the dashboard under **Settings → API Keys**, but creating one is not yet enabled. Until then, drive these endpoints from the dashboard, which authenticates with your session.

Scoped keys are additionally checked against their granted scopes. Every endpoint on this page requires `allow:write` (the read-only `GET /orchestrate/grants/:id` also accepts `allow:read`); `full_access` satisfies everything. A key without the required scope receives `403 {"error": "insufficient_scope"}` listing the required and granted scopes.

### Rate limiting

Every API-key request passes a per-key sliding-window rate limit (default 600 requests per 60 seconds). Responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining` headers; exceeding the window returns `429` with a `Retry-After` header and body `{"error": "rate_limited", "detail": "API key rate limit exceeded.", "retryAfter": <seconds>}`.

***

## Grant lifecycle

***

### POST /orchestrate/grants

Create a delegation from a parent agent to a child agent. Both agents must be registered for your organization. The platform runs a policy check on the delegation itself, intersects the requested scopes against the parent's authority, writes a synchronous record proof, and returns a `pending` grant awaiting acceptance.

**Scope:** `allow:write`

**Request body:**

```json theme={null}
{
  "parent_agent_id": "orchestrator",
  "child_agent_id": "research-worker",
  "tool_scope": ["search_web", "read_document"],
  "context_scope": ["public", "internal"],
  "purpose": "Gather background for the Q3 report",
  "duration_seconds": 3600,
  "max_depth": 3
}
```

| Field                | Type                 | Required | Description                                                                                                                                                                   |
| -------------------- | -------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `parent_agent_id`    | `string`             | Yes      | The delegating agent — must be registered (1–255 chars)                                                                                                                       |
| `child_agent_id`     | `string`             | Yes      | The agent receiving authority — must be registered (1–255 chars)                                                                                                              |
| `tool_scope`         | `string[]` \| `null` | No       | Actions the child may perform. `null` (default) inherits the parent's tool authority; `[]` is deny-all; a list is the exact set. Intersected with the parent's own tool scope |
| `context_scope`      | `string[]` \| `null` | No       | Resource types the child may touch. Same `null` / `[]` / list semantics as `tool_scope`, intersected with the parent's context scope                                          |
| `purpose`            | `string`             | No       | Human-readable reason for the handoff (max 2000 chars) — recorded on the grant                                                                                                |
| `duration_seconds`   | `number`             | No       | Lifetime of the token issued at acceptance, `1`–`604800` (7 days). Default `3600`                                                                                             |
| `max_depth`          | `number`             | No       | Maximum chain depth for onward sub-delegation, `1`–`16`. Default `3`. Ignored when sub-delegating (the root grant's ceiling governs the whole chain)                          |
| `parent_grant_token` | `string`             | No       | A grant token the parent itself holds — present when **sub-delegating**. The parent must be that token's child agent, and the new grant's authority is intersected against it |

<Note>
  When `parent_grant_token` is supplied, the delegating `parent_agent_id` must equal the `child_agent_id` of that token's grant — you can only sub-delegate authority you were actually granted. The new grant's depth is the parent grant's depth plus one, and its `max_depth` is inherited from the parent grant.
</Note>

**Response (200):**

```json theme={null}
{
  "grant_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "pending",
  "effective_tool_scope": ["search_web", "read_document"],
  "effective_context_scope": ["public", "internal"],
  "accept_deadline": "2026-07-03T10:35:00Z"
}
```

| Field                     | Type                 | Description                                                                                             |
| ------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------- |
| `grant_id`                | `string` (UUID)      | The grant's id — used to accept, poll, and revoke                                                       |
| `status`                  | `string`             | Always `pending` on creation                                                                            |
| `effective_tool_scope`    | `string[]` \| `null` | The **intersected** tool authority actually conferred (parent ∩ requested)                              |
| `effective_context_scope` | `string[]` \| `null` | The intersected context authority conferred                                                             |
| `accept_deadline`         | `string` (ISO 8601)  | The child must accept before this time (5 minutes from creation) or the grant can no longer be accepted |

**Decision & failure semantics:**

* If no rule permits `orchestrate.delegate` for the parent and the parent is in enforce mode, the delegation is denied fail-closed: `403 {"error": "Delegation denied by policy", "reason": "..."}`.
* If the synchronous record proof cannot be written, the just-created grant is revoked and the call returns `500 {"error": "Failed to write ORCHESTRATE proof record"}`.

**Status codes:** `200 OK`, `400 Bad Request` (invalid body), `401 Unauthorized` (missing credential, or invalid `parent_grant_token`), `403 Forbidden` (insufficient scope, delegation denied by policy, or the parent grant does not belong to the delegating agent), `404 Not Found` (parent or child agent not registered), `422 Unprocessable Entity` (delegation depth ceiling reached), `500 Internal Server Error`

**Example:**

```bash theme={null}
curl -X POST https://api.visiqlabs.com/orchestrate/grants \
  -H "Authorization: Bearer $VISIQ_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "parent_agent_id": "orchestrator",
    "child_agent_id": "research-worker",
    "tool_scope": ["search_web", "read_document"],
    "purpose": "Gather background for the Q3 report"
  }'
```

***

### GET /orchestrate/grants/:id

Fetch a grant's current status — the parent polls this to learn whether the child has accepted, and to read the effective scopes and lifecycle timestamps.

**Scope:** `allow:write` or `allow:read`

**Path parameter:** `:id` — the grant UUID (from `POST /orchestrate/grants`)

**Response (200):**

```json theme={null}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "active",
  "parent_agent_id": "orchestrator",
  "child_agent_id": "research-worker",
  "depth": 0,
  "max_depth": 3,
  "effective_tool_scope": ["search_web", "read_document"],
  "effective_context_scope": ["public", "internal"],
  "purpose": "Gather background for the Q3 report",
  "accept_deadline": "2026-07-03T10:35:00Z",
  "created_at": "2026-07-03T10:30:00Z",
  "accepted_at": "2026-07-03T10:31:00Z",
  "expires_at": "2026-07-03T11:31:00Z",
  "revoked_at": null,
  "revocation_reason": null
}
```

`status` is one of `pending`, `active`, `revoked`, or `expired`. The grant token itself is never returned here — it is issued once, at acceptance.

**Status codes:** `200 OK`, `401 Unauthorized`, `403 Forbidden` (insufficient scope), `404 Not Found`, `500 Internal Server Error`

***

### POST /orchestrate/grants/:id/accept

The child agent accepts a pending grant and receives its signed grant token. Bilateral identity: the child declares itself in the `X-Agent-ID` header, which must match the grant's `child_agent_id`.

**Scope:** `allow:write`

**Request headers:**

| Header       | Required | Description                                                                                       |
| ------------ | -------- | ------------------------------------------------------------------------------------------------- |
| `X-Agent-ID` | Yes      | The accepting child agent's id. Must equal the grant's `child_agent_id` and be a registered agent |

**Path parameter:** `:id` — the grant UUID

This endpoint takes no request body. Acceptance is guarded against a concurrent double-accept: only the caller that flips the grant from `pending` to `active` wins; a lost race returns `409`.

**Response (200):**

```json theme={null}
{
  "grant_token": "eyJ...signed-token...",
  "tool_scope": ["search_web", "read_document"],
  "context_scope": ["public", "internal"],
  "expires_at": "2026-07-03T11:31:00Z"
}
```

| Field           | Type                 | Description                                                                                                                                                                                   |
| --------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `grant_token`   | `string`             | The signed token the child presents on every `POST /orchestrate/evaluate`. Bound to this child and grant; expires at `expires_at`. Returned **only here** — store it; it cannot be re-fetched |
| `tool_scope`    | `string[]` \| `null` | The effective tool authority the token carries                                                                                                                                                |
| `context_scope` | `string[]` \| `null` | The effective context authority the token carries                                                                                                                                             |
| `expires_at`    | `string` (ISO 8601)  | Token expiry, computed from the grant's `duration_seconds`                                                                                                                                    |

<Warning>
  The `grant_token` is present in this response only. Subsequent reads never return it. If a record proof write fails after the token is issued, the acceptance is rolled back to `pending` and the call returns `500` — the token you received in that case is not valid.
</Warning>

**Status codes:** `200 OK`, `400 Bad Request` (missing `X-Agent-ID`), `401 Unauthorized`, `403 Forbidden` (insufficient scope, `X-Agent-ID` does not match the grant's child, or the claimed agent is not registered), `404 Not Found` (unknown grant), `409 Conflict` (grant not pending, accept window expired, or already accepted), `500 Internal Server Error`

***

### POST /orchestrate/grants/:id/revoke

Revoke a grant. Revocation cascades eagerly to every descendant grant in one atomic step, and is checked at enforcement time, so there is no revocation lag.

**Scope:** `allow:write`

**Path parameter:** `:id` — the grant UUID

**Request body:**

```json theme={null}
{
  "reason": "Task complete — tearing down the delegation chain"
}
```

| Field    | Type     | Required | Description                                                                 |
| -------- | -------- | -------- | --------------------------------------------------------------------------- |
| `reason` | `string` | No       | Why the grant is being revoked (max 2000 chars) — recorded in the audit log |

**Response (200):**

```json theme={null}
{
  "revoked_count": 3
}
```

`revoked_count` is the number of grants invalidated — the target plus every descendant in its subtree.

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `403 Forbidden` (insufficient scope), `404 Not Found`, `422 Unprocessable Entity` (grant already revoked), `500 Internal Server Error`

***

## Per-action enforcement

***

### POST /orchestrate/evaluate

The child agent's per-action check. For each action it takes under a grant, the child presents its grant token; the platform verifies the token, walks the ancestor chain, checks the action against the grant's intersected tool and context scopes, records a handoff event, and returns the decision.

**Scope:** `allow:write`

**Request body:**

```json theme={null}
{
  "grant_token": "eyJ...signed-token...",
  "action": "search_web",
  "resource_type": "public",
  "resource_metadata": {
    "query": "Q3 market outlook"
  }
}
```

| Field               | Type               | Required | Description                                                                                 |
| ------------------- | ------------------ | -------- | ------------------------------------------------------------------------------------------- |
| `grant_token`       | `string`           | Yes      | The token issued at acceptance                                                              |
| `action`            | `string`           | Yes      | The action the child is attempting — checked against the grant's tool scope (1–255 chars)   |
| `resource_type`     | `string` \| `null` | No       | The resource type being touched — checked against the grant's context scope (max 255 chars) |
| `resource_metadata` | `object`           | No       | Opaque metadata recorded with the handoff event. Default `{}`                               |

**Response (200):**

```json theme={null}
{
  "decision": "permit",
  "reason": "Action within grant scope",
  "handoff_event_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}
```

| Field              | Type               | Description                                                                                                                      |
| ------------------ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `decision`         | `permit` \| `deny` | Whether the action is within the grant's intersected authority and the whole ancestor chain is valid                             |
| `reason`           | `string`           | Human-readable explanation — e.g. an out-of-scope action names which scope it fell outside, and a revoked ancestor is called out |
| `handoff_event_id` | `string` (UUID)    | The recorded handoff event — the audit trail **is** the permit (a handoff that cannot be recorded is never permitted)            |

**Decision & failure semantics:**

* A token whose signature is invalid, whose vendor does not match the caller, that references an unknown grant, or that no longer matches the grant's stored token returns `401 {"error": "Invalid grant token"}`.
* An expired token or a grant that is not `active` returns `403` (`Grant expired` / `Grant is <status>`).
* A revoked or invalid ancestor grant yields `decision: "deny"` with the reason naming the broken link — a `200` response with a deny, not an error.
* An action outside the grant's tool scope, or a resource type outside its context scope, yields `decision: "deny"`.
* If the handoff event cannot be recorded, the call returns `500 {"error": "Failed to record handoff event"}` — never a silent permit.

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized` (invalid or mismatched grant token), `403 Forbidden` (insufficient scope, expired token, or inactive grant), `500 Internal Server Error`

***

## Errors & conventions

Every endpoint on this page follows the platform-wide REST conventions — the validation-error body shape, the `/v1/` vs unversioned split, and the API stability policy. See [REST API conventions](/reference/rest-conventions).
