HTTP API
The conventions of the /api/v1 HTTP API — base URL, request and response shape, errors, idempotency keys and cursor paging.
This page is for someone writing a program that drives Cadence over HTTP. If you work with Cadence through an MCP client or the browser, you do not need it. See MCP or Web interface instead.
The HTTP API covers much of what MCP does, though not all of it. Each domain page
under Reference says which routes serve a capability, and says so when there is
no /api/v1 route for it. This page covers the conventions every route shares.
Base URL
Every route is under /api/v1 on your instance:
https://cadence.example.com/api/v1Replace https://cadence.example.com with your instance's address.
Authentication
Send a bearer token in the Authorization header of every request:
curl https://cadence.example.com/api/v1/tasks/OPS-42 \
-H "Authorization: Bearer smc_your_token_here"For the kinds of token, how to create one, and what your role in a space lets you call, see Authentication.
Requests and responses
Send a request body as JSON, with Content-Type: application/json. Responses
are JSON, including most errors. There are three exceptions:
- Downloading an attachment returns the file itself.
- Deleting a property, a dependency, a check, a check rule or a worker version
answers
204 No Contentwith no body. - Some lease routes, called for a lease that has already ended, answer
410 Gonewith no body. The410row in the status table below lists which.
Deleting a task's or a doc's attachment is not one of the exceptions. It answers
200 with the attachment it removed:
{ "attachment": { "id": 7, "filename": "notes.txt" } }curl -X PATCH https://cadence.example.com/api/v1/tasks/OPS-42 \
-H "Authorization: Bearer smc_your_token_here" \
-H "Content-Type: application/json" \
-d '{ "priority": "P1" }'Uploading an attachment is the one exception to JSON request bodies. See Task data § Attachments.
Errors
A refused request returns an HTTP error status and, except for 410, a body
with an error object. The object carries a code your program can branch on
and a message a person can read:
{ "error": { "code": "space_not_found", "message": "Space not found" } }Some refusals add a key inside the error object:
-
A
403from a role or scope check addstarget. See A 403 refusal. -
A
422withinvalid_filteraddsdetails, naming the filter it refused and the value you sent. Forpriorityandgate_statusthe value is a list of the values refused. For every other filter it is a single value, includingstageandstage_group, which take several values but report only the first one that matched nothing. On audit events, sending only one ofactor_typeandactor_id(or ofsubject_typeandsubject_id) puts an object with both underactor(orsubject), for example"details": { "actor": { "type": "User", "id": null } }. A list filter looks like this:{ "error": { "code": "invalid_filter", "message": "invalid priority value", "details": { "priority": ["P9"] } } }{ "error": { "code": "invalid_filter", "message": "limit out of range", "details": { "limit": "500" } } } -
A
422withunpermitted_fieldsaddsdetailswith the fields Cadence does not accept on that route, for example"details": { "fields": ["colour"] }. -
A
422withinvalid_requestthat comes from a field failing validation addsdetails, which maps each field that failed to its error messages. Aninvalid_requestabout the request as a whole, such as a missingstage, has nodetails.
Two refusals add a key at the top level, next to error rather than inside it:
- A doc edit refused with
stale_docaddsdoc, holding the doc as it now stands. See A 409 refusal. - A lease route refused with
protocol_version_unsupportedaddssupported, for example"supported": { "current": 2, "min": 1 }.
| Status | When | What to do |
|---|---|---|
401 | The token is missing (missing_token) or does not resolve (invalid_token). The message is Authentication required. The response carries a WWW-Authenticate header pointing at your instance's protected-resource metadata: Bearer resource_metadata="https://cadence.example.com/.well-known/oauth-protected-resource". | See Authentication § When a token is missing or wrong. |
403 | You are not allowed to make the call. Most often your role or your token's scope does not allow it. | Read code and message. See A 403 refusal. |
404 | What you named does not exist, for example space_not_found or task_not_found. | Check the key or id. |
409 | The request conflicts with the current state of what it names. The code says which conflict. | See A 409 refusal. What to do depends on the code. |
410 | A lease that has already ended (completed, failed, cancelled, lost or killed) was named on a route that claims it, heartbeats it, completes it, posts an activity to it, fetches its bundle or payload, or reports its outcome. The response has no body. Reporting usage on an ended lease is not refused this way. | Stop working the lease. An ended lease cannot be claimed or have its outcome reported again. |
422 | The request is not acceptable as sent. The code names the reason, for example missing_idempotency_key, invalid_filter or unpermitted_fields. | Fix the request. The domain page for the route lists its codes. |
424 | payload_unavailable: a lease's bundle or payload could not be built. The message says why, for example missing required env: <KEY>. When a required value is missing and the lease is dispatching, dispatched, running or cancelling, the lease is also failed, so later calls for it answer 410. If that lease works a task, Cadence also puts a blocking hold on the task, unless it already has one. While that hold stands, no pool starts new work on the task. A lease that is pending or paused is not failed and places no hold, and a lease on a workflow or space places no hold. | Fix what the message names, then clear the task's hold if it has one. Fixing the value alone restarts nothing. Clearing the hold asks Cadence to dispatch the task again, but that starts a pool only through an assignment rule at the task's stage that applies to the task and names it, only while that pool is not archived, only while the task has no unresolved dependency that blocks at its current stage and no unpassed required check at its stage, and only while no lease of that pool is active on the task the rule leases. A rule with target: parent leases the task's parent, not the task, and starts nothing if there is no parent or the parent is not of the type the rule names. Otherwise clearing the hold starts nothing. See Web interface § Holds on a task's page for how to get other pools running again. See Leases and Tasks § Holds. |
426 | protocol_version_unsupported: the client sent an X-Cadence-Protocol-Version header below the range Cadence supports (Client protocol version is below the supported range.). This applies to claiming, heartbeating and completing a lease, posting an activity to it, fetching its payload, and reporting its usage or outcome. | Upgrade the client to a version inside supported. |
429 | rate_limited: sending a message went over a cap. | See Messaging § Send a message. |
501 | unsupported_runtime: a lease's logs were asked for and cannot be read. The pool's runtime keeps no logs, or, for a pool that runs on AWS ECS, the instance has no log group configured or the lease has no recorded ECS task. The message says which. | See Leases § Read a worker's logs. |
502 | backend_error: reading a lease's logs failed in the service that stores them. The message names the error. | Try again later. |
A 403 refusal
A 403 from a role or scope check names why it refused in code, and its
error object carries a target naming the kind of thing you tried to reach,
such as Task. target is null when the call had no target. The codes are:
role_insufficient— you have no role in the space (no membership on space), your role is below what the call needs (for examplerole viewer cannot perform tasks_update (write)), or your role is not one Cadence recognises (unknown role …). An action on something shared by the whole instance, such as an image or an agent skill, needs an instance administrator, and the message says so, for exampleinstance-wide image actions require an instance admin.scope_violation— the token is limited to part of a space, and the target is outside it (target outside principal scope), or the target belongs to no space (target has no Space).space_archived— the space is archived and read-only, for examplespace OPS is archived (read-only).unknown_action— the call is not one Cadence recognises for authorization.
{
"error": {
"code": "role_insufficient",
"message": "role viewer cannot perform tasks_update (write)",
"target": "Task"
}
}A few 403s come from the route itself rather than from a role or scope check.
They carry only code and message, with no target:
-
lease_token_mismatch—The lease token does not authorize this request.You get it when:- a lease's token lists leases, reads a lease, cancels a lease or asks where a lease runs, even for its own lease
- a lease's token fetches the payload of a lease other than its own
- anything other than the lease named in the path reports that lease's usage or outcome, or adds an attachment to it. This includes a person's token, even an admin's.
See Leases.
-
lease_task_mismatch— a lease's token tried to add an attachment to a task that is not its own (a lease may only attach to its own task). -
lease_forbidden— a lease's token tried to delete a task's or a doc's attachment (a lease may not delete an attachment), or to post a comment on a task the lease can reach (Posting a comment requires a user API token.). On a task outside the lease's reach, the refusal isscope_violation. -
time_log_owner_mismatch— the time log belongs to someone else (The time log belongs to another user.). See Usage and time logs § Time logs.
A 409 refusal
A 409 means the request conflicts with the current state of what it names. Read
code to see which conflict it is. What to do depends on it.
stale_write_conflict. When two writes to the same task race, Cadence
re-reads the task and tries your request again, up to three attempts in all. Only
if every attempt loses does it refuse:
{ "error": { "code": "stale_write_conflict", "message": "This task changed concurrently; reload and retry." } }Read the task again, decide whether your change still applies, and send it again.
The other 409 codes are not races, and retrying the same request does not help:
code | When | What to do |
|---|---|---|
stale_doc | You edited a doc with expected_updated_at, and it does not match the doc's current updated_at to within a millisecond. The message names who last changed the doc and when, for example doc was updated by user:12 at 2026-09-01T10:00:00Z. The response also carries the current doc at the top level. A value Cadence cannot read as a time is ignored, and the edit goes ahead. | Read the returned doc, merge your change into it, and send it again with its updated_at. See Docs. |
task_on_hold | A lease was claimed for a task under a blocking hold (The task is on hold; no agent runs on a held task.). An advisory hold does not refuse a claim. | Cadence never clears a blocking hold on its own, and a worker cannot clear one through tasks_clear_hold, tasks_nudge or tasks_dispatch_pool. Clear it yourself. Note that a worker whose lease reaches the task can replace it with an advisory hold, after which a claim is no longer refused for the hold. See Tasks § Holds. |
not_ready, pinned, active_leases | Archiving a worker version that is not ready (Only ready WorkerVersions can be archived.), that a pool still pins, or that has active leases. | Repin the pools, or drain or cancel the leases, as the message says. See Fleet § Worker versions. |
not_draft | Discarding a worker version that is not a draft (Only draft WorkerVersions can be discarded. Archive ready versions instead.). | Archive a ready version instead. See Fleet § Worker versions. |
Idempotency keys
Two routes require an Idempotency-Key header: creating a task and posting a
comment on a task. Use a fresh unique value, such as a UUID, for each new task or
comment. If you send the same key with the same body again within 24 hours,
Cadence replays the first response and does not create a second task or post a
second comment. This makes either call safe to retry after a timeout.
- A call with no key is refused with
422andmissing_idempotency_key(Idempotency-Key header required). - Reusing a key with a different body is refused with
422andidempotency_fingerprint_mismatch(Idempotency-Key reused with a different request body).
See Tasks § Create a task and Tasks § Post a comment over the API.
Sending a message takes an optional key, either in the Idempotency-Key header
or as idempotency_key in the body. If you send both, the header is used. See
Messaging § Send a message.
Reporting a lease's or a time log's usage requires an idempotency_key in the
body. A report without one is refused with 422 and the code invalid_usage.
The first report with a key answers 201. A repeat with the same key
answers 200 with the row the first report created, and records nothing new.
Paging
Some list routes return one page at a time, with a next_cursor next to the
list. To read the next page, repeat the request with that value as cursor.
When next_cursor is null, you have the last page.
curl "https://cadence.example.com/api/v1/spaces/OPS/tasks?cursor=eyJ..." \
-H "Authorization: Bearer smc_your_token_here"These routes page this way:
| Route | Page size | Details |
|---|---|---|
GET /api/v1/spaces/:space_key/tasks | 50 by default; limit up to 200 | Tasks § List tasks |
GET /api/v1/audit_events | 50 by default; limit up to 200 | Audit events |
GET /api/v1/leases?space=… | 50 by default; limit up to 100 | Leases § List leases |
On the task and audit-event lists, a limit outside its range is refused with
422 and invalid_filter (limit out of range). The lease list does not refuse
a larger limit. It returns at most 100 instead. A cursor that Cadence cannot
read is refused with 422: invalid_filter on the task and audit-event lists,
and invalid_cursor on the lease list.
Other list routes, such as GET /api/v1/time_logs and
GET /api/v1/spaces/:space_key/docs, return the whole list in one response,
with no next_cursor.
Search routes do not page. They take a limit instead. See
Search.
Paging a lease's logs
GET /api/v1/leases/:id/logs also returns a next_cursor, but it works
differently. next_cursor is set on every page that has events, even the last
one. Keep reading until a page comes back with no events. That page's
next_cursor is null. There is no default page size. tail caps how many
events a page returns, and a tail that is not a positive whole number is
ignored. See Leases § Read a worker's logs.