Agent Users

Per-endpoint reference for the Agent Users resource. An agent user is a special users row with user_type = 'agent' plus a matching org_users membership, scoped to the API key's org. Agents act on behalf of an organization: they receive long-lived API keys (credentials) that can be passed to a third-party AI / automation worker, and they can be rotated or revoked independently of any human user. Credential, token, and invitation endpoints are admin-gated (org_users.role = 1). Webhook GET/PUT is self-serve: the caller may manage their own org membership and direct reports. Each section below shows the exact path, every parameter, a full request example, and the response shape with a worked example.

Quickstart

Base URL

https://api.tasklife.com/v1/

Auth (all endpoints)

Every request must include a bearer token in the Authorization header. The token is an API key bound to a specific organization — endpoints return only data scoped to that org. Create-agent, credentials, token, and invitation endpoints require that the API key's user has org_users.role = 1 in the API key's org (non-admin keys receive 403 — Admin access required from AgentUsersService::assertOrgAdmin()). Webhook GET/PUT is not admin-only: a non-admin key may read and write webhook settings for its own org_user_id and for direct reports (reports_to_org_user_id equals the caller, one level only). Peers get 403 — Not allowed to manage webhook settings. Org admins keep full access.

Authorization: Bearer ***
Content-Type: application/json

Response envelope

All successful responses share a common envelope. Error responses are described in Errors below.

{
  "success": true,
  "data":   { /* endpoint-specific payload */ }
}

Note: the create-credential and rotate-credential endpoints both return two top-level data keys: data.credential.<field> (the row metadata — key id, label, scope, expiry, rotated flag, created timestamp) and data.payload.<field> (a self-contained bootstrap document intended to be handed off to the agent worker). The bootstrap payload carries the raw plaintext api_key under data.payload.auth.api_key — that is the only time the plaintext key is returned by the API. After the response is delivered, the only way to recover the plaintext is the reveal endpoint, which is admin-gated and audit-logged. The create-agent endpoint nests the new agent under data.agent.<field>, and the reveal endpoint returns just data.credential.{ key_id, api_key }.

Errors

Failed responses always have success: false and an error object with a stable code and a human-readable message:

{
  "success": false,
  "error": {
    "code":    "API_ERROR",
    "message": "Admin access required"
  }
}
HTTP statusWhenWhat to do
400Validation failed: missing name on create-agent, expires_at not parseable / not in the future on create-credential, role outside {1,2,3}, agent user is not actually an agent on a credential call, or an older key (pre-ciphertext storage) is being revealed.Read error.message; fix the offending field.
401Missing or invalid Authorization header.Check the API key; confirm the header is present.
403On credential / token / invitation endpoints: API key is valid but the user is not an org admin (org_users.role = 1). On webhook GET/PUT: the caller is not an org admin, is not the target org_user_id, and is not the target's direct manager (reports_to_org_user_id). Cross-org targets are also 403.For credentials/token/invitation, use an org-admin key. For webhooks, use the agent's own key, their manager's key, or an org-admin key.
404{id} does not resolve to an active agent user in the API key's org, or {key_id} does not resolve to a credential row scoped to that agent and org.Verify the id values and that the agent / key haven't been deleted.
500Unexpected server error (e.g. api_key_crypto decrypt failed on reveal, missing API_KEY_ENCRYPTION_KEY env var, DB write failed).Retry with backoff; report if persistent.
501Reveal attempted against a key that was created before ciphertext was stored — the older api_keys row has no key_ciphertext / key_iv / key_tag columns populated.Rotate the credential to mint a fresh, revealable key.

Agent Users

Agent Users let an organization mint long-lived API keys that are bound to a non-human users row (user_type = 'agent'). The flow is three steps: create the agent user (POST /v1/agents) → mint its first credential (POST /v1/agents/{id}/credentials) → hand the returned data.payload.auth.api_key plus a link to the human-facing docs (https://tasklife.com/main/pages/77) to the worker process. Use POST /v1/agents/{id}/credentials/rotate to issue a new key and immediately deactivate the old one, and GET /v1/agents/{id}/credentials/{key_id}/reveal as a last-resort admin tool to recover the plaintext of a specific credential when the worker has lost it. All four endpoints are admin-gated.

Endpoints at a glance

ActionMethod + PathSummary
Create agent userPOST /agentsCreate a new user_type = 'agent' row + matching org_users membership. Required: name (alias: display_name). Returns 201 with the created agent.
Create credentialPOST /agents/{id}/credentialsMint a new bearer API key for the agent. Optional body: expires_at (ISO datetime), scope, rate_limit, rate_limit_burst, label. Returns 201 with the credential metadata plus a bootstrap payload that contains the plaintext api_key.
Rotate credentialPOST or PUT /agents/{id}/credentials/rotateDeactivate every active credential for the agent and issue a new one in the same transaction. Returns 200 with the new credential metadata + bootstrap payload (data.credential.rotated: true).
Reveal credentialGET /agents/{id}/credentials/{key_id}/revealDecrypt and return the plaintext api_key for a previously-issued credential. Admin-only, audit-logged. Sensitive endpoint — treat as privileged.
Get webhook settingsGET /agents/{org_user_id}/webhookRead webhook config for an agent membership. Allowed for org admins, the target themselves, or their direct manager. Path id is org_user_id.
Set webhook settingsPUT / PATCH / POST /agents/{org_user_id}/webhookUpdate webhook URL, events, throttle, and secret. Same self / direct-report / admin gate as GET. Peers receive 403.

POST /agents

Create a new agent user. Inserts a row into users (with user_type = 'agent' on installs that have the column) and a matching org_users membership in the API key's org. Returns 201 with the created agent nested under data.agent.

Body parameters

FieldTypeRequiredDefaultDescription
name string yes — Display name for the agent. Alias: display_name. display_name wins if both are sent. Trimmed; empty after trim returns 400.
display_name string no — Alias for name. Takes precedence when both are present.
description string no — Free-form description. Not persisted by the service — accepted on the wire for forward compatibility, but no field on the created data.agent object reflects it.
role integer no 2 Org-membership role (org_users.role). Must be one of {1, 2, 3}; out-of-range values silently fall back to 2. 1 = admin, 2 = member, 3 = read-only / custom.

Request example

curl -sS -X POST https://api.tasklife.com/v1/agents \
  -H "Authorization: Bearer ***" \
  -H "Content-Type: application/json" \
  -d '{
    "name":        "Stracker Build Agent",
    "description": "CI worker that picks up tasks in the Build column.",
    "role":        2
  }'

Response 201 Created

The created agent is nested under data.agent:

FieldTypeDescription
data.agent.idintegerNew users.id. Use this as the {id} path parameter for the credential / reveal endpoints.
data.agent.display_namestringThe trimmed name you passed in.
data.agent.roleintegerStored org_users.role for this membership.
data.agent.org_idintegerOrg the membership was created in (always equal to the API key's org_id).
data.agent.user_typestring"agent" on installs that have the users.user_type column; "unknown" otherwise.
data.agent.created_atstringRFC 3339 / ISO 8601 timestamp (date('c')) generated at insert time.
{
  "success": true,
  "data": {
    "agent": {
      "id":           9187,
      "display_name": "Stracker Build Agent",
      "role":         2,
      "org_id":       42,
      "user_type":    "agent",
      "created_at":   "2026-07-03T14:21:48+00:00"
    }
  }
}

Errors

{
  "success": false,
  "error": {
    "code":    "API_ERROR",
    "message": "Admin access required"
  }
}

POST /agents/{id}/credentials

Mint a new bearer API key for the agent and return it once in a self-contained bootstrap payload. The response carries two top-level keys: data.credential (the row metadata you can persist — key id, label, scope, expiry, rotated flag, created timestamp) and data.payload (a handoff document for the worker that contains the raw api_key). Save the payload immediately — the plaintext is not recoverable without reveal, which is admin-gated and audit-logged.

Path parameters

FieldTypeRequiredDescription
idintegeryesAgent users.id returned by POST /agents. Must be an active membership in the API key's org with user_type = 'agent'.

Body parameters

FieldTypeRequiredDefaultDescription
expires_at string no null (no expiry) ISO-8601 datetime at which the credential should expire. Must parse via strtotime() and resolve to a future timestamp — past or unparseable values return 400 — expires_at must be a valid future datetime. Stored as MySQL datetime (Y-m-d H:i:s, UTC) and echoed back as that string.
scope string no "agent" Single scope token written to api_keys.scope and echoed back as data.credential.scope / data.payload.auth.scopes[0]. Trimmed; empty string falls back to "agent".
label string no "Agent key" Free-form label stored on api_keys.label. Trimmed; empty falls back to the default.
rate_limit integer no 120 Per-minute request budget. Clamped to a minimum of 1. Not surfaced in the response — stored on the api_keys row only.
rate_limit_burst integer no null (or max(rate_limit, value) when supplied) Burst budget. When supplied, clamped to max(rate_limit, value); otherwise null. Also stored only — not in the response.

Request example

curl -sS -X POST https://api.tasklife.com/v1/agents/9187/credentials \
  -H "Authorization: Bearer ***" \
  -H "Content-Type: application/json" \
  -d '{
    "expires_at":      "2027-01-01T00:00:00Z",
    "scope":           "agent",
    "label":           "Stracker Build Agent — key 1",
    "rate_limit":      240,
    "rate_limit_burst": 480
  }'

Response 201 Created

Two top-level keys under data — data.credential (metadata) and data.payload (handoff document with the plaintext api_key):

FieldTypeDescription
data.credential.key_idintegerNew api_keys.id. Use as {key_id} for the reveal endpoint.
data.credential.labelstringThe (possibly defaulted) label stored on api_keys.label.
data.credential.scopestringThe resolved scope token ("agent" by default).
data.credential.expires_atstring | nullMySQL Y-m-d H:i:s string in UTC, or null when no expiry was supplied.
data.credential.created_atstringRFC 3339 / ISO 8601 timestamp generated at insert time.
data.credential.rotatedbooleanAlways false from this endpoint; true on the rotate endpoint.
data.payload.versionstringBootstrap document schema version. Currently "v1".
data.payload.issuerstringAlways "strackerapp".
data.payload.environmentstringAPP_ENV value at mint time — "prod" on production, "dev" elsewhere. Determines the key prefix (tl_prod_ vs tl_dev_).
data.payload.org.idintegerAPI key's org id.
data.payload.principal.typestringAlways "agent" for this endpoint.
data.payload.principal.agent_user_idintegerEcho of the path {id}.
data.payload.principal.display_namestringAgent's users.name at mint time.
data.payload.auth.schemestringAlways "bearer".
data.payload.auth.api_keystringPlaintext API key — the only time the raw key is returned. Format: tl_prod_<48 hex chars> on prod, tl_dev_<48 hex chars> elsewhere. Hand this off to the worker immediately.
data.payload.auth.key_idintegerSame as data.credential.key_id.
data.payload.auth.scopes[]string[]Single-element array mirroring data.credential.scope.
data.payload.auth.expires_atstring | nullSame value as data.credential.expires_at.
{
  "success": true,
  "data": {
    "credential": {
      "key_id":     4218,
      "label":      "Stracker Build Agent — key 1",
      "scope":      "agent",
      "expires_at": "2027-01-01 00:00:00",
      "created_at": "2026-07-03T14:22:11+00:00",
      "rotated":    false
    },
    "payload": {
      "version":     "v1",
      "issuer":      "strackerapp",
      "environment": "prod",
      "org": {
        "id": 42
      },
      "principal": {
        "type":          "agent",
        "agent_user_id": 9187,
        "display_name":  "Stracker Build Agent"
      },
      "auth": {
        "scheme":     "bearer",
        "api_key":    "tl_prod_8f3a1c9b2e7d6045f1a8b3c6e9d2f4a7b8c1d5e0f2a4b6c8d0e1f3a5b7c9d763",
        "key_id":     4218,
        "scopes":     ["agent"],
        "expires_at": "2027-01-01 00:00:00"
      }
    }
  }
}

Errors

{
  "success": false,
  "error": {
    "code":    "API_ERROR",
    "message": "expires_at must be a valid future datetime"
  }
}

POST /agents/{id}/credentials/rotate (PUT is also accepted)

Issue a new credential for the agent and, in the same DB transaction, set is_active = 0 on every previously-active credential for that agent in the API key's org. The plaintext of the new key is returned once under data.payload.auth.api_key — same envelope as the create-credential endpoint, but with data.credential.rotated: true. Accepts both POST and PUT at the router.

Path parameters

FieldTypeRequiredDescription
idintegeryesAgent users.id.

Body parameters

FieldTypeRequiredDefaultDescription
expires_at string no null (no expiry) Same rules as the create-credential endpoint. Future-only ISO-8601 datetime, or omit for an open-ended key.
scope string no "agent" Scope for the new credential.
label string no "Agent key (rotated)" Label for the new credential. Default flips from "Agent key" to "Agent key (rotated)" on this endpoint.
rate_limit integer no 120 Per-minute request budget for the new credential. Stored only.
rate_limit_burst integer no null (or max(rate_limit, value) when supplied) Burst budget for the new credential. Stored only.

Request example

curl -sS -X POST https://api.tasklife.com/v1/agents/9187/credentials/rotate \
  -H "Authorization: Bearer ***" \
  -H "Content-Type: application/json" \
  -d '{
    "label":      "Stracker Build Agent — rotated after incident",
    "expires_at": "2027-01-01T00:00:00Z"
  }'

Response 200 OK

Same envelope as the create-credential endpoint. data.credential.rotated is true, and the prior api_keys row for this agent has been deactivated:

FieldTypeDescription
data.credential.<field>mixedSame shape as create-credential, with rotated: true.
data.payload.<field>mixedSame shape as create-credential, including the plaintext data.payload.auth.api_key for the newly minted key.
{
  "success": true,
  "data": {
    "credential": {
      "key_id":     4219,
      "label":      "Stracker Build Agent — rotated after incident",
      "scope":      "agent",
      "expires_at": "2027-01-01 00:00:00",
      "created_at": "2026-07-03T14:48:02+00:00",
      "rotated":    true
    },
    "payload": {
      "version":     "v1",
      "issuer":      "strackerapp",
      "environment": "prod",
      "org": {
        "id": 42
      },
      "principal": {
        "type":          "agent",
        "agent_user_id": 9187,
        "display_name":  "Stracker Build Agent"
      },
      "auth": {
        "scheme":     "bearer",
        "api_key":    "tl_prod_4b7e2d1a9f6c8053e2b4d7a0c1f3e5b8a9d2c4f6e0b1a3c5d7f9e1b3a5c7d318",
        "key_id":     4219,
        "scopes":     ["agent"],
        "expires_at": "2027-01-01 00:00:00"
      }
    }
  }
}

Errors

{
  "success": false,
  "error": {
    "code":    "API_ERROR",
    "message": "Agent user not found for org"
  }
}

GET /agents/{id}/credentials/{key_id}/reveal (sensitive — admin-only, audit-logged)

Decrypt the stored AES-GCM ciphertext for a previously-issued credential and return the plaintext api_key. This is the last-resort path for recovering a key when the worker has lost it — it is the only endpoint besides the create / rotate endpoints that exposes the raw api_key. Treat as privileged: the call is audit-logged to api_key_reveal_audit (requesting user id, source IP, user-agent, success / failure reason) when that table exists on the install.

Path parameters

FieldTypeRequiredDescription
idintegeryesAgent users.id.
key_idintegeryesapi_keys.id to reveal. Must belong to this agent in the API key's org (WHERE id = ? AND org_id = ? AND user_id = ?).

Request example

curl -sS https://api.tasklife.com/v1/agents/9187/credentials/4218/reveal \
  -H "Authorization: Bearer ***"

Response 200 OK

Returns just the credential id and the decrypted plaintext key. No metadata (label, scope, expiry) is included:

FieldTypeDescription
data.credential.key_idintegerEcho of the {key_id} path parameter.
data.credential.api_keystringPlaintext API key for the credential — same value originally returned at mint time.
{
  "success": true,
  "data": {
    "credential": {
      "key_id":  4218,
      "api_key": "tl_prod_8f3a1c9b2e7d6045f1a8b3c6e9d2f4a7b8c1d5e0f2a4b6c8d0e1f3a5b7c9d763"
    }
  }
}

Errors

{
  "success": false,
  "error": {
    "code":    "API_ERROR",
    "message": "Credential is not revealable"
  }
}