# Agents

Device sign-in, approvals, and a key acting on itself.

These are the calls an AI assistant or a command-line tool makes about
*itself*: signing in, checking whether a person has approved something, and
handing its own key back. None of them reads or changes anything in a
workspace.

## Signing in without pasting a key

A person should never have to copy a key out of a dashboard and into a config
file. `POST /v1/auth/device` is the alternative, and it inverts who holds the
secret:

1. **Your program makes the key.** Thirty-two random bytes, rendered as 43
   base62 characters, with `bc_live_` in front. Keep it somewhere only you can
   read.
2. **You send its hash.** `key_hash` is the SHA-256 of the whole string;
   `key_prefix` is the first 8 characters after `bc_live_`, which is what a
   person sees in their list of connections. The key itself has no field to
   travel in.
3. **A person approves it** at `boltclaw.ai/auth/device`, choosing the
   workspace and ticking the permissions they are willing to give.
4. **You poll** `POST /v1/auth/device/poll` every `interval` seconds until it
   answers `approved`. Then the key you already hold starts working.

`denied` and `expired` end it; start again. A `device_code` that was never
issued, or has been cleared away, answers `expired` exactly like one that
lapsed, so polling cannot be used to find out which codes exist.

The approval names the workspace the person chose, by id and name. That is one
of the very few answers in this API that carries a workspace id, and it is
answered only to the holder of the `device_code`: the program that started the
sign-in, whose new key belongs to that workspace.

So between the approval and your next poll there is no credential of ours to
lose, and a stolen `device_code` reveals one thing: whether the request was
approved.

**Do not try the key before the poll says `approved`.** Until a person decides,
that key does not exist, and it is refused exactly like any other key that does
not — which the API remembers for five minutes, to make guessing keys
expensive. A client that probes early therefore waits out those five minutes
after the approval before anything works. Poll, and use the key once the poll
tells you to.

Ask for the permissions you need and no more. The person sees the list, can
untick any of it, and whatever survives is narrowed again to what they
themselves may grant. Permissions an assistant may never hold — managing API
keys, managing webhooks — are refused at step 1, not silently dropped at
step 3.

## Approvals

Some calls stop and ask. If you make one, you get a `428` whose body carries
an `approval` object:

```json
{
  "error": "Nothing has been done. A person must approve this action first...",
  "code": "APPROVAL_REQUIRED",
  "request_id": "...",
  "approval": {
    "id": "0c4b...",
    "url": "https://boltclaw.ai/dashboard/approvals/0c4b...",
    "expires_at": "2026-10-20T09:12:44.000Z",
    "status": "pending"
  }
}
```

**Nothing has happened.** Show the `url` to the person. When they approve it,
send the *identical* request again with the id in the `BoltClaw-Approval`
header. While you wait, `GET /v1/approvals/{id}` tells you where it has got to,
and it is cheap enough to poll.

The approval is bound to that exact request *and* to the state it acts on, so
one approved for deleting a page cannot be spent on a different page, or on the
same page after somebody renamed it. It is single use and it lapses after 24
hours.

Which calls ask, and whether an organisation can decide to stop being asked,
is on [Authentication and keys](https://boltclaw.ai/docs/authentication#what-an-assistant-must-ask-about).

## Keeping a key alive, and ending it

A key approved for an assistant lasts 90 days. `POST /v1/auth/key/extend`
pushes that out by another 90, up to a year from the day it was issued, and
only within the last 30 days of its life — so calling it on a schedule is safe
and usually answers `extended: false`. After a year, a person signs in again.

`POST /v1/auth/key/revoke` ends the calling key. Use it when somebody
disconnects your program, and when you uninstall: deleting a key from a
configuration file leaves it working, and this does not. It works even when an
organisation has turned assistant access off, because handing a credential back
must never be the thing you cannot do.

Both act only on the key that made the call, so neither needs a scope.

## Operations

### `POST /v1/auth/device`

**Start a device sign-in**

No scope: any valid key.

How an AI assistant or a command-line tool gets a key, without anybody pasting one.

**Your program generates the key.** Make 32 random bytes, render them as 43 base62
characters, prefix them with `bc_live_`, and send only the SHA-256 of the whole string
plus the first 8 characters of the random body. The key itself must never leave the
machine it was made on, and there is no field here that would carry it. BoltClaw stores
the hash, exactly as it stores every other key's, so no secret of ours exists at any
moment, not even between the approval and your next poll.

You get back a short code and a URL. Show both to the person, poll
`POST /v1/auth/device/poll` every `interval` seconds, and once they approve, the key you
already hold starts working.

Ask for the permissions you need and no more: the person sees them, can untick any of
them, and the key is narrowed again to what they themselves may grant. Permissions an
assistant may never hold, such as managing API keys or webhooks, are refused here rather
than silently dropped later.

This call needs no API key, and it is limited per address rather than per key.

**Request body** (required)

| Field | Type |  |
|---|---|---|
| `client_name` | `string` | What you are called, shown to the person deciding. Never trusted for anything. (**required**; min 1; max 80) |
| `client_host` | `string` | The machine you are running on, shown beside the name so a person can tell two terminals apart. (min 1; max 120) |
| `scopes` | `Scope[]` | The permissions you are asking for. Ask for what you need and no more. (**required**; <= 25 items; >= 1 items) |
| `key_hash` | `string` | (**required**) |
| `key_prefix` | `string` | (**required**) |
| `flow` | `string` | `device` for a program somebody is sitting at. `oauth` is the hosted BoltClaw MCP server acting for a person, and only it may ask for one. (default `device`; one of `device`, `oauth`) |
| `redirect_host` | `string` | Only with `flow: oauth`. The host the person will be sent back to, shown to them before they approve. (min 1; max 253) |

```json
{
  "client_name": "<client_name>",
  "scopes": [],
  "key_hash": "<key_hash>",
  "key_prefix": "<key_prefix>"
}
```

**Answers**

| Status | Body |  |
|---|---|---|
| 201 | `DeviceAuthorization` | The request is waiting for a person. |
| 400 | `ErrorEnvelope` | `VALIDATION_ERROR`: a body that is not JSON, a field that breaks its rule (named in `fields`), a bad query value or cursor, or a malformed `Idempotency-Key`. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 409 | `ErrorEnvelope` | `CONFLICT`: the request clashes with the object's current state (the message says how). `IDEMPOTENCY_KEY_CONFLICT` / `IDEMPOTENCY_KEY_IN_FLIGHT`: the `Idempotency-Key` was used with another request, or its first request is still running. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

### `POST /v1/auth/device/poll`

**Poll a device sign-in**

No scope: any valid key.

Has a person decided yet? Send the `device_code` from `POST /v1/auth/device`, no more
often than `interval` seconds apart.

`status` is `pending` while you wait, `slow_down` if you are asking too fast (wait
`interval` seconds and carry on), `approved` once a person has said yes, `denied` if they
said no, and `expired` if nobody decided in time. A code that was never issued, or has been
cleared away since, answers `expired` too, exactly like one that lapsed: the answer never
tells you whether a code existed. Either way, start a new sign-in.

An approval answers with the key's id, its prefix, the permissions it ended up with and
when it expires. It never answers with a key: you made it, so you already have it.

It also names the workspace the person chose, by id and name. Apart from `GET /workspace`
and one route reserved for the BoltClaw MCP server, this is the only answer in the API that
carries a workspace id, and it goes only to the holder of the device code: the program
that started the sign-in, which is about to hold a key for that workspace and could read
the same id with it.

This call needs no API key.

**Request body** (required)

| Field | Type |  |
|---|---|---|
| `device_code` | `string` | (**required**; min 16; max 200) |

```json
{
  "device_code": "<device_code>"
}
```

**Answers**

| Status | Body |  |
|---|---|---|
| 200 | `DeviceAuthorizationStatus` | Where the sign-in has got to. |
| 400 | `ErrorEnvelope` | `VALIDATION_ERROR`: a body that is not JSON, a field that breaks its rule (named in `fields`), a bad query value or cursor, or a malformed `Idempotency-Key`. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

### `GET /v1/approvals/{id}`

**Read an approval you asked for**

No scope: any valid key. Another workspace's answers `404`.

Has the person decided yet? This is the one thing a key may read about an approval, and
only about its own: an approval another key asked for is `404`, exactly like one that
never existed.

You get the status and the deadline, not the summary: the summary was written for the
person who decides, and you already know what you asked for. A `pending` or `approved`
ticket past its deadline reads as `expired`.

Cheap and safe to poll while you wait, and left out of the agent call log so that waiting
does not bury the calls that did something. Needs no scope.

**Parameters**

| Name | In | Type |  |
|---|---|---|---|
| `id` | path | `uuid` | An id this API gave you. Anything that is not one of yours is `404`. (**required**) |

**Answers**

| Status | Body |  |
|---|---|---|
| 200 | `ApprovalStatus` | The approval, as it stands. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 404 | `ErrorEnvelope` | `NOT_FOUND`: no such object, or it belongs to another workspace. The two are deliberately the same answer and never 403. If the id is right, check you are using the right key. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

### `POST /v1/auth/key/extend`

**Extend the key you are using**

No scope: any valid key. Accepts `Idempotency-Key`.

A key approved for an AI assistant lasts 90 days. Within 30 days of its expiry it can push
that out by another 90, up to a year from the day it was issued, and then a person signs
in again.

Nothing else can be extended this way: a key a person created in the dashboard has an
expiry they chose, and this call will not second-guess it. The extension also stops if the
person who approved the connection can no longer approve it, or if the organisation has
turned AI assistant access off, which is what makes it safe to let a credential renew
itself.

Acts only on the key that made the call, so it needs no scope. Safe to call on a schedule:
outside the 30-day window it answers `extended: false` and changes nothing.

**Answers**

| Status | Body |  |
|---|---|---|
| 200 | `KeyExtendResult` | Whether it moved, and when it now expires. |
| 400 | `ErrorEnvelope` | `VALIDATION_ERROR`: a body that is not JSON, a field that breaks its rule (named in `fields`), a bad query value or cursor, or a malformed `Idempotency-Key`. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 409 | `ErrorEnvelope` | `CONFLICT`: the request clashes with the object's current state (the message says how). `IDEMPOTENCY_KEY_CONFLICT` / `IDEMPOTENCY_KEY_IN_FLIGHT`: the `Idempotency-Key` was used with another request, or its first request is still running. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

### `POST /v1/auth/key/revoke`

**Revoke the key you are using**

No scope: any valid key. Accepts `Idempotency-Key`.

Sign out, for real. Deleting a key from a configuration file leaves it working; this ends
it. Use it when a person disconnects your program, and when you are uninstalling.

Acts only on the key that made the call, so it needs no scope, and it works even when the
organisation has turned AI assistant access off: handing a credential back must never be
the thing you cannot do. It cannot be undone, and it is safe to repeat.

If the key is the root of a connection that spans several workspaces, every key in that
connection ends with it.

**Answers**

| Status | Body |  |
|---|---|---|
| 200 | `KeyRevokeResult` | The key is revoked. |
| 400 | `ErrorEnvelope` | `VALIDATION_ERROR`: a body that is not JSON, a field that breaks its rule (named in `fields`), a bad query value or cursor, or a malformed `Idempotency-Key`. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

### `POST /v1/auth/key/revoke-oauth`

**End a connection**

No scope: any valid key.

Ends a whole AI connection, from its root, so no workspace is left reachable. Reserved
for the BoltClaw MCP server; anything else is `404`.

It takes a key ID rather than a key, because the MCP server reaches it exactly when it no
longer holds one: a person signing the assistant out of their client revokes the grant
whose encrypted store was the only copy of the key. The answer is the same for a key id
that was a connection's root and for one that was never anything, so it tells a caller
nothing about which ids exist.

This call needs no API key.

**Request body** (required)

| Field | Type |  |
|---|---|---|
| `key_id` | `uuid` | The id of any key in the connection. Its root, and every sibling, end with it. (**required**) |

```json
{
  "key_id": "3f8c1e42-9b7a-4d61-a0f5-2c9e1b7d4a83"
}
```

**Answers**

| Status | Body |  |
|---|---|---|
| 200 | `KeyRevokeResult` | The connection is revoked. |
| 400 | `ErrorEnvelope` | `VALIDATION_ERROR`: a body that is not JSON, a field that breaks its rule (named in `fields`), a bad query value or cursor, or a malformed `Idempotency-Key`. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 404 | `ErrorEnvelope` | `NOT_FOUND`: no such object, or it belongs to another workspace. The two are deliberately the same answer and never 403. If the id is right, check you are using the right key. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

### `GET /v1/auth/key/workspaces`

**List a connection's workspaces**

No scope: any valid key.

Which workspaces this connection reaches today. Re-resolved on every call against the live
permissions of the person who approved it, so a workspace they can no longer reach drops
out with nothing to revoke. Reserved for the BoltClaw MCP server; anything else is `404`.

**Answers**

| Status | Body |  |
|---|---|---|
| 200 | `ConnectionWorkspaceList` | The workspaces, and which already have a key. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 404 | `ErrorEnvelope` | `NOT_FOUND`: no such object, or it belongs to another workspace. The two are deliberately the same answer and never 403. If the id is right, check you are using the right key. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

### `POST /v1/auth/key/workspaces`

**Register a connection's workspace**

No scope: any valid key. Accepts `Idempotency-Key`.

Registers the key for one workspace of a connection, from a hash the caller derived and a
prefix to display. Creates nothing if that workspace already has a live key, and answers
the one it has. Reserved for the BoltClaw MCP server; anything else is `404`.

**Request body** (required)

| Field | Type |  |
|---|---|---|
| `workspace_id` | `uuid` | (**required**) |
| `key_hash` | `string` | (**required**) |
| `key_prefix` | `string` | (**required**) |

```json
{
  "workspace_id": "3f8c1e42-9b7a-4d61-a0f5-2c9e1b7d4a83",
  "key_hash": "<key_hash>",
  "key_prefix": "<key_prefix>"
}
```

**Answers**

| Status | Body |  |
|---|---|---|
| 200 | `ConnectionWorkspaceKey` | That workspace already had a key. |
| 201 | `ConnectionWorkspaceKey` | Registered. |
| 400 | `ErrorEnvelope` | `VALIDATION_ERROR`: a body that is not JSON, a field that breaks its rule (named in `fields`), a bad query value or cursor, or a malformed `Idempotency-Key`. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 404 | `ErrorEnvelope` | `NOT_FOUND`: no such object, or it belongs to another workspace. The two are deliberately the same answer and never 403. If the id is right, check you are using the right key. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

## Shapes

The objects these operations send and answer.

### ApprovalAction

What an assistant asked to do. The actions in the first group are the ones an organisation
may decide to stop being asked about, or waive for one connection for a few hours; the
rest are always asked, because they move somebody else's money or change somebody else's
access.

One of: `post.publish`, `post.delete`, `inbox.reply`, `inbox.hide`, `conversation.reply`, `automation.enable`, `automation.delete`, `page.delete`, `page.unpublish`, `link.delete`, `lead.delete`, `referral.payout.paid`, `referral.commission.adjust`, `referral.partner.approve`, `social.account.disconnect`, `referral.commission.bonus`, `referral.reward.delete`, `referral.group.delete`, `referral.referrer.erase`, `referral.grant.fulfil`, `referral.grant.fail`, `referral.stripe.codes`, `referral.testdata.clear`, `referral.flag.dismiss`, `referral.flag.confirm`, `referral.risk.update`, `referral.payout.send`, `referral.payout.settings`, `referral.commission.forfeit`, `referral.tax.settings`, `referral.brand.update`, `referral.domain.add`, `referral.domain.remove`, `referral.listing.update`, `referral.library.delete`, `referral.broadcast.send`, `referral.import.commit`, `referral.contest.award`, `referral.contest.cancel`, `referral.waitlist.disqualify`, `referral.waitlist.invite`, `referral.tier.update`, `referral.waitlist.end`, `referral.waitlist.forget`, `referral.shopify.codes`, `referral.terms.update`, `referral.subject.erase`, `referral.program.archive`, `referral.partner.ban`, `referral.partner.unban`, `referral.commission.approve`, `referral.code.create`, `referral.link.archive`, `referral.program.restore`, `referral.signup.rotate`, `referral.waitlist.rotate`, `referral.embed.rotate`, `referral.referrer.approve`, `referral.code.link`, `referral.payout.reopen`.

### ApprovalStatus

| Field | Type |  |
|---|---|---|
| `id` | `uuid` | (**required**) |
| `action` | `ApprovalAction` | What an assistant asked to do. The actions in the first group are the ones an organisation may decide to stop being asked about, or waive for one connection for a few hours; the rest are always asked, because they move somebody else's money or change somebody else's access. (**required**; one of `post.publish`, `post.delete`, `inbox.reply`, `inbox.hide`, `conversation.reply`, `automation.enable`, `automation.delete`, `page.delete`, `page.unpublish`, `link.delete`, `lead.delete`, `referral.payout.paid`, `referral.commission.adjust`, `referral.partner.approve`, `social.account.disconnect`, `referral.commission.bonus`, `referral.reward.delete`, `referral.group.delete`, `referral.referrer.erase`, `referral.grant.fulfil`, `referral.grant.fail`, `referral.stripe.codes`, `referral.testdata.clear`, `referral.flag.dismiss`, `referral.flag.confirm`, `referral.risk.update`, `referral.payout.send`, `referral.payout.settings`, `referral.commission.forfeit`, `referral.tax.settings`, `referral.brand.update`, `referral.domain.add`, `referral.domain.remove`, `referral.listing.update`, `referral.library.delete`, `referral.broadcast.send`, `referral.import.commit`, `referral.contest.award`, `referral.contest.cancel`, `referral.waitlist.disqualify`, `referral.waitlist.invite`, `referral.tier.update`, `referral.waitlist.end`, `referral.waitlist.forget`, `referral.shopify.codes`, `referral.terms.update`, `referral.subject.erase`, `referral.program.archive`, `referral.partner.ban`, `referral.partner.unban`, `referral.commission.approve`, `referral.code.create`, `referral.link.archive`, `referral.program.restore`, `referral.signup.rotate`, `referral.waitlist.rotate`, `referral.embed.rotate`, `referral.referrer.approve`, `referral.code.link`, `referral.payout.reopen`) |
| `status` | `string` | (**required**; one of `pending`, `approved`, `denied`, `used`, `expired`) |
| `expires_at` | `date-time \| null` | (**required**) |
| `decided_at` | `date-time \| null` | (**required**) |

### ConnectionRevoke

| Field | Type |  |
|---|---|---|
| `key_id` | `uuid` | The id of any key in the connection. Its root, and every sibling, end with it. (**required**) |

### ConnectionWorkspaceKey

| Field | Type |  |
|---|---|---|
| `key_id` | `uuid` | (**required**) |
| `created` | `boolean` | False when that workspace already had a live key. (**required**) |
| `scopes` | `Scope[]` | (**required**) |

### ConnectionWorkspaceList

Answered only to the BoltClaw MCP server, which keeps one key per workspace of a connection
and needs each workspace's id to do it. Anything else asking is answered `404`.

| Field | Type |  |
|---|---|---|
| `connection_id` | `uuid` | (**required**) |
| `all_workspaces` | `boolean` | The connection follows the organisation, including workspaces made after it was approved. (**required**) |
| `data` | `object[]` | (**required**) |
| `next_cursor` | `NextCursor` | Pass back as `cursor` for the next page; null on the last page. (**required**) |

### ConnectionWorkspaceRegister

| Field | Type |  |
|---|---|---|
| `workspace_id` | `uuid` | (**required**) |
| `key_hash` | `string` | (**required**) |
| `key_prefix` | `string` | (**required**) |

### DeviceAuthorization

| Field | Type |  |
|---|---|---|
| `device_code` | `string` | Your half of the exchange. Keep it, poll with it, and never show it to anybody. (**required**) |
| `user_code` | `string` | What the person types, as `XXXX-XXXX`. Show it to them. (**required**) |
| `verification_uri` | `uri` | (**required**) |
| `verification_uri_complete` | `uri` | The same page with the code filled in. Use it when you can show a link or a QR code. (**required**) |
| `expires_in` | `integer` | Seconds until the request lapses. (**required**) |
| `expires_at` | `date-time \| null` | (**required**) |
| `interval` | `integer` | Seconds to wait between polls. Asking faster is answered `slow_down`. (**required**) |

### DeviceAuthorizationPoll

| Field | Type |  |
|---|---|---|
| `device_code` | `string` | (**required**; min 16; max 200) |

### DeviceAuthorizationStart

The key is made on your machine and never sent. `key_hash` is the SHA-256 of the whole
`bc_live_...` string, as 64 lowercase hex characters; `key_prefix` is the first 8
characters of the random body after the prefix, and is what a person sees in the list of
connections.

| Field | Type |  |
|---|---|---|
| `client_name` | `string` | What you are called, shown to the person deciding. Never trusted for anything. (**required**; min 1; max 80) |
| `client_host` | `string` | The machine you are running on, shown beside the name so a person can tell two terminals apart. (min 1; max 120) |
| `scopes` | `Scope[]` | The permissions you are asking for. Ask for what you need and no more. (**required**; <= 25 items; >= 1 items) |
| `key_hash` | `string` | (**required**) |
| `key_prefix` | `string` | (**required**) |
| `flow` | `string` | `device` for a program somebody is sitting at. `oauth` is the hosted BoltClaw MCP server acting for a person, and only it may ask for one. (default `device`; one of `device`, `oauth`) |
| `redirect_host` | `string` | Only with `flow: oauth`. The host the person will be sent back to, shown to them before they approve. (min 1; max 253) |

### DeviceAuthorizationStatus

`pending` while you wait, `slow_down` if you are asking too fast, `approved` once a person
has said yes, `denied` if they said no, `expired` if nobody decided in time. A code that
was never issued answers `expired` as well, so the answer never says whether one existed.

| Field | Type |  |
|---|---|---|
| `status` | `string` | (**required**; one of `pending`, `slow_down`, `approved`, `denied`, `expired`) |
| `interval` | `integer` | Only with `slow_down`. Wait this many seconds. |
| `key` | `object` | Only with `approved`. Never the key itself, which you already hold. |
| `workspace` | `object` | Only with `approved`. The workspace the person chose. One of the few answers that carries a workspace id, because it goes only to the holder of the device code: the program that started the sign-in, whose new key belongs to this workspace. |
| `callback_nonce_hash` | `string` | Only for the `oauth` flow, and only to the client that started it. |

### KeyExtendResult

| Field | Type |  |
|---|---|---|
| `extended` | `boolean` | False when it was not yet near enough to its expiry, or is already at its limit. (**required**) |
| `expires_at` | `date-time \| null` | (**required**) |
| `reason` | `string` | Why it did not move, when it did not. |

### KeyRevokeResult

| Field | Type |  |
|---|---|---|
| `revoked` | `true` | (**required**) |

### NextCursor

Pass back as `cursor` for the next page; null on the last page.

`string | null`.

<!-- https://boltclaw.ai/docs/api/agents as markdown. Generated from openapi.yaml; API version 1.22.0. -->
<!-- Every /docs page answers as markdown: add .md to its path, or send Accept: text/markdown. -->
