External agent tokens
Issue scoped, read-only credentials for third-party AI agents that aren't full workspace members.
What an external agent token is
Most AI clients that talk to orboto do so as a workspace member - a human's own account, or a bot identity created for automation. Either way, the connection acts with a real project membership and a real permission set, exactly like a person using the app (see Connecting an AI client to your workspace).
An external agent token is a different, narrower kind of credential, for a case that doesn't fit that model: a third-party AI agent harness - a background coding agent built into an external tool, for example - that you want to give limited read access to specific orboto data, without making it a member of any project. It has no project membership and no role. What it can read is defined entirely by an explicit allowlist of resource patterns an administrator grants when the token is created.
How it differs from a bot API key
Bot API key (orb_*) | External agent token (xag_*) | |
|---|---|---|
| Identity | A real user account with project membership | No user account, no project membership |
| Access | Whatever its role's permissions allow, across every project it's a member of | Only the specific orboto:// resource patterns listed on the token |
| Can write? | Yes, if its role permits it | No - read-only by design |
| Issued from | The bot user's own Profile → API keys | The admin API, by a super-admin, naming an agent and its allowed scopes directly |
| Typical use | An automation that should act like a team member - create tickets, comment, log time | A third-party client you want to hand a narrow, revocable read window into your workspace, without onboarding it as a user |
If the integration should behave like a teammate - creating tickets, commenting, moving work through statuses - use a bot identity and a regular API key instead, the same as the n8n integration or the MCP server does. Reach for an external agent token only when you specifically want to hand out scoped, read-only visibility to a client you don't want as a full member.
The token format
An external agent token is a random secret prefixed xag_ (distinct from
the orb_ prefix on ordinary API keys, so orboto's authentication layer can
tell the two apart at a glance). Only its SHA-256 hash is stored - the
plaintext secret is shown to the administrator who created it exactly once,
at creation time, and never again.
Managing tokens
Token management is an administrator action, gated behind the
admin:system:write permission (list-only access needs
admin:system:read). There is currently no admin UI page for this - manage
tokens through the REST API directly, for example with curl.
Grant a token
curl -X POST https://your-orboto-host.example.com/api/admin/external-agents \
-H "Authorization: Bearer orb_<an admin's API key>" \
-H "Content-Type: application/json" \
-d '{
"agentName": "ACME support bot",
"allowedScopes": ["orboto://ticket/ACME-*", "orboto://project/ACME"]
}'{ "id": "5b1a...", "token": "xag_3f9c2a1e..." }Copy the token value somewhere safe immediately - this response is the
only time orboto ever returns the plaintext secret. Hand it to whatever
external client will use it (however that client expects a bearer
credential configured).
List tokens
curl https://your-orboto-host.example.com/api/admin/external-agents \
-H "Authorization: Bearer orb_<an admin's API key>"Each entry shows the agent's name, its scope list, when it was created and
last used, and whether it's been revoked - but only the first 12 characters
of the token (apiKeyPrefix), enough to recognize which secret a client is
presenting without ever exposing the full value again.
Revoke a token
curl -X DELETE https://your-orboto-host.example.com/api/admin/external-agents/5b1a... \
-H "Authorization: Bearer orb_<an admin's API key>"Revocation is immediate and permanent. There's no un-revoke - grant a fresh token if the integration needs to reconnect later.
The scope model: what a token can and cannot reach
allowedScopes is a list of orboto:// resource patterns - the same URI
scheme used by MCP resources elsewhere in orboto.
Each pattern is either an exact resource address or ends in a single
trailing * wildcard:
orboto://project/ACME- exactly that one project.orboto://ticket/ACME-42- exactly that one ticket.orboto://ticket/ACME-*- every ticket in theACMEproject, current and future, without listing them one by one.
Anything not covered by at least one pattern on the token is invisible to
it - there's no implicit access to "the rest of the workspace" the way a
project member's role would grant. A token scoped to orboto://ticket/ACME-*
can't see WEB project tickets, docs, milestones, or anything else outside
what its patterns literally cover, no matter how narrow or broad you make
the list.
Current status
Provisioning and revoking external agent tokens through the admin API above
is available today. Connecting a specific external client to consume a
token - the piece where the client actually presents an xag_* token
against a live orboto endpoint and gets back the scoped data - is still
being finalized on orboto's side; there's no client-facing connection guide
to publish yet. If you're evaluating this for an integration, provision a
token now to reserve the scope you want, and check back for the connection
steps once that path ships.
Troubleshooting
| Symptom | Likely cause |
|---|---|
POST /admin/external-agents returns 403 | The calling account doesn't hold admin:system:write - only a super-admin (or an admin role with that specific permission) can create or revoke tokens. |
allowedScopes rejected with a validation error | Every entry must start with orboto://. A bare resource name without the scheme prefix (e.g. ticket/ACME-42) is rejected, not silently corrected. |
| Lost the plaintext token after creation | It can't be recovered - the stored value is a one-way hash. Revoke the old token and grant a new one. |
| A token still shows as active after you meant to remove it | Confirm you deleted the right row - GET /admin/external-agents lists every token's id; match on agentName or apiKeyPrefix before calling DELETE. |