BatonDeck: the hosted platform: the /v1 API reference, the owner MCP server and webhooks
# BatonDeck
> The hosted HDTP platform: the portal, the /v1 API and the owner MCP server, and where each answers.
BatonDeck runs HDTP identities for people who would rather not run a node. Three doors reach the same platform, each answering on its own host in production: | Door | Where | For | | ----------------------------------- | ------------------------------- | ------------------------------------------- | | The portal | `https://app.batondeck.com` | a person, signed in with the portal session | | [The /v1 API](/cloud/api/) | `https://api.batondeck.com/v1` | a program, with a workspace API key | | [The owner MCP server](/cloud/mcp/) | `https://mcp.batondeck.com/mcp` | an agent, over OAuth | Every action the owner MCP takes is a `/v1` operation run through the same pipeline, with the same permission; the [MCP reference](/cloud/mcp/) names the operation behind each action. ## Keys [Section titled “Keys”](#keys) A workspace API key: `bd__`. Minted by a person on the portal’s Agents page (Agent keys), which calls `POST /v1/workspace/keys` with a session; a key cannot mint one, because minting needs step-up. Shown once, hashed at rest. Malformed, unknown and revoked keys answer identically.
# The /v1 API
> BatonDeck's REST API: 109 operations, generated from the route table the router mounts.
The API the BatonDeck portal is built on, and the one a program talks to. Two credentials. A browser sends the portal session cookie and, on anything that changes state, must be same-origin. A program sends `Authorization: Bearer ` — a workspace API key, minted in the portal, shown once. A key is never wider than the owner who minted it: its scopes narrow their permissions and can never widen them. A key has no session, so it has no step-up: every operation marked as requiring one is refused to a key, whatever scopes it carries. Those are the irreversible ones — export, deletion, key rotation, changing who is in the workspace — and a person has to be present for them. Webhooks. A workspace registers an endpoint with `POST /v1/workspace/webhooks`, naming the events it wants, and lists its endpoints with `GET /v1/workspace/webhooks`, which also answers the event names a filter may use. Each delivery is a JSON POST to the endpoint, signed in `X-BatonDeck-Signature: t=,v1=." under the endpoint's secret>` (two `v1` values while a rotated secret is still accepted), and carrying `X-BatonDeck-Event-Id`, `X-BatonDeck-Event-Type` and `X-BatonDeck-Delivery`. Nothing is delivered while the workspace’s `webhooks_enabled` flag is off; the list says which it is, as `enabled`. **Server:** `https://api.batondeck.com` · **OpenAPI:** [3.1.0 document](/batondeck-openapi.json) (109 operations; the same document the API serves at `https://api.batondeck.com/v1/openapi.json`). ## Credentials [Section titled “Credentials”](#credentials) | Scheme | How it is sent | Notes | | --------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `session` | cookie `__Host-bd_session` | | | `apiKey` | `Authorization: Bearer …` | A workspace API key: `bd__`. Minted by a person on the portal’s Agents page (Agent keys), which calls `POST /v1/workspace/keys` with a session; a key cannot mint one, because minting needs step-up. Shown once, hashed at rest. Malformed, unknown and revoked keys answer identically. | ## Errors [Section titled “Errors”](#errors) Every refusal answers this body:
```json
{
"type": "object",
"properties": {
"error": {
"type": "object",
"properties": {
"code": {
"description": "a stable machine-readable code; HDTP §12 names where a call maps to one",
"type": "string"
},
"message": {
"description": "what went wrong, in words a person can act on",
"type": "string"
},
"request_id": {
"description": "the same id that appears in our logs for this request",
"type": "string"
},
"retry_after": {
"description": "on a 429: seconds until the window closes, as the Retry-After header says",
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
}
},
"required": [
"code",
"message",
"request_id"
],
"additionalProperties": false
}
},
"required": [
"error"
],
"additionalProperties": false
}
```
## Calling it from a browser [Section titled “Calling it from a browser”](#calling-it-from-a-browser) The API answers cross-origin (CORS) requests only on `/mcp`, `/oauth/token`, `/oauth/register`, `/oauth/revoke`, `/.well-known/`. `/v1` is not among them, so a page on another origin — this one included — cannot call it, and this reference has no in-page “try it”. Each operation shows the `curl` call instead. ## Operations [Section titled “Operations”](#operations) * [Factors](/cloud/api/factors/) — 4 operations * [Identities](/cloud/api/identities/) — 3 operations * [Identity: address](/cloud/api/identity-address/) — 1 operation * [Identity: addresses](/cloud/api/identity-addresses/) — 3 operations * [Identity: audit](/cloud/api/identity-audit/) — 1 operation * [Identity: badges](/cloud/api/identity-badges/) — 1 operation * [Identity: card](/cloud/api/identity-card/) — 1 operation * [Identity: certificate](/cloud/api/identity-certificate/) — 1 operation * [Identity: changes](/cloud/api/identity-changes/) — 1 operation * [Identity: contacts](/cloud/api/identity-contacts/) — 14 operations * [Identity: csr](/cloud/api/identity-csr/) — 1 operation * [Identity: digest](/cloud/api/identity-digest/) — 1 operation * [Identity: export](/cloud/api/identity-export/) — 2 operations * [Identity: grants](/cloud/api/identity-grants/) — 3 operations * [Identity: import](/cloud/api/identity-import/) — 5 operations * [Identity: inbox](/cloud/api/identity-inbox/) — 1 operation * [Identity: integrations](/cloud/api/identity-integrations/) — 6 operations * [Identity: invites](/cloud/api/identity-invites/) — 4 operations * [Identity: leaf](/cloud/api/identity-leaf/) — 1 operation * [Identity: media](/cloud/api/identity-media/) — 1 operation * [Identity: messages](/cloud/api/identity-messages/) — 2 operations * [Identity: pending](/cloud/api/identity-pending/) — 2 operations * [Identity: presets](/cloud/api/identity-presets/) — 2 operations * [Identity: settings](/cloud/api/identity-settings/) — 2 operations * [Identity: storage](/cloud/api/identity-storage/) — 1 operation * [Identity: threads](/cloud/api/identity-threads/) — 4 operations * [Identity: wallet request](/cloud/api/identity-wallet-request/) — 2 operations * [Integrations](/cloud/api/integrations/) — 1 operation * [Presets](/cloud/api/presets/) — 1 operation * [Sessions](/cloud/api/sessions/) — 3 operations * [Statuses](/cloud/api/statuses/) — 1 operation * [Workspace](/cloud/api/workspace/) — 2 operations * [Workspace: audit](/cloud/api/workspace-audit/) — 1 operation * [Workspace: billing](/cloud/api/workspace-billing/) — 3 operations * [Workspace: deletion](/cloud/api/workspace-deletion/) — 2 operations * [Workspace: domains](/cloud/api/workspace-domains/) — 5 operations * [Workspace: export](/cloud/api/workspace-export/) — 1 operation * [Workspace: keys](/cloud/api/workspace-keys/) — 3 operations * [Workspace: members](/cloud/api/workspace-members/) — 1 operation * [Workspace: pause](/cloud/api/workspace-pause/) — 2 operations * [Workspace: plan](/cloud/api/workspace-plan/) — 1 operation * [Workspace: residency](/cloud/api/workspace-residency/) — 1 operation * [Workspace: settings](/cloud/api/workspace-settings/) — 1 operation * [Workspace: sso](/cloud/api/workspace-sso/) — 3 operations * [Workspace: usage](/cloud/api/workspace-usage/) — 1 operation * [Workspace: webhooks](/cloud/api/workspace-webhooks/) — 6 operations
# Factors
> The caller's own second factors: whether two-factor authentication is on, and each authenticator; Start adding an authenticator app: the QR code and secret, shown once, and the enrolment its first code finishes. Nothing changes until then; Finish adding an authenticator with the code it shows: from
## The caller’s own second factors: whether two-factor authentication is on, and each authenticator [Section titled “The caller’s own second factors: whether two-factor authentication is on, and each authenticator”](#the-callers-own-second-factors-whether-two-factor-authentication-is-on-and-each-authenticator) `GET /v1/factors` · operation `listFactors` Requires the `batondeck:workspace:read` permission (action `factor:list`). **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------ | | 200 | The caller’s own second factors: whether two-factor authentication is on, and each authenticator | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/factors' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Start adding an authenticator app: the QR code and secret, shown once, and the enrolment its first code finishes. Nothing changes until then [Section titled “Start adding an authenticator app: the QR code and secret, shown once, and the enrolment its first code finishes. Nothing changes until then”](#start-adding-an-authenticator-app-the-qr-code-and-secret-shown-once-and-the-enrolment-its-first-code-finishes-nothing-changes-until-then) `POST /v1/factors` · operation `startFactorEnrolment` Requires the `batondeck:workspace:read` permission (action `factor:enroll`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Responses** | Status | Meaning | | ------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | 201 | Start adding an authenticator app: the QR code and secret, shown once, and the enrolment its first code finishes. Nothing changes until then | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/factors' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Finish adding an authenticator with the code it shows: from then on, signing in asks for it [Section titled “Finish adding an authenticator with the code it shows: from then on, signing in asks for it”](#finish-adding-an-authenticator-with-the-code-it-shows-from-then-on-signing-in-asks-for-it) `POST /v1/factors/verify` · operation `verifyFactorEnrolment` Requires the `batondeck:workspace:read` permission (action `factor:enroll`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Request body** (`application/json`) | Field | Type | Required | Notes | | ----------- | ------ | -------- | ----------------------- | | `enrolment` | string | yes | ≥ 1 chars, ≤ 2048 chars | | `code` | string | yes | | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------- | | 201 | Finish adding an authenticator with the code it shows: from then on, signing in asks for it | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/factors/verify' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Remove one of the caller’s authenticators; with none left, signing in asks for no code [Section titled “Remove one of the caller’s authenticators; with none left, signing in asks for no code”](#remove-one-of-the-callers-authenticators-with-none-left-signing-in-asks-for-no-code) `DELETE /v1/factors/{id}` · operation `removeFactor` Requires the `batondeck:workspace:read` permission (action `factor:remove`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ---- | ---- | ------ | -------- | ----- | | `id` | path | string | yes | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 204 | Done. No body. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X DELETE 'https://api.batondeck.com/v1/factors/:id' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Identities
> The identities in the signed-in workspace; One identity: its key fingerprint, hostname and card; Erase one identity: its object, its media, and its routing row
## The identities in the signed-in workspace [Section titled “The identities in the signed-in workspace”](#the-identities-in-the-signed-in-workspace) `GET /v1/identities` · operation `listIdentities` Requires the `batondeck:identities:read` permission (action `identity:list`). **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 200 | The identities in the signed-in workspace | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## One identity: its key fingerprint, hostname and card [Section titled “One identity: its key fingerprint, hostname and card”](#one-identity-its-key-fingerprint-hostname-and-card) `GET /v1/identities/{slug}` · operation `getIdentity` Requires the `batondeck:identities:read` permission (action `identity:read`). **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------- | | 200 | One identity: its key fingerprint, hostname and card | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Erase one identity: its object, its media, and its routing row [Section titled “Erase one identity: its object, its media, and its routing row”](#erase-one-identity-its-object-its-media-and-its-routing-row) `DELETE /v1/identities/{slug}` · operation `deleteIdentity` Requires the `batondeck:identities:manage` permission (action `identity:delete`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 204 | Done. No body. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X DELETE 'https://api.batondeck.com/v1/identities/:slug' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Identity: address
> Switch the identity to the address its current leaf names: the routing half of a move (SPEC §5.3, §9)
## Switch the identity to the address its current leaf names: the routing half of a move (SPEC §5.3, §9) [Section titled “Switch the identity to the address its current leaf names: the routing half of a move (SPEC §5.3, §9)”](#switch-the-identity-to-the-address-its-current-leaf-names-the-routing-half-of-a-move-spec-53-9) `POST /v1/identities/{slug}/address` · operation `moveIdentityAddress` Requires the `batondeck:identities:manage` permission (action `identity:move`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | --------- | ------ | -------- | ---------------------- | | `address` | string | yes | ≥ 3 chars, ≤ 300 chars | **Responses** | Status | Meaning | | ------ | ----------------------------------------------------------------------------------------------------- | | 201 | Switch the identity to the address its current leaf names: the routing half of a move (SPEC §5.3, §9) | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/address' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
# Identity: addresses
> Contacts waiting at a new address for the owner's decision (SPEC §5.3); Re-pin the contact at the new address, as accept_new_hosts: auto would have; Leave the pin where it is; the new address is a stranger the owner may block
## Contacts waiting at a new address for the owner’s decision (SPEC §5.3) [Section titled “Contacts waiting at a new address for the owner’s decision (SPEC §5.3)”](#contacts-waiting-at-a-new-address-for-the-owners-decision-spec-53) `GET /v1/identities/{slug}/addresses/pending` · operation `listPendingAddresses` Requires the `batondeck:contacts:read` permission (action `contact:read`). **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------- | | 200 | Contacts waiting at a new address for the owner’s decision (SPEC §5.3) | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/addresses/pending' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Re-pin the contact at the new address, as accept_new_hosts: auto would have [Section titled “Re-pin the contact at the new address, as accept_new_hosts: auto would have”](#re-pin-the-contact-at-the-new-address-as-accept_new_hosts-auto-would-have) `POST /v1/identities/{slug}/addresses/{root}/approve` · operation `approvePendingAddress` Requires the `batondeck:contacts:manage` permission (action `address:decide`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `root` | path | string | yes | | **Responses** | Status | Meaning | | ------ | --------------------------------------------------------------------------- | | 200 | Re-pin the contact at the new address, as accept_new_hosts: auto would have | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/addresses/:root/approve' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Leave the pin where it is; the new address is a stranger the owner may block [Section titled “Leave the pin where it is; the new address is a stranger the owner may block”](#leave-the-pin-where-it-is-the-new-address-is-a-stranger-the-owner-may-block) `POST /v1/identities/{slug}/addresses/{root}/reject` · operation `rejectPendingAddress` Requires the `batondeck:contacts:manage` permission (action `address:decide`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `root` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------- | | 200 | Leave the pin where it is; the new address is a stranger the owner may block | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/addresses/:root/reject' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Identity: audit
> The identity's own audit chain, newest first
## The identity’s own audit chain, newest first [Section titled “The identity’s own audit chain, newest first”](#the-identitys-own-audit-chain-newest-first) `GET /v1/identities/{slug}/audit` · operation `listIdentityAudit` Requires the `batondeck:audit:read` permission (action `audit:identity:read`). **Parameters** | Name | In | Type | Required | Notes | | ------------- | ----- | ------- | -------- | ---------------------------- | | `slug` | path | string | yes | | | `limit` | query | integer | yes | | | `subject` | query | string | | | | `action_like` | query | string | | only actions containing this | **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 200 | The identity’s own audit chain, newest first | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/audit' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Identity: badges
> The portal shell's counts in one read: unread over every thread (counted up to a cap), unread per thread of the newest page, contact requests waiting, requests parked for a person
## The portal shell’s counts in one read: unread over every thread (counted up to a cap), unread per thread of the newest page, contact requests waiting, requests parked for a person [Section titled “The portal shell’s counts in one read: unread over every thread (counted up to a cap), unread per thread of the newest page, contact requests waiting, requests parked for a person”](#the-portal-shells-counts-in-one-read-unread-over-every-thread-counted-up-to-a-cap-unread-per-thread-of-the-newest-page-contact-requests-waiting-requests-parked-for-a-person) `GET /v1/identities/{slug}/badges` · operation `getIdentityBadges` Requires the `batondeck:messages:read` permission (action `message:read`). **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 200 | The portal shell’s counts in one read: unread over every thread (counted up to a cap), unread per thread of the newest page, contact requests waiting, requests parked for a person | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/badges' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Identity: card
> The identity's own contact card, as a peer would fetch it
## The identity’s own contact card, as a peer would fetch it [Section titled “The identity’s own contact card, as a peer would fetch it”](#the-identitys-own-contact-card-as-a-peer-would-fetch-it) `GET /v1/identities/{slug}/card` · operation `getIdentityCard` Requires the `batondeck:identities:read` permission (action `identity:read`). **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Responses** | Status | Meaning | | ------ | --------------------------------------------------------- | | 200 | The identity’s own contact card, as a peer would fetch it | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/card' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Identity: certificate
> Whether the identity is certified, and its root, chain, validity and pending CSR (SPEC §2)
## Whether the identity is certified, and its root, chain, validity and pending CSR (SPEC §2) [Section titled “Whether the identity is certified, and its root, chain, validity and pending CSR (SPEC §2)”](#whether-the-identity-is-certified-and-its-root-chain-validity-and-pending-csr-spec-2) `GET /v1/identities/{slug}/certificate` · operation `getCertificate` Requires the `batondeck:identities:read` permission (action `cert:read`). **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------ | | 200 | Whether the identity is certified, and its root, chain, validity and pending CSR (SPEC §2) | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/certificate' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Identity: changes
> What has happened since a cursor, waiting up to 25 seconds for it to
## What has happened since a cursor, waiting up to 25 seconds for it to [Section titled “What has happened since a cursor, waiting up to 25 seconds for it to”](#what-has-happened-since-a-cursor-waiting-up-to-25-seconds-for-it-to) `GET /v1/identities/{slug}/changes` · operation `watchChanges` Requires the `batondeck:messages:read` permission (action `message:read`). **Parameters** | Name | In | Type | Required | Notes | | ------- | ----- | ------- | -------- | ---------------------------------------------------------------- | | `slug` | path | string | yes | | | `since` | query | integer | yes | cursor from a previous answer; 0 starts from now with no backlog | | `wait` | query | integer | yes | seconds to wait for something to happen, from 1 to 25 | **Responses** | Status | Meaning | | ------ | -------------------------------------------------------------------- | | 200 | What has happened since a cursor, waiting up to 25 seconds for it to | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/changes' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Identity: contacts
> Contacts with their tier and switchboard; Ask the holder of a contact card to be a contact of this identity (SPEC §5.2, the manual flow); Set what a contact may do: a preset, or the switches one by one; Remove a contact: the pin goes on both sides and they are told (SPEC §5.3); Approve a contact req
## Contacts with their tier and switchboard [Section titled “Contacts with their tier and switchboard”](#contacts-with-their-tier-and-switchboard) `GET /v1/identities/{slug}/contacts` · operation `listContacts` Requires the `batondeck:contacts:read` permission (action `contact:read`). **Parameters** | Name | In | Type | Required | Notes | | -------- | ----- | ----------- | --------------- | ---------------- | | `slug` | path | string | yes | | | `status` | query | “active” \\ | “pending_in” \\ | “pending_out” \\ | **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 200 | Contacts with their tier and switchboard | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/contacts' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Ask the holder of a contact card to be a contact of this identity (SPEC §5.2, the manual flow) [Section titled “Ask the holder of a contact card to be a contact of this identity (SPEC §5.2, the manual flow)”](#ask-the-holder-of-a-contact-card-to-be-a-contact-of-this-identity-spec-52-the-manual-flow) `POST /v1/identities/{slug}/contacts` · operation `requestContact` Requires the `batondeck:contacts:manage` permission (action `contact:write`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | ------ | ------ | -------- | ------------------------ | | `card` | string | yes | ≥ 1 chars, ≤ 16384 chars | | `note` | string | | ≤ 1024 chars | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 201 | Ask the holder of a contact card to be a contact of this identity (SPEC §5.2, the manual flow) | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. | | 429 | This identity’s outbound budget (HDTP §12: 1 call a second per contact with a burst of 10, the identity’s aggregate, 20 an hour to strangers), or the peer’s own, refused the call; `retry_after` and Retry-After say when to try again. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/contacts' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Set what a contact may do: a preset, or the switches one by one [Section titled “Set what a contact may do: a preset, or the switches one by one”](#set-what-a-contact-may-do-a-preset-or-the-switches-one-by-one) `PATCH /v1/identities/{slug}/contacts/{fingerprint}` · operation `updateContact` Requires the `batondeck:contacts:manage` permission (action `contact:write`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------------- | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `fingerprint` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | ------------- | --------------- | -------------- | ---------- | | `preset` | “basic” \\ | “colleague” \\ | “close” \\ | | `permissions` | array of string | | | **Responses** | Status | Meaning | | ------ | --------------------------------------------------------------- | | 200 | Set what a contact may do: a preset, or the switches one by one | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X PATCH 'https://api.batondeck.com/v1/identities/:slug/contacts/:fingerprint' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Remove a contact: the pin goes on both sides and they are told (SPEC §5.3) [Section titled “Remove a contact: the pin goes on both sides and they are told (SPEC §5.3)”](#remove-a-contact-the-pin-goes-on-both-sides-and-they-are-told-spec-53) `DELETE /v1/identities/{slug}/contacts/{fingerprint}` · operation `removeContact` Requires the `batondeck:contacts:manage` permission (action `contact:write`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------------- | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `fingerprint` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 200 | Remove a contact: the pin goes on both sides and they are told (SPEC §5.3) | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. | | 429 | This identity’s outbound budget (HDTP §12: 1 call a second per contact with a burst of 10, the identity’s aggregate, 20 an hour to strangers), or the peer’s own, refused the call; `retry_after` and Retry-After say when to try again. |
```sh
curl -X DELETE 'https://api.batondeck.com/v1/identities/:slug/contacts/:fingerprint' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Approve a contact request and set the preset it starts on [Section titled “Approve a contact request and set the preset it starts on”](#approve-a-contact-request-and-set-the-preset-it-starts-on) `POST /v1/identities/{slug}/contacts/{fingerprint}/approve` · operation `approveContact` Requires the `batondeck:contacts:manage` permission (action `contact:write`). Refused while the workspace is suspended or on deletion hold. Accepts an `Idempotency-Key` header. A repeat within 24 hours returns the first answer; the same key with different arguments is refused with `idempotency_mismatch`. **Parameters** | Name | In | Type | Required | Notes | | ----------------- | ------ | ------ | -------- | ----------------------------------------------------------------- | | `slug` | path | string | yes | | | `fingerprint` | path | string | yes | | | `Idempotency-Key` | header | string | | Repeat this value to retry the call without repeating its effect. | **Request body** (`application/json`) | Field | Type | Required | Notes | | -------- | ---------- | -------------- | ---------- | | `preset` | “basic” \\ | “colleague” \\ | “close” \\ | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 200 | Approve a contact request and set the preset it starts on | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. | | 422 | That Idempotency-Key was used with different arguments. | | 429 | This identity’s outbound budget (HDTP §12: 1 call a second per contact with a burst of 10, the identity’s aggregate, 20 an hour to strangers), or the peer’s own, refused the call; `retry_after` and Retry-After say when to try again. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/contacts/:fingerprint/approve' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Reject a contact request: they are blocked, so they cannot knock again, and they are told (SPEC §5.1) [Section titled “Reject a contact request: they are blocked, so they cannot knock again, and they are told (SPEC §5.1)”](#reject-a-contact-request-they-are-blocked-so-they-cannot-knock-again-and-they-are-told-spec-51) `POST /v1/identities/{slug}/contacts/{fingerprint}/reject` · operation `rejectContact` Requires the `batondeck:contacts:manage` permission (action `contact:write`). Refused while the workspace is suspended or on deletion hold. Accepts an `Idempotency-Key` header. A repeat within 24 hours returns the first answer; the same key with different arguments is refused with `idempotency_mismatch`. **Parameters** | Name | In | Type | Required | Notes | | ----------------- | ------ | ------ | -------- | ----------------------------------------------------------------- | | `slug` | path | string | yes | | | `fingerprint` | path | string | yes | | | `Idempotency-Key` | header | string | | Repeat this value to retry the call without repeating its effect. | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 200 | Reject a contact request: they are blocked, so they cannot knock again, and they are told (SPEC §5.1) | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. | | 422 | That Idempotency-Key was used with different arguments. | | 429 | This identity’s outbound budget (HDTP §12: 1 call a second per contact with a burst of 10, the identity’s aggregate, 20 an hour to strangers), or the peer’s own, refused the call; `retry_after` and Retry-After say when to try again. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/contacts/:fingerprint/reject' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Tell an active contact again that we accepted them (SPEC §5.1), when the approval did not reach them [Section titled “Tell an active contact again that we accepted them (SPEC §5.1), when the approval did not reach them”](#tell-an-active-contact-again-that-we-accepted-them-spec-51-when-the-approval-did-not-reach-them) `POST /v1/identities/{slug}/contacts/{fingerprint}/tell-accepted` · operation `notifyAcceptance` Requires the `batondeck:contacts:manage` permission (action `contact:write`). Refused while the workspace is suspended or on deletion hold. Accepts an `Idempotency-Key` header. A repeat within 24 hours returns the first answer; the same key with different arguments is refused with `idempotency_mismatch`. **Parameters** | Name | In | Type | Required | Notes | | ----------------- | ------ | ------ | -------- | ----------------------------------------------------------------- | | `slug` | path | string | yes | | | `fingerprint` | path | string | yes | | | `Idempotency-Key` | header | string | | Repeat this value to retry the call without repeating its effect. | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 200 | Tell an active contact again that we accepted them (SPEC §5.1), when the approval did not reach them | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. | | 422 | That Idempotency-Key was used with different arguments. | | 429 | This identity’s outbound budget (HDTP §12: 1 call a second per contact with a burst of 10, the identity’s aggregate, 20 an hour to strangers), or the peer’s own, refused the call; `retry_after` and Retry-After say when to try again. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/contacts/:fingerprint/tell-accepted' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## The tools a contact offers us, asked of them over the sealed handshake [Section titled “The tools a contact offers us, asked of them over the sealed handshake”](#the-tools-a-contact-offers-us-asked-of-them-over-the-sealed-handshake) `GET /v1/identities/{slug}/contacts/{fingerprint}/tools` · operation `listContactTools` Requires the `batondeck:contacts:read` permission (action `contact:read`). **Parameters** | Name | In | Type | Required | Notes | | ------------- | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `fingerprint` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 200 | The tools a contact offers us, asked of them over the sealed handshake | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. | | 429 | This identity’s outbound budget (HDTP §12: 1 call a second per contact with a burst of 10, the identity’s aggregate, 20 an hour to strangers), or the peer’s own, refused the call; `retry_after` and Retry-After say when to try again. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/contacts/:fingerprint/tools' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Call one of a contact’s tools as this identity (SPEC §5.4) [Section titled “Call one of a contact’s tools as this identity (SPEC §5.4)”](#call-one-of-a-contacts-tools-as-this-identity-spec-54) `POST /v1/identities/{slug}/contacts/{fingerprint}/call` · operation `callContactTool` Requires the `batondeck:messages:send` permission (action `message:send`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------------- | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `fingerprint` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | ----------- | ------ | -------- | --------- | | `tool` | string | yes | ≥ 1 chars | | `arguments` | object | | | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 200 | Call one of a contact’s tools as this identity (SPEC §5.4) | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. | | 429 | This identity’s outbound budget (HDTP §12: 1 call a second per contact with a burst of 10, the identity’s aggregate, 20 an hour to strangers), or the peer’s own, refused the call; `retry_after` and Retry-After say when to try again. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/contacts/:fingerprint/call' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Block a contact; the block is silent to them (SPEC §5.5) [Section titled “Block a contact; the block is silent to them (SPEC §5.5)”](#block-a-contact-the-block-is-silent-to-them-spec-55) `POST /v1/identities/{slug}/contacts/{fingerprint}/block` · operation `blockContact` Requires the `batondeck:contacts:manage` permission (action `contact:write`). Refused while the workspace is suspended or on deletion hold. Accepts an `Idempotency-Key` header. A repeat within 24 hours returns the first answer; the same key with different arguments is refused with `idempotency_mismatch`. **Parameters** | Name | In | Type | Required | Notes | | ----------------- | ------ | ------ | -------- | ----------------------------------------------------------------- | | `slug` | path | string | yes | | | `fingerprint` | path | string | yes | | | `Idempotency-Key` | header | string | | Repeat this value to retry the call without repeating its effect. | **Responses** | Status | Meaning | | ------ | -------------------------------------------------------- | | 200 | Block a contact; the block is silent to them (SPEC §5.5) | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. | | 422 | That Idempotency-Key was used with different arguments. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/contacts/:fingerprint/block' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Undo a block: a former contact returns to active as they were; a declined request is forgotten (SPEC §5) [Section titled “Undo a block: a former contact returns to active as they were; a declined request is forgotten (SPEC §5)”](#undo-a-block-a-former-contact-returns-to-active-as-they-were-a-declined-request-is-forgotten-spec-5) `POST /v1/identities/{slug}/contacts/{fingerprint}/unblock` · operation `unblockContact` Requires the `batondeck:contacts:manage` permission (action `contact:write`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------------- | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `fingerprint` | path | string | yes | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------------------------------------------------------------- | | 200 | Undo a block: a former contact returns to active as they were; a declined request is forgotten (SPEC §5) | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/contacts/:fingerprint/unblock' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## The owner’s own private name for a contact [Section titled “The owner’s own private name for a contact”](#the-owners-own-private-name-for-a-contact) `PATCH /v1/identities/{slug}/contacts/{fingerprint}/petname` · operation `setPetname` Requires the `batondeck:contacts:manage` permission (action `contact:write`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------------- | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `fingerprint` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | --------- | ------ | -------- | ---------- | | `petname` | string | yes | ≤ 64 chars | **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 200 | The owner’s own private name for a contact | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X PATCH 'https://api.batondeck.com/v1/identities/:slug/contacts/:fingerprint/petname' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Re-fetch ONE contact’s signed card now, and say what was found (HDTP §14.3) [Section titled “Re-fetch ONE contact’s signed card now, and say what was found (HDTP §14.3)”](#re-fetch-one-contacts-signed-card-now-and-say-what-was-found-hdtp-143) `POST /v1/identities/{slug}/contacts/{fingerprint}/refresh` · operation `refreshContact` Requires the `batondeck:contacts:manage` permission (action `contact:write`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------------- | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `fingerprint` | path | string | yes | | **Request body** (`application/json`) **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 200 | Re-fetch ONE contact’s signed card now, and say what was found (HDTP §14.3) | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. | | 429 | This identity’s outbound budget (HDTP §12: 1 call a second per contact with a burst of 10, the identity’s aggregate, 20 an hour to strangers), or the peer’s own, refused the call; `retry_after` and Retry-After say when to try again. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/contacts/:fingerprint/refresh' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Whether this contact may instruct, or only send messages (SPEC §6.2) [Section titled “Whether this contact may instruct, or only send messages (SPEC §6.2)”](#whether-this-contact-may-instruct-or-only-send-messages-spec-62) `PATCH /v1/identities/{slug}/contacts/{fingerprint}/trust` · operation `setTrust` Requires the `batondeck:contacts:trust` permission (action `contact:trust`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------------- | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `fingerprint` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | ------- | ------------------ | -------------- | ----- | | `trust` | “messages_only” \\ | “may_instruct” | yes | **Responses** | Status | Meaning | | ------ | -------------------------------------------------------------------- | | 200 | Whether this contact may instruct, or only send messages (SPEC §6.2) | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X PATCH 'https://api.batondeck.com/v1/identities/:slug/contacts/:fingerprint/trust' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
# Identity: csr
> Mint a key and a certificate signing request for the wallet to sign (SPEC §9)
## Mint a key and a certificate signing request for the wallet to sign (SPEC §9) [Section titled “Mint a key and a certificate signing request for the wallet to sign (SPEC §9)”](#mint-a-key-and-a-certificate-signing-request-for-the-wallet-to-sign-spec-9) `POST /v1/identities/{slug}/csr` · operation `issueCsr` Requires the `batondeck:identities:manage` permission (action `cert:csr`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | ---------- | ----------- | ---------- | ---------------------- | | `purpose` | “signup” \\ | “renew” \\ | “move” | | `endpoint` | string | | ≥ 1 chars, ≤ 512 chars | **Responses** | Status | Meaning | | ------ | ----------------------------------------------------------------------------- | | 201 | Mint a key and a certificate signing request for the wallet to sign (SPEC §9) | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/csr' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
# Identity: digest
> A per-contact summary of the last day, for an agent catching up
## A per-contact summary of the last day, for an agent catching up [Section titled “A per-contact summary of the last day, for an agent catching up”](#a-per-contact-summary-of-the-last-day-for-an-agent-catching-up) `GET /v1/identities/{slug}/digest` · operation `getDigest` Requires the `batondeck:messages:read` permission (action `message:read`). **Parameters** | Name | In | Type | Required | Notes | | ------- | ----- | ------- | -------- | ----- | | `slug` | path | string | yes | | | `since` | query | integer | yes | | **Responses** | Status | Meaning | | ------ | --------------------------------------------------------------- | | 200 | A per-contact summary of the last day, for an agent catching up | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/digest' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Identity: export
> This identity's contacts, threads, messages and files as one unencrypted zip (design §4), streamed and never stored; What this identity's export would say of itself, without the file: each import ceiling it passes, and every message it leaves out, by id and why
## This identity’s contacts, threads, messages and files as one unencrypted zip (design §4), streamed and never stored [Section titled “This identity’s contacts, threads, messages and files as one unencrypted zip (design §4), streamed and never stored”](#this-identitys-contacts-threads-messages-and-files-as-one-unencrypted-zip-design-4-streamed-and-never-stored) `GET /v1/identities/{slug}/export` · operation `exportIdentity` Requires the `batondeck:workspace:admin` permission (action `identity:export`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. **Parameters** | Name | In | Type | Required | Notes | | ------------- | ----- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `slug` | path | string | yes | | | `acknowledge` | query | string | | Must be “unencrypted”: This file is not encrypted. Anyone who gets it can read your contact list and all your conversations and files. It holds no keys, so it cannot be used to speak as you. Keep it where you keep private documents, and delete it once it has been imported. | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------------------------- | | 200 | This identity’s contacts, threads, messages and files as one unencrypted zip (design §4), streamed and never stored | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/export' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## What this identity’s export would say of itself, without the file: each import ceiling it passes, and every message it leaves out, by id and why [Section titled “What this identity’s export would say of itself, without the file: each import ceiling it passes, and every message it leaves out, by id and why”](#what-this-identitys-export-would-say-of-itself-without-the-file-each-import-ceiling-it-passes-and-every-message-it-leaves-out-by-id-and-why) `GET /v1/identities/{slug}/export/report` · operation `getExportReport` Requires the `batondeck:identities:read` permission (action `identity:read`). **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | | 200 | What this identity’s export would say of itself, without the file: each import ceiling it passes, and every message it leaves out, by id and why | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/export/report' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Identity: grants
> Which owners may act as this identity. Several is a shared inbox (SPEC §3.3); Let another owner act as this identity, which is how a shared inbox is made; Take back an owner's access to this identity
## Which owners may act as this identity. Several is a shared inbox (SPEC §3.3) [Section titled “Which owners may act as this identity. Several is a shared inbox (SPEC §3.3)”](#which-owners-may-act-as-this-identity-several-is-a-shared-inbox-spec-33) `GET /v1/identities/{slug}/grants` · operation `listIdentityGrants` Requires the `batondeck:identities:read` permission (action `identity:read`). **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------- | | 200 | Which owners may act as this identity. Several is a shared inbox (SPEC §3.3) | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/grants' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Let another owner act as this identity, which is how a shared inbox is made [Section titled “Let another owner act as this identity, which is how a shared inbox is made”](#let-another-owner-act-as-this-identity-which-is-how-a-shared-inbox-is-made) `POST /v1/identities/{slug}/grants` · operation `grantIdentity` Requires the `batondeck:identities:manage` permission (action `identity:grant`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | ---------- | ------ | -------- | --------- | | `owner_id` | string | yes | ≥ 1 chars | **Responses** | Status | Meaning | | ------ | --------------------------------------------------------------------------- | | 200 | Let another owner act as this identity, which is how a shared inbox is made | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/grants' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Take back an owner’s access to this identity [Section titled “Take back an owner’s access to this identity”](#take-back-an-owners-access-to-this-identity) `DELETE /v1/identities/{slug}/grants/{ownerId}` · operation `revokeIdentityGrant` Requires the `batondeck:identities:manage` permission (action `identity:revoke`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Allowed while the workspace is paused, suspended or on deletion hold: it only takes access away. **Parameters** | Name | In | Type | Required | Notes | | --------- | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `ownerId` | path | string | yes | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 200 | Take back an owner’s access to this identity | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X DELETE 'https://api.batondeck.com/v1/identities/:slug/grants/:ownerId' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Identity: import
> Review an export zip sent as base64url in JSON (up to 6,000,000 bytes): validated whole and its contacts shown, nothing written; the import names the digest this answers; Review an export zip sent as the raw body (application/zip, up to the host's import ceiling): the same review as the JSON door; A
## Review an export zip sent as base64url in JSON (up to 6,000,000 bytes): validated whole and its contacts shown, nothing written; the import names the digest this answers [Section titled “Review an export zip sent as base64url in JSON (up to 6,000,000 bytes): validated whole and its contacts shown, nothing written; the import names the digest this answers”](#review-an-export-zip-sent-as-base64url-in-json-up-to-6000000-bytes-validated-whole-and-its-contacts-shown-nothing-written-the-import-names-the-digest-this-answers) `POST /v1/identities/{slug}/import/review` · operation `reviewImportArchive` Requires the `batondeck:identities:manage` permission (action `identity:import`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | --------- | ------ | -------- | --------- | | `archive` | string | yes | ≥ 1 chars | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 200 | Review an export zip sent as base64url in JSON (up to 6,000,000 bytes): validated whole and its contacts shown, nothing written; the import names the digest this answers | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/import/review' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Review an export zip sent as the raw body (application/zip, up to the host’s import ceiling): the same review as the JSON door [Section titled “Review an export zip sent as the raw body (application/zip, up to the host’s import ceiling): the same review as the JSON door”](#review-an-export-zip-sent-as-the-raw-body-applicationzip-up-to-the-hosts-import-ceiling-the-same-review-as-the-json-door) `PUT /v1/identities/{slug}/import/review` · operation `uploadImportReview` Requires the `batondeck:identities:manage` permission (action `identity:import`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Request body** (`application/zip`): The file itself, with its Content-Length, up to 99614720 bytes. **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------------------------------------ | | 200 | Review an export zip sent as the raw body (application/zip, up to the host’s import ceiling): the same review as the JSON door | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X PUT 'https://api.batondeck.com/v1/identities/:slug/import/review' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/zip' \
--data-binary @file
```
## An open review of an import: the contacts shown for that file, until it closes [Section titled “An open review of an import: the contacts shown for that file, until it closes”](#an-open-review-of-an-import-the-contacts-shown-for-that-file-until-it-closes) `GET /v1/identities/{slug}/import/review/{digest}` · operation `getImportReview` Requires the `batondeck:identities:read` permission (action `identity:read`). **Parameters** | Name | In | Type | Required | Notes | | -------- | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `digest` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------------ | | 200 | An open review of an import: the contacts shown for that file, until it closes | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/import/review/:digest' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Close an open review of an import before its time: the review and the uploaded file it holds go, and nothing else changes [Section titled “Close an open review of an import before its time: the review and the uploaded file it holds go, and nothing else changes”](#close-an-open-review-of-an-import-before-its-time-the-review-and-the-uploaded-file-it-holds-go-and-nothing-else-changes) `DELETE /v1/identities/{slug}/import/review/{digest}` · operation `cancelImportReview` Requires the `batondeck:identities:manage` permission (action `identity:import`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | -------- | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `digest` | path | string | yes | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 204 | Done. No body. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X DELETE 'https://api.batondeck.com/v1/identities/:slug/import/review/:digest' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Take in a reviewed export zip, named by its review’s digest: the file the review holds is read again and written, refused unless it reads as reviewed; answers the renew request the wallet signs next [Section titled “Take in a reviewed export zip, named by its review’s digest: the file the review holds is read again and written, refused unless it reads as reviewed; answers the renew request the wallet signs next”](#take-in-a-reviewed-export-zip-named-by-its-reviews-digest-the-file-the-review-holds-is-read-again-and-written-refused-unless-it-reads-as-reviewed-answers-the-renew-request-the-wallet-signs-next) `POST /v1/identities/{slug}/import` · operation `importArchive` Requires the `batondeck:identities:manage` permission (action `identity:import`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | -------- | ------ | -------- | ----- | | `digest` | string | yes | | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | 200 | Take in a reviewed export zip, named by its review’s digest: the file the review holds is read again and written, refused unless it reads as reviewed; answers the renew request the wallet signs next | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/import' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
# Identity: inbox
> Thread count, unread total and newest activity over every thread, counted by the object
## Thread count, unread total and newest activity over every thread, counted by the object [Section titled “Thread count, unread total and newest activity over every thread, counted by the object”](#thread-count-unread-total-and-newest-activity-over-every-thread-counted-by-the-object) `GET /v1/identities/{slug}/inbox` · operation `getInboxTotals` Requires the `batondeck:messages:read` permission (action `message:read`). **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Responses** | Status | Meaning | | ------ | --------------------------------------------------------------------------------------- | | 200 | Thread count, unread total and newest activity over every thread, counted by the object | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/inbox' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Identity: integrations
> The integrations this identity has connected, and whether each is healthy; Connect an MCP server: a catalogue entry in one click, or any server by its URL (SPEC §6.1-§6.4); Start the upstream OAuth ceremony and hand back the URL to send the owner to; What this integration offers, and what it could o
## The integrations this identity has connected, and whether each is healthy [Section titled “The integrations this identity has connected, and whether each is healthy”](#the-integrations-this-identity-has-connected-and-whether-each-is-healthy) `GET /v1/identities/{slug}/integrations` · operation `listIntegrations` Requires the `batondeck:identities:read` permission (action `identity:read`). **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------- | | 200 | The integrations this identity has connected, and whether each is healthy | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/integrations' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Connect an MCP server: a catalogue entry in one click, or any server by its URL (SPEC §6.1-§6.4) [Section titled “Connect an MCP server: a catalogue entry in one click, or any server by its URL (SPEC §6.1-§6.4)”](#connect-an-mcp-server-a-catalogue-entry-in-one-click-or-any-server-by-its-url-spec-61-64) `POST /v1/identities/{slug}/integrations` · operation `createIntegration` Requires the `batondeck:identities:manage` permission (action `integration:write`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | ----------- | -------------------- | -------- | --------------------- | | `catalogue` | string | | ≤ 64 chars | | `slug` | string | | ≥ 2 chars, ≤ 32 chars | | `endpoint` | string | | | | `transport` | “streamable-http” \\ | “sse” | | | `auth` | object \\ | null | | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------ | | 201 | Connect an MCP server: a catalogue entry in one click, or any server by its URL (SPEC §6.1-§6.4) | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/integrations' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Start the upstream OAuth ceremony and hand back the URL to send the owner to [Section titled “Start the upstream OAuth ceremony and hand back the URL to send the owner to”](#start-the-upstream-oauth-ceremony-and-hand-back-the-url-to-send-the-owner-to) `POST /v1/identities/{slug}/integrations/{name}/authorize` · operation `authorizeIntegration` Requires the `batondeck:identities:manage` permission (action `integration:write`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `name` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | ------- | ------ | -------- | ----------- | | `scope` | string | | ≤ 512 chars | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------- | | 200 | Start the upstream OAuth ceremony and hand back the URL to send the owner to | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/integrations/:name/authorize' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## What this integration offers, and what it could offer (SPEC §6.5) [Section titled “What this integration offers, and what it could offer (SPEC §6.5)”](#what-this-integration-offers-and-what-it-could-offer-spec-65) `GET /v1/identities/{slug}/integrations/{name}/exposure` · operation `getExposure` Requires the `batondeck:identities:read` permission (action `identity:read`). **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `name` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ----------------------------------------------------------------- | | 200 | What this integration offers, and what it could offer (SPEC §6.5) | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/integrations/:name/exposure' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Replace which of an integration’s tools contacts may reach [Section titled “Replace which of an integration’s tools contacts may reach”](#replace-which-of-an-integrations-tools-contacts-may-reach) `POST /v1/identities/{slug}/integrations/{name}/exposure` · operation `setExposure` Requires the `batondeck:identities:manage` permission (action `integration:write`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `name` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | --------- | --------------- | -------- | ----- | | `entries` | array of object | yes | | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------- | | 200 | Replace which of an integration’s tools contacts may reach | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/integrations/:name/exposure' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Disconnect an upstream and forget its credential [Section titled “Disconnect an upstream and forget its credential”](#disconnect-an-upstream-and-forget-its-credential) `DELETE /v1/identities/{slug}/integrations/{name}` · operation `deleteIntegration` Requires the `batondeck:identities:manage` permission (action `integration:write`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `name` | path | string | yes | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 200 | Disconnect an upstream and forget its credential | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X DELETE 'https://api.batondeck.com/v1/identities/:slug/integrations/:name' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Identity: invites
> Invites this identity has issued. Each carries its link where the token was kept, and only for a caller who may mint an invite (contact:write); Mint an invite. The link is in the answer, and listInvites answers it again, to a caller who may mint one, for as long as the row exists; Accept somebody el
## Invites this identity has issued. Each carries its link where the token was kept, and only for a caller who may mint an invite (contact:write) [Section titled “Invites this identity has issued. Each carries its link where the token was kept, and only for a caller who may mint an invite (contact:write)”](#invites-this-identity-has-issued-each-carries-its-link-where-the-token-was-kept-and-only-for-a-caller-who-may-mint-an-invite-contactwrite) `GET /v1/identities/{slug}/invites` · operation `listInvites` Requires the `batondeck:contacts:read` permission (action `contact:read`). **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | 200 | Invites this identity has issued. Each carries its link where the token was kept, and only for a caller who may mint an invite (contact:write) | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/invites' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Mint an invite. The link is in the answer, and listInvites answers it again, to a caller who may mint one, for as long as the row exists [Section titled “Mint an invite. The link is in the answer, and listInvites answers it again, to a caller who may mint one, for as long as the row exists”](#mint-an-invite-the-link-is-in-the-answer-and-listinvites-answers-it-again-to-a-caller-who-may-mint-one-for-as-long-as-the-row-exists) `POST /v1/identities/{slug}/invites` · operation `createInvite` Requires the `batondeck:contacts:manage` permission (action `contact:write`). Refused while the workspace is suspended or on deletion hold. Accepts an `Idempotency-Key` header. A repeat within 24 hours returns the first answer; the same key with different arguments is refused with `idempotency_mismatch`. **Parameters** | Name | In | Type | Required | Notes | | ----------------- | ------ | ------ | -------- | ----------------------------------------------------------------- | | `slug` | path | string | yes | | | `Idempotency-Key` | header | string | | Repeat this value to retry the call without repeating its effect. | **Request body** (`application/json`) | Field | Type | Required | Notes | | ----------------- | ---------- | -------------- | ----------------------------------------------------------- | | `label` | string | yes | ≤ 128 chars, default “” | | `preset` | “basic” \\ | “colleague” \\ | “close” \\ | | `max_uses` | integer | yes | 1 for one-time, 0 for unlimited. min 0, max 1000, default 1 | | `auto_accept` | boolean | yes | default false | | `expires_in_days` | integer | yes | min 1, max 90, default 14 | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | 201 | Mint an invite. The link is in the answer, and listInvites answers it again, to a caller who may mint one, for as long as the row exists | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. | | 422 | That Idempotency-Key was used with different arguments. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/invites' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Accept somebody else’s invite link: this identity becomes their contact [Section titled “Accept somebody else’s invite link: this identity becomes their contact”](#accept-somebody-elses-invite-link-this-identity-becomes-their-contact) `POST /v1/identities/{slug}/invites/redeem` · operation `redeemInvite` Requires the `batondeck:contacts:manage` permission (action `contact:write`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | ----- | ------ | -------- | ------------------------ | | `url` | string | yes | ≥ 12 chars, ≤ 2048 chars | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 201 | Accept somebody else’s invite link: this identity becomes their contact | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. | | 429 | This identity’s outbound budget (HDTP §12: 1 call a second per contact with a burst of 10, the identity’s aggregate, 20 an hour to strangers), or the peer’s own, refused the call; `retry_after` and Retry-After say when to try again. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/invites/redeem' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Revoke an invite; a revoked token is indistinguishable from one that never existed [Section titled “Revoke an invite; a revoked token is indistinguishable from one that never existed”](#revoke-an-invite-a-revoked-token-is-indistinguishable-from-one-that-never-existed) `DELETE /v1/identities/{slug}/invites/{id}` · operation `revokeInvite` Requires the `batondeck:contacts:manage` permission (action `contact:write`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `id` | path | string | yes | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 204 | Done. No body. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X DELETE 'https://api.batondeck.com/v1/identities/:slug/invites/:id' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Identity: leaf
> Install the chain the wallet issued: the identity becomes, or renews as, 2.0
## Install the chain the wallet issued: the identity becomes, or renews as, 2.0 [Section titled “Install the chain the wallet issued: the identity becomes, or renews as, 2.0”](#install-the-chain-the-wallet-issued-the-identity-becomes-or-renews-as-20) `POST /v1/identities/{slug}/leaf` · operation `installLeaf` Requires the `batondeck:identities:manage` permission (action `cert:install`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | ----------------- | --------------- | -------- | ----- | | `chain` | array of string | yes | | | `credential_id` | string \\ | null | | | `backup_verified` | boolean \\ | null | | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------- | | 201 | Install the chain the wallet issued: the identity becomes, or renews as, 2.0 | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/leaf' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
# Identity: media
> Send a file to a contact (SPEC §7.4)
## Send a file to a contact (SPEC §7.4) [Section titled “Send a file to a contact (SPEC §7.4)”](#send-a-file-to-a-contact-spec-74) `POST /v1/identities/{slug}/media` · operation `sendMedia` Requires the `batondeck:messages:send` permission (action `message:send`). Refused while the workspace is suspended or on deletion hold. Accepts an `Idempotency-Key` header. A repeat within 24 hours returns the first answer; the same key with different arguments is refused with `idempotency_mismatch`. **Parameters** | Name | In | Type | Required | Notes | | ----------------- | ------ | ------ | -------- | ----------------------------------------------------------------- | | `slug` | path | string | yes | | | `Idempotency-Key` | header | string | | Repeat this value to retry the call without repeating its effect. | **Request body** (`application/json`) | Field | Type | Required | Notes | | ------------- | ------ | -------- | ----------------------------------------------- | | `fingerprint` | string | yes | ≥ 1 chars | | `data` | string | yes | ≥ 1 chars | | `filename` | string | yes | ≤ 256 chars, default “” | | `mime` | string | yes | ≤ 128 chars, default “application/octet-stream” | | `thread_id` | string | | ≤ 128 chars | | `msg_id` | string | | ≤ 128 chars | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 201 | Send a file to a contact (SPEC §7.4) | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. | | 422 | That Idempotency-Key was used with different arguments. | | 429 | This identity’s outbound budget (HDTP §12: 1 call a second per contact with a burst of 10, the identity’s aggregate, 20 an hour to strangers), or the peer’s own, refused the call; `retry_after` and Retry-After say when to try again. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/media' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
# Identity: messages
> Send a message to a contact as this identity; Try again now to deliver an outbound text message that has not arrived (pending, or given up on)
## Send a message to a contact as this identity [Section titled “Send a message to a contact as this identity”](#send-a-message-to-a-contact-as-this-identity) `POST /v1/identities/{slug}/messages` · operation `sendMessage` Requires the `batondeck:messages:send` permission (action `message:send`). Refused while the workspace is suspended or on deletion hold. Accepts an `Idempotency-Key` header. A repeat within 24 hours returns the first answer; the same key with different arguments is refused with `idempotency_mismatch`. **Parameters** | Name | In | Type | Required | Notes | | ----------------- | ------ | ------ | -------- | ----------------------------------------------------------------- | | `slug` | path | string | yes | | | `Idempotency-Key` | header | string | | Repeat this value to retry the call without repeating its effect. | **Request body** (`application/json`) | Field | Type | Required | Notes | | ------------- | ------ | -------- | ------------------------ | | `fingerprint` | string | yes | ≥ 1 chars | | `text` | string | yes | ≥ 1 chars, ≤ 16384 chars | | `thread_id` | string | | ≤ 128 chars | | `msg_id` | string | | ≤ 128 chars | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 201 | Send a message to a contact as this identity | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. | | 422 | That Idempotency-Key was used with different arguments. | | 429 | This identity’s outbound budget (HDTP §12: 1 call a second per contact with a burst of 10, the identity’s aggregate, 20 an hour to strangers), or the peer’s own, refused the call; `retry_after` and Retry-After say when to try again. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/messages' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Try again now to deliver an outbound text message that has not arrived (pending, or given up on) [Section titled “Try again now to deliver an outbound text message that has not arrived (pending, or given up on)”](#try-again-now-to-deliver-an-outbound-text-message-that-has-not-arrived-pending-or-given-up-on) `POST /v1/identities/{slug}/messages/{id}/retry` · operation `retryMessage` Requires the `batondeck:messages:send` permission (action `message:send`). Refused while the workspace is suspended or on deletion hold. Accepts an `Idempotency-Key` header. A repeat within 24 hours returns the first answer; the same key with different arguments is refused with `idempotency_mismatch`. **Parameters** | Name | In | Type | Required | Notes | | ----------------- | ------ | ------ | -------- | ----------------------------------------------------------------- | | `slug` | path | string | yes | | | `id` | path | string | yes | | | `Idempotency-Key` | header | string | | Repeat this value to retry the call without repeating its effect. | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 200 | Try again now to deliver an outbound text message that has not arrived (pending, or given up on) | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. | | 422 | That Idempotency-Key was used with different arguments. | | 429 | This identity’s outbound budget (HDTP §12: 1 call a second per contact with a burst of 10, the identity’s aggregate, 20 an hour to strangers), or the peer’s own, refused the call; `retry_after` and Retry-After say when to try again. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/messages/:id/retry' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Identity: pending
> Answer a request an integration parked for a person (SPEC §6.8); Agent-answered requests a caller is waiting on (SPEC §6.8)
## Answer a request an integration parked for a person (SPEC §6.8) [Section titled “Answer a request an integration parked for a person (SPEC §6.8)”](#answer-a-request-an-integration-parked-for-a-person-spec-68) `POST /v1/identities/{slug}/pending/{id}` · operation `answerPendingRequest` Requires the `batondeck:requests:answer` permission (action `requests:answer`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `id` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | -------- | ---- | -------- | ----------------- | | `answer` | | yes | the reply payload | **Responses** | Status | Meaning | | ------ | --------------------------------------------------------------- | | 200 | Answer a request an integration parked for a person (SPEC §6.8) | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/pending/:id' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Agent-answered requests a caller is waiting on (SPEC §6.8) [Section titled “Agent-answered requests a caller is waiting on (SPEC §6.8)”](#agent-answered-requests-a-caller-is-waiting-on-spec-68) `GET /v1/identities/{slug}/pending` · operation `listPendingRequests` Requires the `batondeck:messages:read` permission (action `message:read`). **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------- | | 200 | Agent-answered requests a caller is waiting on (SPEC §6.8) | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/pending' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Identity: presets
> This identity's own preset bundles, and which have been edited; Change what a preset grants, for this identity
## This identity’s own preset bundles, and which have been edited [Section titled “This identity’s own preset bundles, and which have been edited”](#this-identitys-own-preset-bundles-and-which-have-been-edited) `GET /v1/identities/{slug}/presets` · operation `listIdentityPresets` Requires the `batondeck:contacts:read` permission (action `contact:read`). **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------------------- | | 200 | This identity’s own preset bundles, and which have been edited | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/presets' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Change what a preset grants, for this identity [Section titled “Change what a preset grants, for this identity”](#change-what-a-preset-grants-for-this-identity) `PATCH /v1/identities/{slug}/presets/{name}` · operation `setIdentityPreset` Requires the `batondeck:contacts:manage` permission (action `contact:write`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `name` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | ------------- | --------------- | -------- | ----- | | `permissions` | array of string | yes | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 200 | Change what a preset grants, for this identity | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X PATCH 'https://api.batondeck.com/v1/identities/:slug/presets/:name' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
# Identity: settings
> This identity's own settings: accept_new_hosts, the owner's status and moved_away_at; What happens when a pinned contact turns up at a new address (accept_new_hosts), the owner's status, which decides what contacts read with get_status, or, alone, moved_away: no renewal reminders
## This identity’s own settings: accept_new_hosts, the owner’s status and moved_away_at [Section titled “This identity’s own settings: accept_new_hosts, the owner’s status and moved_away_at”](#this-identitys-own-settings-accept_new_hosts-the-owners-status-and-moved_away_at) `GET /v1/identities/{slug}/settings` · operation `getIdentitySettings` Requires the `batondeck:identities:read` permission (action `identity:read`). **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------------------ | | 200 | This identity’s own settings: accept_new_hosts, the owner’s status and moved_away_at | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/settings' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## What happens when a pinned contact turns up at a new address (accept_new_hosts), the owner’s status, which decides what contacts read with get_status, or, alone, moved_away: no renewal reminders [Section titled “What happens when a pinned contact turns up at a new address (accept_new_hosts), the owner’s status, which decides what contacts read with get_status, or, alone, moved_away: no renewal reminders”](#what-happens-when-a-pinned-contact-turns-up-at-a-new-address-accept_new_hosts-the-owners-status-which-decides-what-contacts-read-with-get_status-or-alone-moved_away-no-renewal-reminders) `PATCH /v1/identities/{slug}/settings` · operation `updateIdentitySettings` Requires the `batondeck:identities:manage` permission (action `identity:update`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | ------------------ | --------- | -------------- | ------------------ | | `accept_new_hosts` | “auto” \\ | “ask” | | | `status` | “auto” \\ | “available” \\ | “not_available” \\ | | `moved_away` | boolean | | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 200 | What happens when a pinned contact turns up at a new address (accept_new_hosts), the owner’s status, which decides what contacts read with get_status, or, alone, moved_away: no renewal reminders | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X PATCH 'https://api.batondeck.com/v1/identities/:slug/settings' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
# Identity: storage
> Bytes this identity holds, and the ceiling it is held against
## Bytes this identity holds, and the ceiling it is held against [Section titled “Bytes this identity holds, and the ceiling it is held against”](#bytes-this-identity-holds-and-the-ceiling-it-is-held-against) `GET /v1/identities/{slug}/storage` · operation `getStorage` Requires the `batondeck:identities:read` permission (action `identity:read`). **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------- | | 200 | Bytes this identity holds, and the ceiling it is held against | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/storage' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Identity: threads
> Message threads, newest first, with unread counts; One thread by its id, whatever page it is on, with its unread count; The newest messages in one thread, oldest first within the window; The owner has read this thread, through the message `through` names or, with no body, all of it (SPEC §7.6)
## Message threads, newest first, with unread counts [Section titled “Message threads, newest first, with unread counts”](#message-threads-newest-first-with-unread-counts) `GET /v1/identities/{slug}/threads` · operation `listThreads` Requires the `batondeck:messages:read` permission (action `message:read`). **Parameters** | Name | In | Type | Required | Notes | | -------- | ----- | ------- | -------- | ----- | | `slug` | path | string | yes | | | `limit` | query | integer | yes | | | `cursor` | query | string | | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 200 | Message threads, newest first, with unread counts | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/threads' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## One thread by its id, whatever page it is on, with its unread count [Section titled “One thread by its id, whatever page it is on, with its unread count”](#one-thread-by-its-id-whatever-page-it-is-on-with-its-unread-count) `GET /v1/identities/{slug}/threads/{threadId}` · operation `getThread` Requires the `batondeck:messages:read` permission (action `message:read`). **Parameters** | Name | In | Type | Required | Notes | | ---------- | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `threadId` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------- | | 200 | One thread by its id, whatever page it is on, with its unread count | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/threads/:threadId' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## The newest messages in one thread, oldest first within the window [Section titled “The newest messages in one thread, oldest first within the window”](#the-newest-messages-in-one-thread-oldest-first-within-the-window) `GET /v1/identities/{slug}/threads/{threadId}/messages` · operation `listMessages` Requires the `batondeck:messages:read` permission (action `message:read`). **Parameters** | Name | In | Type | Required | Notes | | ---------- | ----- | ------- | -------- | ----- | | `slug` | path | string | yes | | | `threadId` | path | string | yes | | | `limit` | query | integer | yes | | **Responses** | Status | Meaning | | ------ | ----------------------------------------------------------------- | | 200 | The newest messages in one thread, oldest first within the window | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/threads/:threadId/messages' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## The owner has read this thread, through the message `through` names or, with no body, all of it (SPEC §7.6) [Section titled “The owner has read this thread, through the message through names or, with no body, all of it (SPEC §7.6)”](#the-owner-has-read-this-thread-through-the-message-through-names-or-with-no-body-all-of-it-spec-76) `POST /v1/identities/{slug}/threads/{threadId}/read` · operation `markThreadRead` Requires the `batondeck:messages:read` permission (action `message:read`). **Parameters** | Name | In | Type | Required | Notes | | ---------- | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `threadId` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | --------- | ------ | -------- | --------- | | `through` | string | | ≥ 1 chars | **Responses** | Status | Meaning | | ------ | ----------------------------------------------------------------------------------------------------------- | | 200 | The owner has read this thread, through the message `through` names or, with no body, all of it (SPEC §7.6) | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/threads/:threadId/read' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
# Identity: wallet request
> Mint a CSR and open a single-use request for the wallet page to sign (onboarding/wallet design §1a); What the wallet did with a request: still open, the chain it issued, or why it did not
## Mint a CSR and open a single-use request for the wallet page to sign (onboarding/wallet design §1a) [Section titled “Mint a CSR and open a single-use request for the wallet page to sign (onboarding/wallet design §1a)”](#mint-a-csr-and-open-a-single-use-request-for-the-wallet-page-to-sign-onboardingwallet-design-1a) `POST /v1/identities/{slug}/wallet-request` · operation `openWalletRequest` Requires the `batondeck:identities:manage` permission (action `cert:csr`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | ---------- | ----------- | ---------- | ---------------------- | | `purpose` | “signup” \\ | “renew” \\ | “move” | | `endpoint` | string | | ≥ 1 chars, ≤ 512 chars | **Responses** | Status | Meaning | | ------ | --------------------------------------------------------------------------------------------------- | | 201 | Mint a CSR and open a single-use request for the wallet page to sign (onboarding/wallet design §1a) | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/identities/:slug/wallet-request' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## What the wallet did with a request: still open, the chain it issued, or why it did not [Section titled “What the wallet did with a request: still open, the chain it issued, or why it did not”](#what-the-wallet-did-with-a-request-still-open-the-chain-it-issued-or-why-it-did-not) `GET /v1/identities/{slug}/wallet-request/{code}` · operation `readWalletRequest` Requires the `batondeck:identities:read` permission (action `cert:read`). **Parameters** | Name | In | Type | Required | Notes | | ------ | ---- | ------ | -------- | ----- | | `slug` | path | string | yes | | | `code` | path | string | yes | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------------------------------------------- | | 200 | What the wallet did with a request: still open, the chain it issued, or why it did not | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/identities/:slug/wallet-request/:code' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Integrations
> The MCP servers that connect in one click: each id, name, category, one line, endpoint and icon
## The MCP servers that connect in one click: each id, name, category, one line, endpoint and icon [Section titled “The MCP servers that connect in one click: each id, name, category, one line, endpoint and icon”](#the-mcp-servers-that-connect-in-one-click-each-id-name-category-one-line-endpoint-and-icon) `GET /v1/integrations/catalogue` · operation `listIntegrationCatalogue` Requires the `batondeck:identities:read` permission (action `identity:list`). **Responses** | Status | Meaning | | ------ | ----------------------------------------------------------------------------------------------- | | 200 | The MCP servers that connect in one click: each id, name, category, one line, endpoint and icon | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/integrations/catalogue' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Presets
> The permission presets a contact or an invite can be put on
## The permission presets a contact or an invite can be put on [Section titled “The permission presets a contact or an invite can be put on”](#the-permission-presets-a-contact-or-an-invite-can-be-put-on) `GET /v1/presets` · operation `listPresets` Requires the `batondeck:identities:read` permission (action `identity:list`). **Responses** | Status | Meaning | | ------ | ----------------------------------------------------------- | | 200 | The permission presets a contact or an invite can be put on | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/presets' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Sessions
> The caller's own signed-in sessions, across every workspace, newest first; the one this request came in on is marked; Sign one of the caller's other sessions out; it is refused on its next request; Sign out every session of the caller's but the one this request came in on
## The caller’s own signed-in sessions, across every workspace, newest first; the one this request came in on is marked [Section titled “The caller’s own signed-in sessions, across every workspace, newest first; the one this request came in on is marked”](#the-callers-own-signed-in-sessions-across-every-workspace-newest-first-the-one-this-request-came-in-on-is-marked) `GET /v1/sessions` · operation `listSessions` Requires the `batondeck:workspace:read` permission (action `session:list`). **Responses** | Status | Meaning | | ------ | -------------------------------------------------------------------------------------------------------------------- | | 200 | The caller’s own signed-in sessions, across every workspace, newest first; the one this request came in on is marked | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/sessions' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Sign one of the caller’s other sessions out; it is refused on its next request [Section titled “Sign one of the caller’s other sessions out; it is refused on its next request”](#sign-one-of-the-callers-other-sessions-out-it-is-refused-on-its-next-request) `DELETE /v1/sessions/{id}` · operation `revokeSession` Requires the `batondeck:workspace:read` permission (action `session:revoke`). Allowed while the workspace is paused, suspended or on deletion hold: it only takes access away. **Parameters** | Name | In | Type | Required | Notes | | ---- | ---- | ------ | -------- | ----- | | `id` | path | string | yes | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 204 | Done. No body. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X DELETE 'https://api.batondeck.com/v1/sessions/:id' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Sign out every session of the caller’s but the one this request came in on [Section titled “Sign out every session of the caller’s but the one this request came in on”](#sign-out-every-session-of-the-callers-but-the-one-this-request-came-in-on) `POST /v1/sessions/sign-out-others` · operation `revokeOtherSessions` Requires the `batondeck:workspace:read` permission (action `session:revoke`). Allowed while the workspace is paused, suspended or on deletion hold: it only takes access away. **Responses** | Status | Meaning | | ------ | -------------------------------------------------------------------------- | | 200 | Sign out every session of the caller’s but the one this request came in on | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/sessions/sign-out-others' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Statuses
> The choices an identity's status may be set to, in the order to offer them, and how recent a use keeps Auto available
## The choices an identity’s status may be set to, in the order to offer them, and how recent a use keeps Auto available [Section titled “The choices an identity’s status may be set to, in the order to offer them, and how recent a use keeps Auto available”](#the-choices-an-identitys-status-may-be-set-to-in-the-order-to-offer-them-and-how-recent-a-use-keeps-auto-available) `GET /v1/statuses` · operation `listStatusChoices` Requires the `batondeck:identities:read` permission (action `identity:list`). **Responses** | Status | Meaning | | ------ | --------------------------------------------------------------------------------------------------------------------- | | 200 | The choices an identity’s status may be set to, in the order to offer them, and how recent a use keeps Auto available | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/statuses' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Workspace
> The workspace this session is signed in to; Rename the workspace
## The workspace this session is signed in to [Section titled “The workspace this session is signed in to”](#the-workspace-this-session-is-signed-in-to) `GET /v1/workspace` · operation `getWorkspace` Requires the `batondeck:workspace:read` permission (action `workspace:read`). **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 200 | The workspace this session is signed in to | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/workspace' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Rename the workspace [Section titled “Rename the workspace”](#rename-the-workspace) `PATCH /v1/workspace` · operation `updateWorkspace` Requires the `batondeck:workspace:admin` permission (action `workspace:update`). Refused while the workspace is suspended or on deletion hold. **Request body** (`application/json`) | Field | Type | Required | Notes | | ----------------------- | ---------- | -------- | --------------------- | | `name` | string | | ≥ 1 chars, ≤ 64 chars | | `session_max_age_hours` | integer \\ | null | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 200 | Rename the workspace | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X PATCH 'https://api.batondeck.com/v1/workspace' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
# Workspace: audit
> The newest rows of the workspace's audit chain, oldest first within the window
## The newest rows of the workspace’s audit chain, oldest first within the window [Section titled “The newest rows of the workspace’s audit chain, oldest first within the window”](#the-newest-rows-of-the-workspaces-audit-chain-oldest-first-within-the-window) `GET /v1/workspace/audit` · operation `listWorkspaceAudit` Requires the `batondeck:audit:read` permission (action `audit:read`). **Parameters** | Name | In | Type | Required | Notes | | ------- | ----- | ------- | -------- | ----- | | `limit` | query | integer | yes | | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------------ | | 200 | The newest rows of the workspace’s audit chain, oldest first within the window | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/workspace/audit' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Workspace: billing
> Whether a plan can be bought here, which ones, and whether a subscription exists to manage; A Stripe Checkout link for a paid plan (review P-14); A link to Stripe's customer portal, where the subscription is changed or cancelled
## Whether a plan can be bought here, which ones, and whether a subscription exists to manage [Section titled “Whether a plan can be bought here, which ones, and whether a subscription exists to manage”](#whether-a-plan-can-be-bought-here-which-ones-and-whether-a-subscription-exists-to-manage) `GET /v1/workspace/billing` · operation `getBilling` Requires the `batondeck:billing:read` permission (action `plan:read`). **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------ | | 200 | Whether a plan can be bought here, which ones, and whether a subscription exists to manage | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/workspace/billing' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## A Stripe Checkout link for a paid plan (review P-14) [Section titled “A Stripe Checkout link for a paid plan (review P-14)”](#a-stripe-checkout-link-for-a-paid-plan-review-p-14) `POST /v1/workspace/billing/checkout` · operation `openCheckout` Requires the `batondeck:billing:manage` permission (action `plan:change`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Request body** (`application/json`) | Field | Type | Required | Notes | | ------ | -------- | --------- | ------------ | | `plan` | “pro” \\ | “team” \\ | “enterprise” | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------- | | 200 | A Stripe Checkout link for a paid plan (review P-14) | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/workspace/billing/checkout' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## A link to Stripe’s customer portal, where the subscription is changed or cancelled [Section titled “A link to Stripe’s customer portal, where the subscription is changed or cancelled”](#a-link-to-stripes-customer-portal-where-the-subscription-is-changed-or-cancelled) `POST /v1/workspace/billing/portal` · operation `openBillingPortal` Requires the `batondeck:billing:manage` permission (action `plan:change`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Request body** (`application/json`) **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------------- | | 200 | A link to Stripe’s customer portal, where the subscription is changed or cancelled | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/workspace/billing/portal' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
# Workspace: deletion
> Schedule this workspace for deletion, after a seven-day hold; Change your mind inside the seven days
## Schedule this workspace for deletion, after a seven-day hold [Section titled “Schedule this workspace for deletion, after a seven-day hold”](#schedule-this-workspace-for-deletion-after-a-seven-day-hold) `POST /v1/workspace/deletion` · operation `requestWorkspaceDeletion` Requires the `batondeck:workspace:admin` permission (action `workspace:delete`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Request body** (`application/json`) | Field | Type | Required | Notes | | -------- | ------ | -------- | ----------- | | `reason` | string | | ≤ 500 chars | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------ | | 201 | Schedule this workspace for deletion, after a seven-day hold | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/workspace/deletion' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Change your mind inside the seven days [Section titled “Change your mind inside the seven days”](#change-your-mind-inside-the-seven-days) `DELETE /v1/workspace/deletion` · operation `cancelWorkspaceDeletion` Requires the `batondeck:workspace:admin` permission (action `workspace:cancel-deletion`). Refused while the workspace is suspended or on deletion hold. **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 200 | Change your mind inside the seven days | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X DELETE 'https://api.batondeck.com/v1/workspace/deletion' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Workspace: domains
> Every hostname the workspace holds, the platform name included; Add a custom domain, on a plan that carries `custom_domain`; answers the DNS records to create; One custom domain: its status, the DNS records to create and what is still needed, read live; Remove a custom domain no identity lives at; i
## Every hostname the workspace holds, the platform name included [Section titled “Every hostname the workspace holds, the platform name included”](#every-hostname-the-workspace-holds-the-platform-name-included) `GET /v1/workspace/domains` · operation `listDomains` Requires the `batondeck:workspace:read` permission (action `workspace:read`). **Responses** | Status | Meaning | | ------ | -------------------------------------------------------------- | | 200 | Every hostname the workspace holds, the platform name included | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/workspace/domains' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Add a custom domain, on a plan that carries `custom_domain`; answers the DNS records to create [Section titled “Add a custom domain, on a plan that carries custom_domain; answers the DNS records to create”](#add-a-custom-domain-on-a-plan-that-carries-custom_domain-answers-the-dns-records-to-create) `POST /v1/workspace/domains` · operation `addCustomDomain` Requires the `batondeck:workspace:admin` permission (action `domain:manage`). Refused while the workspace is suspended or on deletion hold. **Request body** (`application/json`) | Field | Type | Required | Notes | | ---------- | ------ | -------- | ---------------------- | | `hostname` | string | yes | ≥ 3 chars, ≤ 253 chars | **Responses** | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------- | | 201 | Add a custom domain, on a plan that carries `custom_domain`; answers the DNS records to create | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/workspace/domains' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## One custom domain: its status, the DNS records to create and what is still needed, read live [Section titled “One custom domain: its status, the DNS records to create and what is still needed, read live”](#one-custom-domain-its-status-the-dns-records-to-create-and-what-is-still-needed-read-live) `GET /v1/workspace/domains/{hostname}` · operation `getCustomDomain` Requires the `batondeck:workspace:read` permission (action `workspace:read`). **Parameters** | Name | In | Type | Required | Notes | | ---------- | ---- | ------ | -------- | ----- | | `hostname` | path | string | yes | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------------------------------------------------- | | 200 | One custom domain: its status, the DNS records to create and what is still needed, read live | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/workspace/domains/:hostname' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Remove a custom domain no identity lives at; its certificate stops being served [Section titled “Remove a custom domain no identity lives at; its certificate stops being served”](#remove-a-custom-domain-no-identity-lives-at-its-certificate-stops-being-served) `DELETE /v1/workspace/domains/{hostname}` · operation `removeCustomDomain` Requires the `batondeck:workspace:admin` permission (action `domain:remove`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ---------- | ---- | ------ | -------- | ----- | | `hostname` | path | string | yes | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 204 | Done. No body. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X DELETE 'https://api.batondeck.com/v1/workspace/domains/:hostname' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Check the domain now instead of waiting for the five-minute poll, and record the result [Section titled “Check the domain now instead of waiting for the five-minute poll, and record the result”](#check-the-domain-now-instead-of-waiting-for-the-five-minute-poll-and-record-the-result) `POST /v1/workspace/domains/{hostname}/check` · operation `checkCustomDomain` Requires the `batondeck:workspace:admin` permission (action `domain:manage`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ---------- | ---- | ------ | -------- | ----- | | `hostname` | path | string | yes | | **Responses** | Status | Meaning | | ------ | --------------------------------------------------------------------------------------- | | 200 | Check the domain now instead of waiting for the five-minute poll, and record the result | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/workspace/domains/:hostname/check' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Workspace: export
> Everything this workspace holds, as one unencrypted zip streamed and never stored: the control-plane document, every identity's export zip, and an index
## Everything this workspace holds, as one unencrypted zip streamed and never stored: the control-plane document, every identity’s export zip, and an index [Section titled “Everything this workspace holds, as one unencrypted zip streamed and never stored: the control-plane document, every identity’s export zip, and an index”](#everything-this-workspace-holds-as-one-unencrypted-zip-streamed-and-never-stored-the-control-plane-document-every-identitys-export-zip-and-an-index) `GET /v1/workspace/export` · operation `exportWorkspace` Requires the `batondeck:workspace:admin` permission (action `export:read`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. **Parameters** | Name | In | Type | Required | Notes | | ------------- | ----- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `acknowledge` | query | string | | Must be “unencrypted”: This file is not encrypted. Anyone who gets it can read your contact list and all your conversations and files. It holds no keys, so it cannot be used to speak as you. Keep it where you keep private documents, and delete it once it has been imported. | **Responses** | Status | Meaning | | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | 200 | Everything this workspace holds, as one unencrypted zip streamed and never stored: the control-plane document, every identity’s export zip, and an index | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/workspace/export' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Workspace: keys
> Agent keys: the caller's own, or every owner's for an admin. Never the keys themselves; Mint an agent key for the caller. The key is in this response and nowhere else, ever; Revoke an agent key: the caller's own, or any for an admin. The row stays, so the audit rows it wrote still name it
## Agent keys: the caller’s own, or every owner’s for an admin. Never the keys themselves [Section titled “Agent keys: the caller’s own, or every owner’s for an admin. Never the keys themselves”](#agent-keys-the-callers-own-or-every-owners-for-an-admin-never-the-keys-themselves) `GET /v1/workspace/keys` · operation `listApiKeys` Requires the `batondeck:workspace:read` permission (action `apikey:read`). **Responses** | Status | Meaning | | ------ | -------------------------------------------------------------------------------------- | | 200 | Agent keys: the caller’s own, or every owner’s for an admin. Never the keys themselves | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/workspace/keys' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Mint an agent key for the caller. The key is in this response and nowhere else, ever [Section titled “Mint an agent key for the caller. The key is in this response and nowhere else, ever”](#mint-an-agent-key-for-the-caller-the-key-is-in-this-response-and-nowhere-else-ever) `POST /v1/workspace/keys` · operation `mintApiKey` Requires the `batondeck:workspace:read` permission (action `apikey:mint`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Request body** (`application/json`) | Field | Type | Required | Notes | | ----------------- | --------------- | -------- | ---------------------- | | `name` | string | yes | ≥ 1 chars, ≤ 100 chars | | `scopes` | array of string | | | | `everything` | boolean | | | | `identity_ids` | array of string | | | | `expires_in_days` | integer | | min 1, max 365 | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------------------ | | 201 | Mint an agent key for the caller. The key is in this response and nowhere else, ever | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/workspace/keys' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Revoke an agent key: the caller’s own, or any for an admin. The row stays, so the audit rows it wrote still name it [Section titled “Revoke an agent key: the caller’s own, or any for an admin. The row stays, so the audit rows it wrote still name it”](#revoke-an-agent-key-the-callers-own-or-any-for-an-admin-the-row-stays-so-the-audit-rows-it-wrote-still-name-it) `DELETE /v1/workspace/keys/{id}` · operation `revokeApiKey` Requires the `batondeck:workspace:read` permission (action `apikey:revoke`). Allowed while the workspace is paused, suspended or on deletion hold: it only takes access away. **Parameters** | Name | In | Type | Required | Notes | | ---- | ---- | ------ | -------- | ----- | | `id` | path | string | yes | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 204 | Done. No body. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X DELETE 'https://api.batondeck.com/v1/workspace/keys/:id' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Workspace: members
> The owners in the workspace and which of them administer it
## The owners in the workspace and which of them administer it [Section titled “The owners in the workspace and which of them administer it”](#the-owners-in-the-workspace-and-which-of-them-administer-it) `GET /v1/workspace/members` · operation `listMembers` Requires the `batondeck:members:read` permission (action `member:list`). **Responses** | Status | Meaning | | ------ | ----------------------------------------------------------- | | 200 | The owners in the workspace and which of them administer it | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/workspace/members' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Workspace: pause
> Pause the workspace: restriction of processing, which you set and you lift; Lift your own pause
## Pause the workspace: restriction of processing, which you set and you lift [Section titled “Pause the workspace: restriction of processing, which you set and you lift”](#pause-the-workspace-restriction-of-processing-which-you-set-and-you-lift) `POST /v1/workspace/pause` · operation `pauseWorkspace` Requires the `batondeck:workspace:admin` permission (action `workspace:pause`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Request body** (`application/json`) | Field | Type | Required | Notes | | -------- | ------ | -------- | ----------- | | `reason` | string | | ≤ 500 chars | **Responses** | Status | Meaning | | ------ | -------------------------------------------------------------------------- | | 201 | Pause the workspace: restriction of processing, which you set and you lift | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/workspace/pause' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Lift your own pause [Section titled “Lift your own pause”](#lift-your-own-pause) `DELETE /v1/workspace/pause` · operation `resumeWorkspace` Requires the `batondeck:workspace:admin` permission (action `workspace:resume`). Refused while the workspace is suspended or on deletion hold. **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 200 | Lift your own pause | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X DELETE 'https://api.batondeck.com/v1/workspace/pause' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Workspace: plan
> The plan in force and every entitlement it resolves to
## The plan in force and every entitlement it resolves to [Section titled “The plan in force and every entitlement it resolves to”](#the-plan-in-force-and-every-entitlement-it-resolves-to) `GET /v1/workspace/plan` · operation `getWorkspacePlan` Requires the `batondeck:billing:read` permission (action `plan:read`). **Responses** | Status | Meaning | | ------ | ------------------------------------------------------ | | 200 | The plan in force and every entitlement it resolves to | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/workspace/plan' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Workspace: residency
> Where this workspace's data is held, and whether the Enterprise residency guarantee is in effect
## Where this workspace’s data is held, and whether the Enterprise residency guarantee is in effect [Section titled “Where this workspace’s data is held, and whether the Enterprise residency guarantee is in effect”](#where-this-workspaces-data-is-held-and-whether-the-enterprise-residency-guarantee-is-in-effect) `GET /v1/workspace/residency` · operation `getResidency` Requires the `batondeck:workspace:read` permission (action `residency:read`). **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------ | | 200 | Where this workspace’s data is held, and whether the Enterprise residency guarantee is in effect | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/workspace/residency' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Workspace: settings
> The five knobs, each fixed by the platform, with the reason
## The five knobs, each fixed by the platform, with the reason [Section titled “The five knobs, each fixed by the platform, with the reason”](#the-five-knobs-each-fixed-by-the-platform-with-the-reason) `GET /v1/workspace/settings` · operation `listSettings` Requires the `batondeck:workspace:read` permission (action `workspace:read`). **Responses** | Status | Meaning | | ------ | ----------------------------------------------------------- | | 200 | The five knobs, each fixed by the platform, with the reason | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/workspace/settings' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Workspace: sso
> Whether this workspace requires single sign-on, its connections and its domains; Require single sign-on for this workspace, or stop requiring it; A short-lived link into the identity provider's Admin Portal, to set up a connection or verify a domain
## Whether this workspace requires single sign-on, its connections and its domains [Section titled “Whether this workspace requires single sign-on, its connections and its domains”](#whether-this-workspace-requires-single-sign-on-its-connections-and-its-domains) `GET /v1/workspace/sso` · operation `getSso` Requires the `batondeck:workspace:read` permission (action `sso:read`). **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------------- | | 200 | Whether this workspace requires single sign-on, its connections and its domains | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/workspace/sso' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Require single sign-on for this workspace, or stop requiring it [Section titled “Require single sign-on for this workspace, or stop requiring it”](#require-single-sign-on-for-this-workspace-or-stop-requiring-it) `PATCH /v1/workspace/sso` · operation `setSsoRequired` Requires the `batondeck:workspace:admin` permission (action `sso:manage`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Request body** (`application/json`) | Field | Type | Required | Notes | | ---------- | ------- | -------- | ----- | | `required` | boolean | yes | | **Responses** | Status | Meaning | | ------ | --------------------------------------------------------------- | | 200 | Require single sign-on for this workspace, or stop requiring it | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X PATCH 'https://api.batondeck.com/v1/workspace/sso' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## A short-lived link into the identity provider’s Admin Portal, to set up a connection or verify a domain [Section titled “A short-lived link into the identity provider’s Admin Portal, to set up a connection or verify a domain”](#a-short-lived-link-into-the-identity-providers-admin-portal-to-set-up-a-connection-or-verify-a-domain) `POST /v1/workspace/sso/portal-link` · operation `createSsoPortalLink` Requires the `batondeck:workspace:admin` permission (action `sso:manage`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Request body** (`application/json`) | Field | Type | Required | Notes | | -------- | -------- | --------------------- | ----- | | `intent` | “sso” \\ | “domain_verification” | yes | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------------- | | 200 | A short-lived link into the identity provider’s Admin Portal, to set up a connection or verify a domain | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/workspace/sso/portal-link' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
# Workspace: usage
> Rolled-up usage, newest day first, one row per day and metric
## Rolled-up usage, newest day first, one row per day and metric [Section titled “Rolled-up usage, newest day first, one row per day and metric”](#rolled-up-usage-newest-day-first-one-row-per-day-and-metric) `GET /v1/workspace/usage` · operation `listUsage` Requires the `batondeck:billing:read` permission (action `plan:read`). **Parameters** | Name | In | Type | Required | Notes | | ------- | ----- | ------- | -------- | ----- | | `limit` | query | integer | yes | | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------- | | 200 | Rolled-up usage, newest day first, one row per day and metric | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/workspace/usage' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# Workspace: webhooks
> The workspace's webhook endpoints. Never their signing secrets; Register an endpoint. The signing secret is in this answer and nowhere else; A new signing secret; the old one keeps working for 24 hours; Pause an endpoint, or resume one — resuming clears its failure run; Remove an endpoint. Its deliv
## The workspace’s webhook endpoints. Never their signing secrets [Section titled “The workspace’s webhook endpoints. Never their signing secrets”](#the-workspaces-webhook-endpoints-never-their-signing-secrets) `GET /v1/workspace/webhooks` · operation `listWebhooks` Requires the `batondeck:workspace:admin` permission (action `webhook:read`). **Responses** | Status | Meaning | | ------ | -------------------------------------------------------------- | | 200 | The workspace’s webhook endpoints. Never their signing secrets | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/workspace/webhooks' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Register an endpoint. The signing secret is in this answer and nowhere else [Section titled “Register an endpoint. The signing secret is in this answer and nowhere else”](#register-an-endpoint-the-signing-secret-is-in-this-answer-and-nowhere-else) `POST /v1/workspace/webhooks` · operation `createWebhook` Requires the `batondeck:workspace:admin` permission (action `webhook:create`). Requires a step-up: MFA enrolled and re-authenticated within 15 minutes. **Not reachable with an API key** — a key has no session and so can never step up. Refused while the workspace is suspended or on deletion hold. **Request body** (`application/json`) | Field | Type | Required | Notes | | -------- | --------------- | -------- | ----------- | | `url` | string | yes | ≥ 1 chars | | `events` | array of string | yes | default \[] | **Responses** | Status | Meaning | | ------ | --------------------------------------------------------------------------- | | 201 | Register an endpoint. The signing secret is in this answer and nowhere else | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/workspace/webhooks' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## A new signing secret; the old one keeps working for 24 hours [Section titled “A new signing secret; the old one keeps working for 24 hours”](#a-new-signing-secret-the-old-one-keeps-working-for-24-hours) `POST /v1/workspace/webhooks/{id}/secret` · operation `rotateWebhookSecret` Requires the `batondeck:workspace:admin` permission (action `webhook:manage`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ---- | ---- | ------ | -------- | ----- | | `id` | path | string | yes | | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------ | | 200 | A new signing secret; the old one keeps working for 24 hours | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X POST 'https://api.batondeck.com/v1/workspace/webhooks/:id/secret' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## Pause an endpoint, or resume one — resuming clears its failure run [Section titled “Pause an endpoint, or resume one — resuming clears its failure run”](#pause-an-endpoint-or-resume-one--resuming-clears-its-failure-run) `PATCH /v1/workspace/webhooks/{id}` · operation `setWebhookStatus` Requires the `batondeck:workspace:admin` permission (action `webhook:manage`). Refused while the workspace is suspended or on deletion hold. **Parameters** | Name | In | Type | Required | Notes | | ---- | ---- | ------ | -------- | ----- | | `id` | path | string | yes | | **Request body** (`application/json`) | Field | Type | Required | Notes | | -------- | ----------- | -------- | ----- | | `status` | “active” \\ | “paused” | yes | **Responses** | Status | Meaning | | ------ | ------------------------------------------------------------------ | | 200 | Pause an endpoint, or resume one — resuming clears its failure run | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X PATCH 'https://api.batondeck.com/v1/workspace/webhooks/:id' \
-H "Authorization: Bearer $BATONDECK_API_KEY" \
-H 'content-type: application/json' \
-d @body.json
```
## Remove an endpoint. Its delivery log stays, so what it did is still readable [Section titled “Remove an endpoint. Its delivery log stays, so what it did is still readable”](#remove-an-endpoint-its-delivery-log-stays-so-what-it-did-is-still-readable) `DELETE /v1/workspace/webhooks/{id}` · operation `deleteWebhook` Requires the `batondeck:workspace:admin` permission (action `webhook:delete`). Allowed while the workspace is paused, suspended or on deletion hold: it only takes access away. **Parameters** | Name | In | Type | Required | Notes | | ---- | ---- | ------ | -------- | ----- | | `id` | path | string | yes | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 204 | Done. No body. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X DELETE 'https://api.batondeck.com/v1/workspace/webhooks/:id' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
## What was attempted, when, and what came back [Section titled “What was attempted, when, and what came back”](#what-was-attempted-when-and-what-came-back) `GET /v1/workspace/webhooks/{id}/deliveries` · operation `listWebhookDeliveries` Requires the `batondeck:workspace:admin` permission (action `webhook:read`). **Parameters** | Name | In | Type | Required | Notes | | ------- | ----- | ------- | -------- | ----- | | `id` | path | string | yes | | | `limit` | query | integer | yes | | **Responses** | Status | Meaning | | ------ | -------------------------------------------------- | | 200 | What was attempted, when, and what came back | | 400 | The arguments did not validate. | | 401 | No portal session, and no live API key. | | 403 | The policy refused, or the request was cross-site. | | 404 | No such resource, or none this session may see. |
```sh
curl -X GET 'https://api.batondeck.com/v1/workspace/webhooks/:id/deliveries' \
-H "Authorization: Bearer $BATONDECK_API_KEY"
```
# The owner MCP server
> BatonDeck's owner MCP: 12 router tools over 95 actions, each one a /v1 operation.
**Endpoint:** `https://mcp.batondeck.com/mcp` · **Protocol:** `2026-07-28` (stateless, no handshake), and the `initialize` handshake at `2025-11-25` or `2025-06-18` · **Authorization:** OAuth, discovered from `https://mcp.batondeck.com/.well-known/oauth-protected-resource`. ## What the server tells a model [Section titled “What the server tells a model”](#what-the-server-tells-a-model) The server sends this text as its instructions on `initialize`: > The owner surface of a BatonDeck workspace: read and answer your inbox, manage the contacts who may reach you, issue invites, run your identities, their integrations and your workspace, and read the audit chain. Tools are routers named batondeck\_\\_\: `read` tools change nothing but your own read marker, `write` tools only add, `change` tools alter or remove. Each takes `action` and `arguments`, and lists every action’s arguments. Every call acts as ONE identity: the one named by `?identity=` on the server URL, else the first this connection may act as; `batondeck_identity_read` `accounts` lists them and marks the acting one. Every change is recorded on that identity’s audit chain, naming the owner behind this connection. ## Tools [Section titled “Tools”](#tools) The listing has 12 tools. Each takes `action` and `arguments`; the actions it accepts, and each action’s arguments, are below. A client that connects with `?expanded=true` on the server URL is listed one tool per action instead (95 tools, named `__`), with the same arguments. | Tool | Title | Actions | Hints | | ----------------------------------------------------------- | --------------------------------------- | ------- | -------------------------------------------------------------------------------------------------- | | [`batondeck_inbox_read`](#batondeck_inbox_read) | Read the inbox | 5 | `readOnlyHint: true` · `openWorldHint: false` | | [`batondeck_inbox_write`](#batondeck_inbox_write) | Send a message or a file | 2 | `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: true` | | [`batondeck_inbox_change`](#batondeck_inbox_change) | Call, retry or answer | 3 | `readOnlyHint: false` · `destructiveHint: true` · `idempotentHint: false` · `openWorldHint: true` | | [`batondeck_contacts_read`](#batondeck_contacts_read) | List contacts and invites | 6 | `readOnlyHint: true` · `openWorldHint: true` | | [`batondeck_contacts_change`](#batondeck_contacts_change) | Manage contacts and invites | 15 | `readOnlyHint: false` · `destructiveHint: true` · `idempotentHint: false` · `openWorldHint: true` | | [`batondeck_contacts_write`](#batondeck_contacts_write) | Invite or ask | 2 | `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: true` | | [`batondeck_identity_read`](#batondeck_identity_read) | Read the identity | 14 | `readOnlyHint: true` · `openWorldHint: false` | | [`batondeck_identity_change`](#batondeck_identity_change) | Change the identity | 10 | `readOnlyHint: false` · `destructiveHint: true` · `idempotentHint: false` · `openWorldHint: true` | | [`batondeck_identity_write`](#batondeck_identity_write) | Connect, share or open a wallet request | 5 | `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: true` | | [`batondeck_workspace_read`](#batondeck_workspace_read) | Read the workspace | 15 | `readOnlyHint: true` · `openWorldHint: true` | | [`batondeck_workspace_change`](#batondeck_workspace_change) | Change the workspace | 12 | `readOnlyHint: false` · `destructiveHint: true` · `idempotentHint: false` · `openWorldHint: true` | | [`batondeck_workspace_write`](#batondeck_workspace_write) | Create in the workspace | 6 | `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: true` | ### batondeck_inbox_read [Section titled “batondeck_inbox_read”](#batondeck_inbox_read) **Read the inbox.** Threads, messages, pending requests, waits and digests. Reading a thread moves your own read marker and nothing else. `readOnlyHint: true` · `openWorldHint: false` | Action | What it does | Arguments | Scope | /v1 operation | | --------- | ----------------------------------------------------------------------- | ------------------------ | ------------ | --------------------- | | `list` | Threads newest first, with unread counts | `limit`?, `cursor`? | `inbox:read` | `listThreads` | | `read` | The messages in one thread; reading advances your read cursor | `thread_id`, `limit`? | `inbox:read` | `listMessages` | | `pending` | Requests waiting for the owner to answer | — | `inbox:read` | `listPendingRequests` | | `wait` | Block until something changes, then report what moved since your cursor | `since`?, `timeout_sec`? | `inbox:read` | `watchChanges` | | `digest` | What happened in a window and what is still open, per contact | `since`? | `inbox:read` | `getDigest` | ### batondeck_inbox_write [Section titled “batondeck_inbox_write”](#batondeck_inbox_write) **Send a message or a file.** Send a message or a file to a contact. `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: true` | Action | What it does | Arguments | Scope | /v1 operation | | ----------- | -------------------------------- | -------------------------------------------------------------------- | ------------- | ------------- | | `send` | Send a message to a contact | `fingerprint`, `text`, `thread_id`?, `msg_id`? | `inbox:write` | `sendMessage` | | `send_file` | Send a file to a contact, base64 | `fingerprint`, `data`, `filename`?, `mime`?, `thread_id`?, `msg_id`? | `inbox:write` | `sendMedia` | ### batondeck_inbox_change [Section titled “batondeck_inbox_change”](#batondeck_inbox_change) **Call, retry or answer.** Call a tool on a contact’s node, retry a message that did not arrive, or answer a request a contact is waiting on. `readOnlyHint: false` · `destructiveHint: true` · `idempotentHint: false` · `openWorldHint: true` | Action | What it does | Arguments | Scope | /v1 operation | | -------- | ---------------------------------------------------------------- | ----------------------------------- | ----------------- | ---------------------- | | `call` | Call a tool on a contact’s node; their switchboard still applies | `fingerprint`, `tool`, `arguments`? | `inbox:write` | `callContactTool` | | `retry` | Retry an undelivered message | `message_id` | `inbox:write` | `retryMessage` | | `answer` | Answer an agent-answered request | `request_id`, `answer` | `requests:answer` | `answerPendingRequest` | ### batondeck_contacts_read [Section titled “batondeck_contacts_read”](#batondeck_contacts_read) **List contacts and invites.** Contacts with their status, preset and permissions, the tools a contact offers you, contacts waiting at a new address, and the invites this identity has issued. `readOnlyHint: true` · `openWorldHint: true` | Action | What it does | Arguments | Scope | /v1 operation | | ---------------- | ----------------------------------------------------------------- | ------------- | --------------- | ---------------------- | | `list` | Contacts with their tier and permissions | `status`? | `contacts:read` | `listContacts` | | `invites` | Invites this identity has issued: uses, and links if you may mint | — | `contacts:read` | `listInvites` | | `tools` | The tools a contact offers you, asked of their node | `fingerprint` | `contacts:read` | `listContactTools` | | `addresses` | Contacts waiting at a new address for your decision | — | `contacts:read` | `listPendingAddresses` | | `preset_catalog` | The shipped presets and every permission a contact can be granted | — | `identity:read` | `listPresets` | | `presets` | This identity’s own preset bundles, and which have been edited | — | `contacts:read` | `listIdentityPresets` | ### batondeck_contacts_change [Section titled “batondeck_contacts_change”](#batondeck_contacts_change) **Manage contacts and invites.** Approve, reject, block, unblock, remove, rename or refresh a contact, change what it may do or whether it may instruct you, decide on its new address, re-send your acceptance, accept its invite, or revoke one of yours. `readOnlyHint: false` · `destructiveHint: true` · `idempotentHint: false` · `openWorldHint: true` | Action | What it does | Arguments | Scope | /v1 operation | | ----------------- | ------------------------------------------------------------------------------ | ---------------------------------------- | ----------------- | ----------------------- | | `approve` | Approve a pending contact request, applying a preset | `fingerprint`, `preset`? | `contacts:manage` | `approveContact` | | `reject` | Refuse a pending contact request | `fingerprint` | `contacts:manage` | `rejectContact` | | `block` | Stop a contact reaching you. Silent: they are not told (HDTP §6.2) | `fingerprint` | `contacts:manage` | `blockContact` | | `unblock` | Undo a block: a former contact returns active, a declined request is forgotten | `fingerprint` | `contacts:manage` | `unblockContact` | | `remove` | Remove a contact; an active one is told | `fingerprint` | `contacts:manage` | `removeContact` | | `add` | Accept their invite link | `invite_url` | `contacts:manage` | `redeemInvite` | | `tell_accepted` | Re-send your acceptance | `fingerprint` | `contacts:manage` | `notifyAcceptance` | | `rename` | Set your own local name for a contact; empty clears it | `fingerprint`, `petname` | `contacts:manage` | `setPetname` | | `permissions` | Change what a contact may do | `fingerprint`, `preset`?, `permissions`? | `contacts:trust` | `updateContact` | | `trust` | Whether this contact’s text may instruct your agent | `fingerprint`, `trust` | `contacts:trust` | `setTrust` | | `refresh` | Re-fetch a contact’s card | `fingerprint` | `contacts:manage` | `refreshContact` | | `revoke_invite` | Revoke an invite; its link stops working | `invite_id` | `invites:manage` | `revokeInvite` | | `approve_address` | Re-pin a contact at the new address it is waiting at | `root` | `contacts:manage` | `approvePendingAddress` | | `reject_address` | Keep the pin where it is; the new address is a stranger | `root` | `contacts:manage` | `rejectPendingAddress` | | `set_preset` | Change what a preset grants, for this identity; existing contacts keep theirs | `name`, `permissions` | `contacts:trust` | `setIdentityPreset` | ### batondeck_contacts_write [Section titled “batondeck_contacts_write”](#batondeck_contacts_write) **Invite or ask.** Mint an invite link, or ask the holder of a contact card to be your contact. `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: true` | Action | What it does | Arguments | Scope | /v1 operation | | --------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | ----------------- | ---------------- | | `invite` | Mint an invite link, optionally auto-accepting | `label`?, `preset`?, `max_uses`?, `auto_accept`?, `expires_in_days`? | `invites:manage` | `createInvite` | | `request` | Ask the holder of a contact card to be your contact; they approve it | `card`, `note`? | `contacts:manage` | `requestContact` | ### batondeck_identity_read [Section titled “batondeck_identity_read”](#batondeck_identity_read) **Read the identity.** Accounts, this identity, its card, certificate, audit chain, storage and grants, a wallet request, and its integrations and what each exposes. `readOnlyHint: true` · `openWorldHint: false` | Action | What it does | Arguments | Scope | /v1 operation | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ------------------- | -------------------------- | | `accounts` | The identities this connection may act as; the acting one is marked, another is reached with ?identity= | — | `identity:read` | `listIdentities` | | `card` | The signed card peers receive | — | `identity:read` | `getIdentityCard` | | `certificate` | Certificate state | — | `identity:read` | `getCertificate` | | `audit` | The audit chain, newest first | `limit`?, `subject`?, `action_like`? | `audit:read` | `listIdentityAudit` | | `integrations` | Connected integrations and their health | — | `integrations:read` | `listIntegrations` | | `identity` | This identity: slug, root, address, status and whether it is certified | — | `identity:read` | `getIdentity` | | `storage` | Bytes this identity holds, and the ceiling it is held against | — | `identity:read` | `getStorage` | | `settings` | Your status, accept_new_hosts and moved_away_at, as last set | — | `identity:read` | `getIdentitySettings` | | `grants` | Which owners may act as this identity; several is a shared inbox | — | `identity:read` | `listIdentityGrants` | | `wallet_request` | What the wallet did with a request: still open, the chain it issued, or why not | `code` | `identity:read` | `readWalletRequest` | | `export_report` | What this identity’s export would say of itself: each import ceiling it passes, and every message it leaves out, by id and why | — | `identity:read` | `getExportReport` | | `export` | This identity’s export zip, unencrypted, base64, up to 5 MiB; send acknowledge: “unencrypted”; the owner approves it in the portal | `acknowledge`? | `export:read` | `exportIdentity` | | `exposure` | What an integration offers, and which of its tools contacts may reach | `integration` | `integrations:read` | `getExposure` | | `integration_catalogue` | The MCP servers that connect in one click | — | `integrations:read` | `listIntegrationCatalogue` | ### batondeck_identity_change [Section titled “batondeck_identity_change”](#batondeck_identity_change) **Change the identity.** Whether a contact at a new address is re-pinned without asking; an integration’s exposure, sign-in and disconnection; install a wallet’s chain, move, import, delete the identity, or take back a grant. The last five wait for the owner’s approval in the portal. `readOnlyHint: false` · `destructiveHint: true` · `idempotentHint: false` · `openWorldHint: true` | Action | What it does | Arguments | Scope | /v1 operation | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | -------------------- | ------------------------ | | `exposure` | Replace which of an integration’s tools contacts may reach | `integration`, `entries` | `integrations:write` | `setExposure` | | `settings` | Your status, which decides what contacts read; whether a contact at a new address is re-pinned without asking (auto) or held (ask); or, alone, moved_away (no renewal reminders) | `accept_new_hosts`?, `status`?, `moved_away`? | `identity:manage` | `updateIdentitySettings` | | `revoke_grant` | Take back an owner’s access to this identity; the owner approves it in the portal | `owner_id` | `identity:share` | `revokeIdentityGrant` | | `install_leaf` | Install the chain the wallet issued; the identity answers with the new key | `chain`, `credential_id`?, `backup_verified`? | `identity:lifecycle` | `installLeaf` | | `move_address` | Switch to the address the current leaf names; contacts are told | `address` | `identity:lifecycle` | `moveIdentityAddress` | | `cancel_import` | Close an open review of an export zip by its digest: the uploaded file goes, and nothing else changes | `digest` | `identity:lifecycle` | `cancelImportReview` | | `import_archive` | Take in the export zip a review holds, named by its digest; the owner approves it in the portal, shown the rows | `digest` | `identity:lifecycle` | `importArchive` | | `delete_identity` | Erase this identity, its messages and its address, at once; the owner approves it in the portal | — | `identity:lifecycle` | `deleteIdentity` | | `authorize_integration` | Start an integration’s OAuth sign-in; answers the URL the person opens | `integration`, `scope`? | `integrations:write` | `authorizeIntegration` | | `disconnect` | Disconnect an integration and forget its credential | `integration` | `integrations:write` | `deleteIntegration` | ### batondeck_identity_write [Section titled “batondeck_identity_write”](#batondeck_identity_write) **Connect, share or open a wallet request.** Connect an upstream MCP server; let another owner act as this identity; open a request for your wallet to sign, or mint a signing request. Sharing and signing wait for the owner’s approval in the portal. `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: true` | Action | What it does | Arguments | Scope | /v1 operation | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | -------------------- | --------------------- | | `grant` | Let another owner of the workspace act as this identity; the owner approves it in the portal | `owner_id` | `identity:share` | `grantIdentity` | | `open_wallet_request` | Open a request for your wallet to sign a new key (signup, renew, move, or return: a renewal that also hands back your wallet’s contact book); answers the page you sign on | `purpose`, `endpoint`? | `identity:lifecycle` | `openWalletRequest` | | `csr` | Mint a key and a signing request for a wallet you run yourself | `purpose`, `endpoint`? | `identity:lifecycle` | `issueCsr` | | `review_import` | Review an export zip (base64url): its contacts and what an import would do with each, and its digest; nothing is written | `archive` | `identity:lifecycle` | `reviewImportArchive` | | `connect` | Connect an MCP server (a catalogue id, or an endpoint); answers the sign-in URL when it needs one | `catalogue`?, `slug`?, `endpoint`?, `transport`?, `auth`? | `integrations:write` | `createIntegration` | ### batondeck_workspace_read [Section titled “batondeck_workspace_read”](#batondeck_workspace_read) **Read the workspace.** The workspace, its settings, residency, single sign-on, plan, billing, usage, members, audit, hostnames and a custom domain, agent keys, webhooks and their deliveries, exports, and an export’s archive (which waits for the owner’s approval in the portal). `readOnlyHint: true` · `openWorldHint: true` | Action | What it does | Arguments | Scope | /v1 operation | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------------------ | ----------------------- | | `settings` | The workspace’s settings, and why the platform fixes the ones it fixes | — | `workspace:read` | `listSettings` | | `workspace` | The workspace: name, status, plan, jurisdiction, any pause or deletion hold | — | `workspace:read` | `getWorkspace` | | `plan` | The plan and every entitlement in force, with where each comes from | — | `billing:read` | `getWorkspacePlan` | | `billing` | The subscription as billing knows it | — | `billing:read` | `getBilling` | | `usage` | Metered usage by day | `limit`? | `billing:read` | `listUsage` | | `members` | The owners in the workspace and which of them administer it | — | `members:read` | `listMembers` | | `audit` | The workspace’s audit: its chain and its events | `limit`? | `audit:read` | `listWorkspaceAudit` | | `domains` | The hostnames the workspace answers on | — | `workspace:read` | `listDomains` | | `keys` | Agent keys: name, prefix, scopes, who made it, last use; never a key. The owner’s own, or every key for an admin | — | `keys:read` | `listApiKeys` | | `webhooks` | Webhook endpoints and their state; never a secret | — | `credentials:read` | `listWebhooks` | | `webhook_deliveries` | An endpoint’s delivery log, newest first | `webhook_id`, `limit`? | `credentials:read` | `listWebhookDeliveries` | | `domain` | One custom domain: its status, the DNS record to create and what is still needed, read live | `hostname` | `workspace:read` | `getCustomDomain` | | `residency` | Where the workspace’s data is held, and whether the residency guarantee is in effect | — | `workspace:read` | `getResidency` | | `sso` | Whether the workspace requires single sign-on, its connections and its domains | — | `workspace:read` | `getSso` | | `download_export` | The workspace export zip, unencrypted, base64, up to 5 MiB; send acknowledge: “unencrypted”; the owner approves it in the portal | `acknowledge`? | `export:read` | `exportWorkspace` | ### batondeck_workspace_change [Section titled “batondeck_workspace_change”](#batondeck_workspace_change) **Change the workspace.** Change a setting or its name; pause, resume, schedule or cancel its deletion; require single sign-on; check or remove a custom domain; revoke a key; rotate, pause or remove a webhook. Pausing, deleting and single sign-on wait for the owner’s approval in the portal. `readOnlyHint: false` · `destructiveHint: true` · `idempotentHint: false` · `openWorldHint: true` | Action | What it does | Arguments | Scope | /v1 operation | | ----------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------- | -------------------- | -------------------------- | | `update` | Rename the workspace, or set how long a sign-in lasts | `name`?, `session_max_age_hours`? | `workspace:manage` | `updateWorkspace` | | `pause` | Pause the workspace (restriction of processing); the owner approves it in the portal | `reason`? | `workspace:admin` | `pauseWorkspace` | | `resume` | Lift your own pause | — | `workspace:admin` | `resumeWorkspace` | | `request_deletion` | Schedule the workspace for deletion after a seven-day hold; the owner approves it in the portal | `reason`? | `workspace:admin` | `requestWorkspaceDeletion` | | `cancel_deletion` | Cancel a scheduled deletion inside its seven days | — | `workspace:admin` | `cancelWorkspaceDeletion` | | `revoke_key` | Revoke an agent key: the owner’s own, or any for an admin; it stops working at once | `key_id` | `keys:manage` | `revokeApiKey` | | `rotate_webhook_secret` | Rotate an endpoint’s signing secret; the new one is in the answer once | `webhook_id` | `credentials:manage` | `rotateWebhookSecret` | | `set_webhook_status` | Pause an endpoint, or resume one | `webhook_id`, `status` | `credentials:manage` | `setWebhookStatus` | | `delete_webhook` | Remove a webhook endpoint; its delivery log stays | `webhook_id` | `credentials:manage` | `deleteWebhook` | | `check_domain` | Check a custom domain now, and record the result | `hostname` | `workspace:manage` | `checkCustomDomain` | | `remove_domain` | Remove a custom domain no identity lives at | `hostname` | `workspace:manage` | `removeCustomDomain` | | `set_sso_required` | Require single sign-on, or stop requiring it; the owner approves it in the portal | `required` | `workspace:admin` | `setSsoRequired` | ### batondeck_workspace_write [Section titled “batondeck_workspace_write”](#batondeck_workspace_write) **Create in the workspace.** Mint an agent key, register a webhook, start an export, or open a plan checkout, billing or single sign-on link — each waiting for the owner’s approval in the portal — or add a custom domain. `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: true` | Action | What it does | Arguments | Scope | /v1 operation | | ----------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- | -------------------- | --------------------- | | `checkout` | A Stripe Checkout link for a paid plan; the owner approves it in the portal first | `plan` | `billing:manage` | `openCheckout` | | `billing_portal` | A link to Stripe’s customer portal; the owner approves it in the portal first | — | `billing:manage` | `openBillingPortal` | | `mint_key` | Mint an agent key; the owner approves it in the portal, and the key is in the answer once | `name`, `scopes`?, `everything`?, `identity_ids`?, `expires_in_days`? | `keys:manage` | `mintApiKey` | | `create_webhook` | Register a webhook endpoint; the owner approves it in the portal, and the secret is in the answer once | `url`, `events`? | `credentials:manage` | `createWebhook` | | `add_domain` | Add a custom domain; answers the DNS records to create | `hostname` | `workspace:manage` | `addCustomDomain` | | `sso_portal_link` | A short-lived link to set up single sign-on or verify a domain; the owner approves it in the portal | `intent` | `workspace:admin` | `createSsoPortalLink` | ## Resources [Section titled “Resources”](#resources) | URI | Name | What it holds | | -------------------- | ------------------------------- | ------------------------------------------------------------- | | `hdtp://inbox` | inbox | Unread counts per thread, for a badge that costs no tool call | | `hdtp://requests` | contact requests | Contacts waiting for the owner to approve them | | `hdtp://pending` | pending agent-answered requests | Calls a contact made that this agent is expected to answer | | `hdtp://thread/{id}` | thread | One conversation’s state: unread count and when it last moved |
# Webhooks
> We POST to your URL when something happens on your workspace.
We POST to your URL when something happens on your workspace. This page is the contract: what arrives, how to verify it, and — the part worth reading twice — **what we do not promise**. ## Until webhooks are switched on for your workspace [Section titled “Until webhooks are switched on for your workspace”](#until-webhooks-are-switched-on-for-your-workspace) Webhooks are off until we switch them on, per workspace or for everyone. You can register an endpoint and rotate its secret while they are off, but nothing is delivered: no request is made and the endpoint’s delivery log stays empty. The **Webhooks** section under **Apps and keys** says when this is the case, and `GET /v1/workspace/webhooks` answers `"enabled": false`. Once they are on, events from that moment are delivered; nothing that happened while they were off is sent later. On staging (`app-stg.batondeck.com`) webhooks are on by default, so an endpoint registered there receives deliveries from the start; we can still switch them off for a workspace, and `"enabled"` says which. Production starts with them off. ## One attempt. No retries. [Section titled “One attempt. No retries.”](#one-attempt-no-retries) **A delivery is attempted exactly once.** If your endpoint is slow, down, or answers anything outside 2xx, that event is not sent again. The event passes through our own queue on its way to you, and that queue does not retry it either: no second attempt, no backoff, and no dead-letter replay. That is a deliberate choice, and the trade is yours to plan around: * **What you get**: a webhook sent soon after the event, not at the moment of it — the event waits in our queue first, usually for a few seconds and occasionally for much longer, because the queue does not bound that wait; then the one attempt is made — and a log in the portal showing every attempt, its HTTP status, its latency and the first 500 bytes your server answered with. * **What you do not get**: a guarantee that every event reaches you. A network blip on your side loses that event. **If you need certainty, treat the webhook as a hint and `/v1` as the record.** Poll the resource the event names — `GET /v1/identities/:slug/threads`, `/contacts`, and so on — and reconcile. A webhook tells you *when* to look; the API tells you *what is true*. If you need a queue with retries, put one in front of your own endpoint: accept the POST, enqueue it yourself, answer 200 immediately. We may add at-least-once delivery later. We will not remove this page’s promise without telling you. ## Verifying a delivery [Section titled “Verifying a delivery”](#verifying-a-delivery) Every request carries: | Header | What it is | | ------------------------ | --------------------------------------------------------------- | | `X-BatonDeck-Signature` | `t=,v1=` — see below | | `X-BatonDeck-Event-Id` | Stable per event. The same id never means two different things. | | `X-BatonDeck-Event-Type` | e.g. `message.received` | | `X-BatonDeck-Delivery` | This attempt. Quote it if you contact us. | The signature is HMAC-SHA256 over `` `${t}.${rawBody}` `` with your endpoint’s signing secret, hex-encoded. Verify it against the **raw** body, before any JSON parsing.
```js
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verify(secret, rawBody, header, nowSeconds = Date.now() / 1000) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
// Reject anything older than five minutes, or a captured delivery can be replayed
// against you for ever. This check is not optional.
if (Math.abs(nowSeconds - Number(parts.t)) > 300) return false
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')
// Constant time. A byte-by-byte compare leaks the signature to a patient attacker.
const a = Buffer.from(expected), b = Buffer.from(parts.v1 ?? '')
return a.length === b.length && timingSafeEqual(a, b)
}
```
**During a secret rotation the header carries two `v1` values** — the new secret first, the old one second — for 24 hours. Accept the delivery if *either* verifies, and you can deploy the new secret whenever suits you inside that window. ## What is in the body [Section titled “What is in the body”](#what-is-in-the-body) Ids and metadata. **Never a message body, media, a contact card, or key material.**
```json
{
"id": "evt_9f8c…",
"type": "message.received",
"created_at": 1757462400000,
"identity": "alice",
"data": { "thread_id": "th-1", "contact": "sha256:…" }
}
```
Use the ids to fetch what you need from `/v1` with an API key. That is the same rule as the retry policy above, for the same reason: the webhook is a notification, not a copy of your data. ## Endpoints [Section titled “Endpoints”](#endpoints) Registered under **Apps and keys** in the portal. * **https only.** A signature over plaintext still hands the payload to anyone on the path. * **A hostname, not an IP address**, and not a name that only resolves on your own network. Our requests come from Cloudflare, so a private address would never reach you. * **From the moment it is registered.** An endpoint is sent the events that happen after it is created, never one from before, even when that event is still on its way to you. * **An event filter, or none** — no filter means every event. A filter may name only events we send; a name that is reserved and not yet sent is refused rather than stored. * **Twenty consecutive failures pauses the endpoint.** We stop dialling a URL that has stopped answering; you resume it in the portal when it is fixed. The signing secret is shown once, when you create the endpoint and when you rotate it. It is stored sealed under your workspace’s key and opened only to sign a delivery; no route reads it back, so nobody here can look it up for you. Lost it? Rotate.