Agents
Create seat-free agent users, configure their guardrails, mint scoped access tokens, and read the organization-wide agent overview.
An agent is a member of your organization with no login of any kind. It authenticates exclusively with access tokens, costs no seat, and receives no e-mail. Everything a person can be attributed with — a ticket, a comment, a time entry, a status change — an agent can be attributed with too.
These endpoints are not reachable with a personal access token. They require a session (JWT), so an agent can never mint tokens for itself. They are also blocked while impersonating another user.
Who may call them
| Requirement | |
|---|---|
| List, get, update, delete, list/revoke tokens | The agents:manage permission, or being the agent's owner |
| Create an agent, mint a token | agents:manage and the AGENT_USERS plan feature (Pro and up) |
| Overview, update policies | agents:manage only — these carry organization-wide numbers and org-wide rules |
Creating and minting are the only plan-gated operations. Reading, deactivating, deleting and revoking stay open on every plan, so an organization that moves down can always see and cut off the machine users it already owns.
List Agents
GET /api/v1/organizations/{orgId}/agentsWith agents:manage, every agent in the organization. Without it, only the agents the caller owns.
Example Response
[
{
"id": "usr_agent123",
"name": "Nightly Refactor",
"description": "Runs the housekeeping loop on ci-1",
"email": "[email protected]",
"isActive": true,
"owner": { "id": "usr_abc123", "name": "Alex Smith", "email": "[email protected]" },
"timeFactor": null,
"weeklyHoursCap": 20,
"maxConcurrentClaims": 3,
"gitIdentity": "nightly-refactor[bot]",
"tokenCount": 1,
"lastSeenAt": "2026-09-04T22:11:00Z",
"permissionGroups": [{ "id": "grp_agents", "name": "Agents", "color": "#8b8b8b" }],
"createdAt": "2026-08-01T09:00:00Z"
}
]email is synthetic and undeliverable — agents never receive mail. lastSeenAt is the most recent use of any of the agent's tokens.
Create Agent
POST /api/v1/organizations/{orgId}/agentsCreates a seat-free agent user with the TEAM_MEMBER role. Returns 201 with the same shape as above.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Display name, shown wherever a member appears (max 100 characters) |
| description | string | null | No | What this agent does / which loop drives it (max 2,000 characters) |
| ownerId | string | No | Human member owning the agent (defaults to the caller) |
| permissionGroupId | string | No | INTERNAL permission group to place the agent in (defaults to the system Agents group) |
An organization may hold at most 25 live agents. Beyond that the call fails with AGENT_LIMIT_REACHED — "seat-free" must not mean "unbounded".
Errors
| Status | Code / reason |
|---|---|
| 403 | Missing agents:manage, or the plan does not include AGENT_USERS |
| 403 | AGENT_LIMIT_REACHED — the organization already holds 25 agents |
Get Agent
GET /api/v1/organizations/{orgId}/agents/{id}Returns one agent. 404 when the id is not an agent of this organization.
Update Agent
PATCH /api/v1/organizations/{orgId}/agents/{id}All fields optional; send only what changes. Send null to clear a nullable field.
| Field | Type | Description |
|---|---|---|
| name | string | Display name |
| description | string | null | Free text |
| ownerId | string | New human owner (must be an active, non-agent member). Changing the owner needs agents:manage |
| timeFactor | number | null | Per-agent MCP time factor, 1.0–10.0. null falls back to the organization factor |
| weeklyHoursCap | number | null | Weekly cap on MCP-tracked hours (approved + pending), 1–168. null = no cap |
| maxConcurrentClaims | number | null | How many tickets this agent may hold a claim on at once, 1–50. null = organization default (3) |
| gitIdentity | string | null | GitHub/GitLab login or commit e-mail its pull requests are recognised by |
| isActive | boolean | false deactivates: the tokens stay, but stop working |
Delete Agent
DELETE /api/v1/organizations/{orgId}/agents/{id}Soft-deletes the agent and revokes all of its tokens. Returns 204 No Content.
Agent Policies
Organization-wide rules that apply to every agent. Humans are unaffected by them.
GET /api/v1/organizations/{orgId}/agents/policies
PATCH /api/v1/organizations/{orgId}/agents/policiesReading needs agents:manage or agent ownership; writing needs agents:manage — a policy is the ceiling for everyone's agents, not for your own.
Body / Response
| Field | Type | Description |
|---|---|---|
| statusTransitionsRequireApproval | string[] | Status keys an agent may not move a ticket into on its own (default ["DONE"]). BACKLOG is not accepted |
| prsRequireHumanReview | boolean | Flag pull requests authored by an agent's git identity as needing a human review. Informational — Spedy cannot block a merge |
| gateableStatusKeys | string[] | Read-only: which status keys can be gated at all |
When an agent tries to move a ticket into a gated status, the move does not happen: Spedy records a pending suggestion for a human instead and answers 403 with AGENT_APPROVAL_REQUIRED. This is enforced identically on PATCH /boards/{boardId}/tickets/{ticketId}/status and on the MCP tools — there is no second door. If the board has Explainable Status turned off no suggestion can be recorded, and the move is refused outright rather than silently dropped.
The per-agent time factor, the weekly hours cap and the PENDING approval state of agent-tracked time apply to agent users over REST as well, not only over MCP.
List Access Tokens
GET /api/v1/organizations/{orgId}/agents/{id}/tokensReturns the agent's tokens without their secrets.
{
"tokens": [
{
"id": "tok_abc123",
"name": "ci-1 loop",
"tokenPrefix": "pat_1a2b3c4d",
"expiresAt": "2027-01-01T00:00:00Z",
"lastUsedAt": "2026-09-04T22:11:00Z",
"createdAt": "2026-08-01T09:05:00Z",
"allowedBoardIds": ["brd_web"],
"readOnly": false,
"denyDelete": true,
"clientLabel": "nightly-refactor on ci-1"
}
]
}Mint an Access Token
POST /api/v1/organizations/{orgId}/agents/{id}/tokensReturns 201 and the plaintext token exactly once — it cannot be retrieved later.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Human-readable label (max 200 characters) |
| expiresInDays | number | null | No | Lifetime in days (1–3650). Omit or null for a token that never expires |
| allowedBoardIds | string[] | No | Restrict to these projects regardless of the agent's memberships (max 100). Empty = every project the agent is a member of |
| readOnly | boolean | No | Read tools and GET requests only (default false) |
| denyDelete | boolean | No | Block every delete capability and DELETE request. Defaults to true for agent tokens |
| clientLabel | string | null | No | Free-text label of the client that will use the token (max 200 characters) |
Minting is rate-limited to 20 calls per hour. The caller must hold every permission the agent's groups grant — you cannot mint a credential more capable than yourself.
Errors
| Status | Reason |
|---|---|
| 403 | Plan does not include AGENT_USERS, or the agent holds permissions the caller does not |
See Authentication for how the scopes behave at request time.
Revoke an Access Token
DELETE /api/v1/organizations/{orgId}/agents/{id}/tokens/{tokenId}Cuts the loop off immediately. Returns 204 No Content. Never plan-gated — a downgraded organization must always be able to stop a running loop.
Agents Overview
GET /api/v1/organizations/{orgId}/agents/overviewA read-only aggregate over data other modules already own: are the loops working, and is a human still in the loop. Requires agents:manage, because the response carries organization-wide figures.
Example Response
{
"generatedAt": "2026-09-05T08:00:00Z",
"monthStart": "2026-09-01",
"totals": {
"agentCount": 3,
"activeAgentCount": 2,
"ticketsClosedThisMonth": 48,
"ticketsClosedByAgentsThisMonth": 11,
"agentClosedShare": 0.229,
"minutesBookedThisMonth": 12600,
"agentMinutesThisMonth": 2100,
"humanMinutesThisMonth": 10500,
"agentMinutesShare": 0.167,
"pendingAgentMinutes": 480
},
"agents": [
{
"id": "usr_agent123",
"name": "Nightly Refactor",
"isActive": true,
"ownerName": "Alex Smith",
"lastSeenAt": "2026-09-04T22:11:00Z",
"gitIdentity": "nightly-refactor[bot]",
"openClaims": [
{
"ticketId": "tkt_abc123",
"displayId": "WEB-42",
"title": "Fix login page styling",
"boardId": "brd_web",
"claimExpiresAt": "2026-09-05T08:25:00Z"
}
],
"claimsExpiredLast7Days": 1,
"reportedLast7Days": { "total": 9, "done": 5, "blocked": 1, "needsReview": 3, "partial": 0, "unknown": 0 },
"reportedLast30Days": { "total": 31, "done": 19, "blocked": 4, "needsReview": 7, "partial": 1, "unknown": 0 },
"pendingTime": { "count": 4, "minutes": 480 },
"approvedMinutesThisMonth": 1620,
"pullRequests": { "total": 12, "requiresHumanReview": 3 }
}
]
}agentClosedShare and agentMinutesShare are null when there is nothing to divide — a share with no denominator is never reported as 0.