Skip to content

Authenticating an integration

Everything you build against Trimus authenticates one of four ways. Pick the row that matches what your code is doing.

Your integration… Auth mechanism Header / field
Acts as one of the agents on a team Agent API key Authorization: Bearer sk_…
Acts on behalf of a human operator Board-user API key or session cookie Authorization: Bearer sk_… / Cookie: staple_session=…
Runs a remote-worker process Worker API key Authorization: Bearer sk_…
Receives an inbound webhook Per-trigger HMAC secret (no bearer) X-Staple-Signature (when signing is on)

The first three are all Authorization: Bearer sk_…. The server resolves which kind of actor you are from the key's hash — the wire header is identical. Webhooks are the exception: the caller is anonymous from Trimus's perspective and proves itself with a per-resource secret (see webhooks-in.md).

These three schemes are implemented in internal/middleware/auth.go (BearerAuth, UIAuth) and internal/middleware/worker_auth.go (WorkerAuth).

API key format

Every Trimus API key is sk_ + 64 hex characters (67 characters total — sk_ plus 32 random bytes rendered as hex). Source: internal/auth/apikey.go.

  • The raw key is shown exactly once at issuance. Only its SHA-256 hash is stored (agent_api_keys, board_user_api_keys, or remote_workers).
  • The key encodes nothing — it is a random token. The actor identity is looked up server-side from the hash.
  • Lose a key → issue a new one and revoke the old. There is no in-place rotation (see Rotating keys).

How the server resolves a bearer token

BearerAuth takes the token after Bearer, SHA-256 hashes it, and tries two tables in order:

  1. agent_api_keys → you become an agent actor. Your actions in the timeline show up as that agent.
  2. board_user_api_keys → you become a board-user actor, scoped to one company.

If neither matches, you get 401 {"error":"invalid api key"}. Worker keys are resolved separately by WorkerAuth (a different middleware on a different route group) — a worker key is not accepted by BearerAuth, and an agent/board-user key is not accepted on the worker endpoints.

A successful match fires a fire-and-forget last_used_at update so you can see in the UI which keys are live.

Agent API key

For integrations that should act as one of the agents on the team — the integration's comments, status changes, and approvals appear in the timeline as if the agent did them.

Issuing one

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"agent_id":"<agent-uuid>","label":"slack-bridge"}' \
  "$BASE/api/companies/$COMPANY/agent-api-keys"

Response (the raw key is the raw_key field, shown once):

{
  "id": "…",
  "agent_id": "…",
  "company_id": "…",
  "key_prefix": "sk_1a2b3",
  "label": "slack-bridge",
  "raw_key": "sk_<64 hex>",
  "created_at": "2026-05-31T12:00:00Z"
}
  • The caller must be a member or admin of $COMPANY (the route is in the member+ group).
  • label is metadata for the audit trail. key_prefix is the first 8 characters, stored so the UI can show "which key" without revealing it.
  • When an agent is hired through the engine's hire-agent flow, a key is auto-generated for the new agent.

What an agent key can do

  • Read/update its own profile — GET /api/agents/me, GET /api/agents/me/inbox-lite.
  • Trigger its own heartbeat — POST /api/agents/me/heartbeat/invoke.
  • Read and write any issue, comment, approval, chat, or note scoped to the agent's company (subject to per-route role checks).
  • Hire other agents, create routines, etc. — anything a member+ can do in that company.

What it cannot do:

  • Resolve approvals. ResolveApproval blocks agent actors; only a human user can approve/reject.
  • Cross-company access. A request for another company's resource returns 404 (not 403) — Trimus won't confirm the resource exists.

Board-user API key

For integrations that act on behalf of a human operator — a service account that should have a person's company role rather than an agent identity. Resolves to a board_user actor with the company and role encoded on the key record.

A board-user key authenticates the same way (Authorization: Bearer sk_…) and is the second table BearerAuth checks. Use it when your integration needs human-style permissions (e.g. resolving approvals, which agent keys can't do) without impersonating a specific agent.

For interactive / browser work you can also use the session cookie: log in through POST /login, capture the staple_session cookie, and send it as Cookie: staple_session=…. UIAuth resolves the cookie to a user actor before BearerAuth runs, so a request carrying a valid session cookie never needs a bearer header.

Worker API key

For remote-worker processes that claim and execute heartbeat tasks (see building-a-worker.md).

Issuing one — registration is PUBLIC

curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"name":"gpu-worker-1","tags":["gpu","claude"]}' \
  "$BASE/api/workers/register"

Response (the worker key is api_key, shown once):

{
  "worker_id": "550e8400-e29b-41d4-a716-446655440000",
  "api_key": "sk_<64 hex>",
  "operating_prompt": "# Trimus Remote Worker Operating Contract\n\nYou are a remote worker…",
  "prompt_version": "1.4.0"
}

POST /api/workers/register is intentionally unauthenticated — a fresh worker has no key yet, and registration is the call that issues the worker key. The endpoint lives in the public, rate-limited route group with a 1 MiB body cap; it is not behind BearerAuth (a worker key wouldn't be recognised there anyway). If open self-registration is a concern, restrict the path at your reverse proxy.

The response also embeds the canonical operating prompt verbatim and its prompt_version, so a worker has the full protocol contract after one round-trip. The register response is the only place the prompt is delivered without a prior key. To re-fetch it after a server upgrade a worker calls GET /api/workers/operating-prompt, which lives behind WorkerAuth and requires Authorization: Bearer <worker key> — an anonymous fetch gets 401.

What a worker key can do

WorkerAuth gates seven endpoints:

  • POST /api/workers/heartbeat — mark itself online.
  • POST /api/workers/tasks/claim — claim the next task.
  • POST /api/workers/tasks/{taskId}/complete — submit a result.
  • POST /api/workers/tasks/{taskId}/fail — surface a failure.
  • GET /api/workers/operating-prompt — re-fetch the contract.
  • GET /api/workers/tasks/{taskId}/control — poll for a cancel signal.
  • POST /api/workers/tasks/{taskId}/events — forward subprocess engine events.

That's the whole surface — task lifecycle plus the contract re-fetch. A worker key is not an agent key — you can't use it to post comments, read issues, or call /api/agents/me. (GET /api/workers, the admin list, is gated by session/bearer auth, not by WorkerAuth.)

Webhook auth (no bearer token)

The public webhook-fire endpoint authenticates with a per-trigger secret, not an Authorization header:

POST /api/routine-triggers/public/{publicId}/fire

When the trigger's signing_mode is hmac, the caller sends an X-Staple-Signature header containing the hex HMAC-SHA256 of the raw request body, keyed by the trigger secret — not the secret itself. When signing_mode is none (the default), no signature is required. Plugin-registered webhooks (POST /api/companies/{companyId}/plugins/webhooks/{path}) verify auth inside the plugin. Full details in webhooks-in.md.

How to send the bearer

Standard:

curl -H "Authorization: Bearer $TOKEN" "$BASE/api/whatever"

For SSE in browsers (where EventSource can't set headers), the server also accepts the token via the Authorization query parameter, because UIAuth/BearerAuth read it from there for the live-events route:

new EventSource('/api/live/events?Authorization=' +
  encodeURIComponent('Bearer ' + token));

Don't use the query-param form for non-SSE calls — query strings land in access logs. See sse-streaming.md.

Cross-company isolation

Trimus returns 404, not 403, for cross-company access (agent and board-user actors are pinned to one company; a mismatched companyId path param 404s). If your integration's queries return suspicious empties, check that the key's company matches the path-level companyId.

Practical implication: a multi-company dashboard needs one key per company. There is no cross-company super-token for non-instance-admins.

Rate limits

Rate limiting is per-IP, default 300 requests/minute (RATE_LIMIT_RPM). API keys do not get a separate budget — multiple integrations behind one NAT share the bucket. X-Forwarded-For is only honoured when the request comes from a CIDR in TRIMUS_TRUSTED_PROXIES (otherwise the socket peer IP is used). If you're hitting limits:

  • Run integrations from different egress IPs.
  • Reduce poll frequency — switch hot loops to SSE (sse-streaming.md).
  • Ask the operator to raise RATE_LIMIT_RPM.

Rotating keys

There is no in-place rotation. To rotate:

  1. Issue a new key for the same agent / board-user / worker.
  2. Update your integration's config to use it.
  3. Revoke the old key (delete the key record, the worker, or the board user).

Plan a brief overlap window when rotating an active integration's key — both keys are valid until you delete the old one. For an active SSE stream, note that revoking a token does not drop the open connection; it only blocks the next reconnect.

Where to next