A long-lived key for scripts and CI: the same grant as an agent's, usable with curl on /v1/* or as a bearer on /mcp.

View as Markdown

API keys

An API key is a long-lived bearer for something that is not an MCP client: a shell script, a CI job, a cron. It uses the same tools a connected agent uses, either over /mcp or over the plain-HTTP facade at /v1/*, and nothing a key can do is something an agent could not.

A key is a grant

Minting a key on the dashboard (API keys → New API key) is the consent screen: a name, the permissions, which accounts, whether it signs on its own or asks you each time, and for how long. The permissions are the same scopes an agent gets — connectors:read, signin, approvals:read, audit:read — plus two that are off unless you tick them: enrol ("Add passkeys"), with which a key can add a passkey to a site you have signed in to and create subjects; and credentials:write ("Revoke passkeys"), with which it can revoke the passkeys and subjects it manages. A key holding enrol covers what it enrols (coverEnrolled, on by default), so one key serves every customer it signs up. What that writes is exactly what connecting an agent writes — an oauth_grants row, the unattended-signing policies if you chose them, and one access token of the grant with the grant's own lifetime. The token is the key: apk_key_…, shown once. There is no refresh token, because a script cannot hold a rotating one; "until I revoke it" still ends after 30 days, the ceiling every unattended grant has.

The key is audited under its own name, appears on the Agents page like any connection, and can only ever narrow: an account you did not tick, or a passkey you revoke later, is not reachable whatever the key says.

Using it with curl

export APK_KEY=apk_key_…
export APK=https://app.solospace.dev

curl -sS -H "Authorization: Bearer $APK_KEY" $APK/v1/access
curl -sS -H "Authorization: Bearer $APK_KEY" $APK/v1/connectors
curl -sS -H "Authorization: Bearer $APK_KEY" $APK/v1/connectors/github.com

# sign in: a ticket and the page script, then poll it
curl -sS -H "Authorization: Bearer $APK_KEY" -H "content-type: application/json" \
     -d '{"credentialId":"<from /v1/connectors>"}' $APK/v1/signin/begin
curl -sS -H "Authorization: Bearer $APK_KEY" $APK/v1/signin/<ticket>
Method and pathTool it calls
GET /v1/accesspasskey_check_access
GET /v1/connectorspasskey_list
GET /v1/connectors/:domainpasskey_connector_status
POST /v1/connectors/:domain/notespasskey_note
POST /v1/signin/beginpasskey_begin_signin
GET /v1/signin/:ticketpasskey_signin_status
POST /v1/signin/completepasskey_complete_signin
GET /v1/approvalspasskey_approvals
GET /v1/automationspasskey_automation_list
POST /v1/automationspasskey_automation_propose
POST /v1/runspasskey_run_begin
POST /v1/runs/:id/endpasskey_run_end
GET /v1/runs/:idpasskey_run_status
GET /v1/approvals/:idpasskey_approval_status
GET /v1/auditpasskey_audit
POST /v1/enrol/beginpasskey_begin_enrol — needs the enrol scope
GET /v1/enrol/:ticketpasskey_enrol_status
POST /v1/enrol/completepasskey_complete_enrol
POST /v1/credentialspasskey_enrol_direct
GET /v1/credentials/:idone passkey in full
POST /v1/credentials/:id/revokepasskey_revoke — needs credentials:write
POST /v1/subjects, GET /v1/subjects, GET /v1/subjects/:id, POST /v1/subjects/:id/revokesubjects
POST /v1/keys/rotatea replacement key, 24 h grace

Parameters, examples and every response shape are on the REST API reference; the machine-readable form is GET /v1/openapi.json.

A POST body is the tool's arguments as a JSON object; a path parameter is laid over it. The response body is the tool result — the same object an MCP client gets in structuredContent, with state, retryable, terminal and next first. The status follows state: 200 ok, 202 pending (a human is being asked; wait next.waitMs and poll), 403 denied (a decision on the merits — do not retry), 400 a mistake in the call, 404 a name nobody has, 429 a cap. A 401 means the key is dead or expired; a 403 with WWW-Authenticate: Bearer error="insufficient_scope" means the key was never granted that permission — mint one that has it.

Using it with an MCP client

Any MCP client that can send a static header skips the OAuth flow entirely:

{ "mcpServers": { "agent-passkey": {
    "url": "https://app.solospace.dev/mcp",
    "headers": { "Authorization": "Bearer apk_key_…" } } } }

Rotating

POST /v1/keys/rotate with the key as its own bearer returns a replacement — same grant, fresh expiry, shown once — and the old key keeps working for 24 hours. The key.expiring webhook fires seven days and one day before a key runs out, so rotation is a scheduled job rather than a surprise 401.

Revoking

API keys → Revoke, with the name typed. The grant, the key, the unattended-signing policies and the materialised agent go in one transaction, and the next request answers 401. Sign-ins it already made stay in the audit log. There is nothing cached to wait out.