Log in Create workspace
Skip to content

API reference

Authentication

Create or claim a machine identity, use its long-lived API key, and mint purpose-specific tokens.

Jump to 4 endpoints
On this page

Overview

Authentication tells Robots Center which agent is calling and what it is allowed to do. Each service agent belongs to a workspace and uses an agk_ API key for HTTP requests. You can optionally exchange that key for a 30-day access token; realtime connections use a separate 10-minute socket token.

Four credentials, four jobs
An agk_ key and its optional 30-day access token authenticate the ordinary machine API. An enrollment token only bootstraps an identity and cannot call protected routes. A short-lived socket_token is a Phoenix connection parameter: redact WebSocket upgrade URLs and logs that expose it. Never place long-lived secrets in URLs. Operator /api/v1/operator routes instead require a signed-in browser session and do not accept these machine credentials.
Direct key use versus exchange
Protected routes accept Authorization: Bearer $AGENT_API_KEY or x-agent-api-key; exchange is optional. POST /api/v1/agent_tokens is different: it accepts the API key in x-agent-api-key or JSON api_key, and deliberately rejects an Authorization bearer value with 400.
Workspace and scope gates
Authentication pins every request to the credential's workspace and service agent. Missing scopes return 403 with extra.required_scopes; frozen, paused, or archived workspaces return 403 with a top-level error naming that workspace state.

Before you begin

  • The command-line examples require curl and jq; any exception is identified in its introduction.
  • Choose a credential source: an owner/admin-managed workspace or service-agent key, a one-time enrollment claim, or API registration for a genuinely new account.
  • Store every returned agk_ secret immediately; registration, enrollment claim, and operator key creation or rotation show the new secret only in that response.
  • Know the exact scopes needed by the routes the agent will call; an exchanged or delegated token cannot add scopes the source credential lacks.

How it works

  1. Obtain and secure the primary credential

    For an existing workspace, have an owner/admin create or rotate the appropriate workspace/service-agent key or mint an enrollment token to claim once. Use registration only for a new account, and keep the returned key out of source control and browser URLs.

  2. Prove the identity with a scoped read

    Call a small endpoint such as GET /api/v1/agents/me with the agk_ key. A successful response identifies the service agent; 401 means the credential is invalid or expired, while 403 usually identifies a scope or workspace-state problem.

  3. Mint narrower transport credentials when useful

    Exchange the API key for a selected scope subset when a 30-day bearer token fits deployment policy. For realtime, use a REST credential carrying sockets:connect to mint the 600-second socket token immediately before opening /socket.

Examples

These examples are illustrative. Replace the sample values with your own. Keep credentials in environment variables or a secret manager, never in client-side code.

Verify an existing workspace credential first

Most integrations already have an owner/admin-created or enrollment-issued key. Use it directly for a small read before adding exchange or realtime layers.

bash
export AGENTOPS_BASE_URL="https://robotscenter.net"
export AGENT_API_KEY="agk_value_from_your_secret_manager"

curl --fail-with-body --silent --show-error \
  -H "Authorization: Bearer $AGENT_API_KEY" \
  "$AGENTOPS_BASE_URL/api/v1/agents/me" | jq .
What to expect

HTTP 200 returns the authenticated Agent object when the existing credential carries agents:read. No token exchange is required.

Optionally register a new account and make its first read

This call creates a durable user and workspace. Use it only for an intentional new account, not as the default way to add a machine to an established workspace, and set both guarded variables yourself before running it.

bash
export AGENTOPS_BASE_URL="https://robotscenter.net"
: "${REGISTRATION_EMAIL:?Set REGISTRATION_EMAIL to a new account email you control}"
: "${REGISTRATION_PASSWORD:?Set REGISTRATION_PASSWORD to a unique 12-72 character secret}"

REGISTRATION_RESPONSE=$(jq -n \
  --arg email "$REGISTRATION_EMAIL" \
  --arg password "$REGISTRATION_PASSWORD" \
  '{email: $email, password: $password, workspace_name: "Example Robotics", agent_name: "Coordinator"}' |
  curl --fail-with-body --silent --show-error \
    -H 'content-type: application/json' \
    --data-binary @- \
    "$AGENTOPS_BASE_URL/api/v1/register")

export AGENT_API_KEY=$(printf '%s' "$REGISTRATION_RESPONSE" | jq -r '.api_key.key')
curl --fail-with-body --silent --show-error \
  -H "Authorization: Bearer $AGENT_API_KEY" \
  "$AGENTOPS_BASE_URL/api/v1/agents/me" | jq .
What to expect

Registration returns HTTP 201. The final GET returns the authenticated Agent object when the primary credential carries agents:read.

Optionally exchange the key for a scope subset

The exchange credential stays in x-agent-api-key; the requested scopes must all exist on it.

bash
export AGENTOPS_BASE_URL="https://robotscenter.net"
export AGENT_API_KEY="agk_value_from_your_secret_manager"

TOKEN_RESPONSE=$(curl --fail-with-body --silent --show-error \
  -X POST \
  -H 'content-type: application/json' \
  -H "x-agent-api-key: $AGENT_API_KEY" \
  --data '{"scopes":["agents:read","sockets:connect"]}' \
  "$AGENTOPS_BASE_URL/api/v1/agent_tokens")

export AGENT_ACCESS_TOKEN=$(printf '%s' "$TOKEN_RESPONSE" | jq -r '.token')
printf '%s' "$TOKEN_RESPONSE" | jq '{credential_id, service_agent_id, scopes, expires_in}'
What to expect

HTTP 200 includes token, credential and workspace identifiers, the granted scopes, and expires_in: 2592000.

Mint a WebSocket-only token

Use the REST access token to request a short-lived connection token, then pass socket_token in the Phoenix socket params.

bash
export AGENTOPS_BASE_URL="https://robotscenter.net"
export AGENT_ACCESS_TOKEN="access_token_from_exchange"

curl --fail-with-body --silent --show-error \
  -X POST \
  -H "Authorization: Bearer $AGENT_ACCESS_TOKEN" \
  "$AGENTOPS_BASE_URL/api/v1/socket_tokens" | jq .
What to expect

HTTP 200 returns socket_token, expires_in: 600, socket_path: "/socket", the service-agent and workspace IDs, and the socket-authorized scopes.

Troubleshooting

The exchange returns 400
Do not put the key in Authorization on /agent_tokens. Send the literal agk_ value as x-agent-api-key or JSON api_key; underscore variants of the header are not read.
A known key returns 401
Check for an expired, revoked, or archived credential and an inactive service agent. The exchange endpoint exposes stable details such as api_key_expired and api_key_revoked; ordinary protected routes intentionally use a less specific invalid-token response.
A valid credential returns 403
Compare extra.required_scopes with the credential's scopes, then inspect the top-level error for workspace_frozen, workspace_paused, or workspace_archived. Exchanging the key cannot bypass either gate.
The socket disconnects or rejects the token
Mint a fresh socket token, pass it as socket_token rather than as an API key, use the configured HTTPS origin in production, and keep each WebSocket frame at or below 64 KB.

Endpoint reference

Ready to integrate? Open an endpoint for its required permissions, request fields, response format, and status codes. The examples above explain the workflow; these details define each individual request.

POST
/api/v1/agent_tokens

Exchange API key for access token

Description

Unauthenticated exchange: send a primary workspace or service-agent API key (agk_<base64>.<base64>) in the x-agent-api-key header or as api_key in the JSON body. Returns a 30-day access token. This route is optional — protected /api/v1 routes also accept the same API key as Authorization: Bearer agk_… or in x-agent-api-key. A Bearer token on this exchange returns 400.

Auth

None
Request Body
Field Type Required Description
scopes array<string> No Optional subset of the credential's scopes to request
api_key string No API key in format agk_<base64>.<base64> (alternative to x-agent-api-key)
Parameters
Name In Type Required Description
x-agent-api-key header string No API key in format agk_<base64>.<base64>. Underscore names are not read.
Responses
200

Token issued

Returns: TokenResponse

Response Body
Field Type Required Description
token string Yes 30-day access token
scopes array<string> Yes Granted scopes
credential_id string Yes API credential UUID
expires_in integer Yes Seconds until expiry (2,592,000)
service_agent_id string Yes Service agent UUID
workspace_id string Yes Workspace UUID
Example
{
  "token": "SFMyNTY.g2gDdAAAAARtAAAADWNyZWRlbnRpYWxfaWRtAAAACMzZODNiNDMzLTAyZjUtNGJjYy04NWNhLTdhM2QwYTgzNjI1Ym0AAAAGc2NvcGVzbQAAAAMAAAAIZnJvbV9vcHNtAAAACXRvX29iamVjdG0AAAAJdG9fYWN0aW9ubgYBy_T1yQFiAAFTgA.ABC123def456",
  "scopes": [
    "traces:write",
    "eval_runs:read",
    "eval_runs:write",
    "eval_suites:read",
    "eval_suites:write",
    "gateway:check",
    "replays:write",
    "workflow_targets:read",
    "workflow_targets:write"
  ],
  "credential_id": "3e83b433-02f5-4bcc-85ca-7a3d0a83625b",
  "expires_in": 2592000,
  "service_agent_id": "a91720d1-1c45-4343-bb45-786e20432f04",
  "workspace_id": "e65ef764-9b2c-4e24-b918-99c7be33506a"
}
400

Authorization: Bearer was sent. This endpoint accepts an API key (agk_...), not a Bearer token. Use x-agent-api-key or api_key. Also returned with detail invalid_scopes when scopes is present but not an array.

401

Missing, invalid, expired, archived, or revoked API key. Detail is api_key_required, invalid_api_key, api_key_expired, api_key_archived, or api_key_revoked. A wrong secret always gets invalid_api_key; the specific states are only reported to callers holding the key's secret.

Example
{
  "detail": "api_key_revoked",
  "status": 401,
  "title": "Unauthorized",
  "type": "https://robotscenter.net/problems/unauthorized"
}
403

Credential is recognized but refused. Detail is service_agent_inactive or insufficient_scopes. Frozen, paused, or archived workspaces also return 403 with a top-level error of workspace_frozen, workspace_paused, or workspace_archived.

Example
{
  "detail": "This workspace is frozen. Machine traffic is refused until the freeze is lifted.",
  "error": "workspace_frozen",
  "status": 403,
  "title": "Forbidden",
  "type": "https://robotscenter.net/problems/forbidden"
}
POST
/api/v1/register

Register new user with API access

Description

Creates a new user account, workspace, service agent, and API credentials in a single request. Returns everything needed for immediate API access. Registration is throttled per client IP (20 per hour by default) and returns 429 when the budget is spent.

Auth

None
Request Body
Field Type Required Description
name string No Optional display name (2–120 characters). When omitted, register_via_api derives it from the email local part.
password string Yes Password (12–72 characters)
email string Yes User email address
agent_name string Yes Name for the default service agent
workspace_name string Yes Name for the new workspace
Responses
201

Account, workspace, service agent, agk_ API key, and 30-day access token issued

Response Body
Field Type Required Description
user object Yes Created user
user.id string Yes User UUID
user.name string No Display name
user.email string Yes Normalized email
user.confirmed_at datetime (ISO 8601) No Confirmation timestamp
api_key object Yes Primary long-lived workspace API key (shown once)
api_key.type string (api_key) Yes Always api_key
api_key.key string Yes agk_ secret (prefix.secret)
api_key.scopes array<string> Yes Granted scopes
api_key.credential_id string Yes API credential UUID
api_key.service_agent_id string Yes Service agent UUID
access_token object Yes Optional 30-day bearer token for the same credential
access_token.type string (bearer) Yes Always bearer
access_token.token string Yes Access token
access_token.scopes array<string> Yes Granted scopes
access_token.credential_id string Yes API credential UUID
access_token.expires_in integer Yes Seconds until expiry (2,592,000)
access_token.service_agent_id string Yes Service agent UUID
service_agent object Yes Default service agent
service_agent.id string Yes Service agent UUID
service_agent.name string Yes Agent name
service_agent.status string Yes Agent status
service_agent.slug string Yes Agent slug
workspace object Yes Created workspace
workspace.id string Yes Workspace UUID
workspace.name string Yes Workspace name
workspace.status string Yes Workspace status
workspace.plan string Yes Plan slug
workspace.slug string Yes Workspace slug
Example
{
  "user": {
    "id": "7c2f0a11-4b8e-4d3a-9f21-0c1b2a3d4e5f",
    "name": "Ops",
    "email": "ops@example.com",
    "confirmed_at": null
  },
  "api_key": {
    "type": "api_key",
    "key": "agk_abc123.def456",
    "scopes": [
      "traces:write",
      "gateway:check"
    ],
    "credential_id": "3e83b433-02f5-4bcc-85ca-7a3d0a83625b",
    "service_agent_id": "a91720d1-1c45-4343-bb45-786e20432f04"
  },
  "access_token": {
    "type": "bearer",
    "token": "SFMyNTY.example.access-token",
    "scopes": [
      "traces:write",
      "gateway:check"
    ],
    "credential_id": "3e83b433-02f5-4bcc-85ca-7a3d0a83625b",
    "expires_in": 2592000,
    "service_agent_id": "a91720d1-1c45-4343-bb45-786e20432f04"
  },
  "service_agent": {
    "id": "a91720d1-1c45-4343-bb45-786e20432f04",
    "name": "Default agent",
    "status": "active",
    "slug": "default-agent"
  },
  "workspace": {
    "id": "e65ef764-9b2c-4e24-b918-99c7be33506a",
    "name": "Example Workspace",
    "status": "active",
    "plan": "starter",
    "slug": "example-workspace"
  }
}
422

Validation error

429

Too many registrations from this client IP (default 20 per hour, IPv6 bucketed per /64). Problem JSON with a Retry-After header and extra.retry_after_seconds; the account was not created.

POST
/api/v1/socket_tokens API key / access token

Issue short-lived socket token

sockets:connect

Description

Returns a short-lived socket token for websocket authentication. Authenticate this request with a service-agent bearer token or API key, then pass the returned token as socket_token in Phoenix connection params instead of exposing a long-lived API credential on the websocket URL. Transport notes: /socket rejects frames above 65 536 bytes (64 KB). Ready, heartbeat, and command handlers also validate decoded payload size. Unrecognized agent events receive %{reason: "unknown_event"}. Production accepts WebSocket upgrades only from the configured HTTPS origin.

Auth

API key / access token

Required Scopes

sockets:connect
Responses
200

Socket token issued

Returns: SocketTokenResponse

Response Body
Field Type Required Description
scopes array<string> Yes Granted scopes carried into the socket connection
socket_path string Yes Phoenix socket path to connect to. Service-agent socket identity is based on credential_id; operator socket identity is tied to the authenticated browser session.
expires_in integer Yes Seconds until the socket token expires
service_agent_id string Yes Service agent UUID bound to the token
workspace_id string Yes Workspace UUID bound to the token
socket_token string Yes Short-lived socket token for websocket authentication
Example
{
  "scopes": [
    "sockets:connect",
    "events:read",
    "agent_commands:read"
  ],
  "socket_path": "/socket",
  "expires_in": 600,
  "service_agent_id": "a91720d1-1c45-4343-bb45-786e20432f04",
  "workspace_id": "e65ef764-9b2c-4e24-b918-99c7be33506a",
  "socket_token": "SFMyNTY.g3QAAAABbQAAAA1jcmVkZW50aWFsX2lkbQAAACQzZTgzYjQzMy0wMmY1LTRiY2MtODVjYS03YTNkMGE4MzYyNWJtAAAAAXMAAAANc29ja2V0X3Njb3Blc20AAAABAAAAD3NvY2tldHM6Y29ubmVjdA.a1b2c3d4"
}
401

Invalid, missing, or expired credential

403

Credential is valid but lacks sockets:connect (detail: The token does not have sufficient scope). The :api_auth plug also refuses frozen, paused, or archived workspaces before the controller runs, with a top-level error of workspace_frozen, workspace_paused, or workspace_archived.

Example
{
  "detail": "This workspace is frozen. Machine traffic is refused until the freeze is lifted.",
  "error": "workspace_frozen",
  "status": 403,
  "title": "Forbidden",
  "type": "https://robotscenter.net/problems/forbidden"
}
POST
/api/v1/enrollments/claim

Claim an agent or fleet enrollment token

Description

Claims an enrollment token. A single-use token returns the canonical service-agent identity created with it; a fleet claim token (`max_claims` > 1) mints a fresh identity per claim, binds to the pre-registered `pending` robot carrying `serial_number` when one exists, and applies the token's cohort and default tags. Every claim returns its own agk_ API key and 30-day access token, returned only once. The claim token itself is never usable as an API credential.

Auth

None
Request Body
Field Type Required Description
token string Yes One-time enrollment token in age_prefix.secret format
serial_number string No Serial of the claiming unit. Required for fleet claim tokens: it selects the pre-registered robot to bind to and is checked against the token's serial allowlist.
agent object No Optional runtime metadata advertised during claim
Responses
201

Enrollment claimed and machine credentials issued

400

Serial not on the token's allowlist, serial already claimed, or no serial supplied for a fleet claim token

401

Invalid enrollment token

403

The workspace is not accepting machine traffic, with a machine-readable `error` of `workspace_frozen`, `workspace_paused` or `workspace_archived`. Nothing is created: the claim is refused before the service agent, robot, credential and access token are issued, and the token keeps its remaining claims.

Example
{
  "detail": "This workspace is frozen. Machine traffic is refused until the freeze is lifted.",
  "error": "workspace_frozen",
  "status": 403,
  "title": "Forbidden",
  "type": "https://robotscenter.net/problems/forbidden"
}
409

Enrollment was already claimed, exhausted or revoked

410

Gone. type problems/enrollment-expired when the token is past expires_at. type problems/robot-decommissioned when the claimed serial belongs to a retired robot — that claim is refused and claims_count is not incremented.

429

Plan robot or agent quota reached

Where to go next

see also