Authentication

/v1/workspaces/{workspaceId}/api_keys. The one exception is the global key: a system-managed key created with your account that works in every workspace, managed at /v1/account/global_api_key. Workspace-scoped operations require a workspace_id in the request path. The example paths in this guide drop the /v1/workspaces/{workspaceId}/ prefix for brevity; a real call includes it, as the guides show.
A few practical notes:
- Every key carries a list of scopes that decide which endpoints it can call. Scopes are deny-by-default: a key with none can only identify itself.
- The raw token value is only returned at creation time and again on rotation. Cadenya never echoes it back on later reads. If you misplace it, rotate the key.
- Rotate in place with
POST /v1/workspaces/{workspaceId}/api_keys/{id}:rotate(orPOST /v1/account/global_api_key:rotatefor the global key). The previous token stops working the moment you rotate, so plan the cutover. - Disable a key with
:disableto shut it off without losing it;:enablebrings it back. A disabled key’s token fails authentication on every endpoint. - API keys (e.g.
apikey_01HXK...) carry the same labels, external ids, and metadata as anything else you create.
ID choice
Every Cadenya identifier is a prefixed ULID. For example:agent_01HXK7M..., toolset_01HXK8P.... Cadenya uses this format on purpose:
- The prefix tells you the type at a glance.
agent_,toolset_,tool_,obj_,memlyr_. You never have to squint at an id wondering what it points to. - ULIDs are lexicographically sortable by creation time. Lists come back in a stable, time-ordered way, and inserts stay sequential. Friendlier on database indexes than random UUIDs.
- No hyphens. Double-click an id in your terminal or editor and the whole thing highlights as one token, ready to copy.
Common definitions
Cadenya has a few common types you see throughout its API. Most of the time, you interact with:Anatomy of a resource
Every persistent resource hangs three things off a single object:metadata, spec, and info.
metadata and spec on create and update. The user-controlled metadata fields are name, externalId, and labels; everything else inside metadata (id, accountId, workspaceId, profileId, createdAt) is server-populated and ignored if you set it. The info block is server-only and never accepted on writes. The split keeps your intent (spec) cleanly separated from how you organize the resource (metadata) and from anything Cadenya derived for you (info).
External IDs are first-class in paths
Any resource (and some operation types, like objectives) you create in Cadenya carries ametadata.externalId field you can set to whatever value makes sense in your own system: a row id, a workflow key, a slug. Set it once at creation time and you can refer to that resource by your id forever after.
externalId has to be unique inside the workspace. Subresources scope to their parent: an agent variation’s externalId only has to be unique within its agent, which means two different agents can both have a variation called concise without colliding.
Net effect: you can wire Cadenya into your existing systems without ever holding onto Cadenya IDs if you don’t want to. For a hands-on walkthrough, see Use your own IDs.
Multi-tenancy: account, workspace, operation
There are three scopes you see in the API:
Profiles (your team members and API keys) live at the account level so the same human can move between workspaces without being re-invited. Almost everything else is workspace-scoped, which gives you a clean boundary for staging vs. production, customer A vs. customer B, or two squads working out of one account.
List requests
Every list endpoint takes the same shape:
A response carries the items, plus a
pagination block while more pages remain:
nextCursor, and Cadenya omits the whole block on the last page, so its absence is the end-of-list signal. There is no total-count field; cursor pagination does not carry one. Cursors are opaque, so don’t decode them; the format can change. To page, follow nextCursor until the pagination block stops coming back.
About includeInfo
This one is worth pausing on. Resources have an info block (counts, denormalized creator profiles, derived metrics) that costs more to compute than the row itself. Most APIs pick one of two unhappy defaults:
- Always include it. Lists get slow, especially under load.
- Never include it. Clients fan out into N+1 follow-up requests to render a list with counts.
includeInfo=true and Cadenya does the work in one round trip. Piping a list into a sync job that only cares about ids and timestamps? Leave it off and your rate-limit budget thanks you.
Updates use field masks
Updates arePATCH and behave like a field mask: only the paths in updateMask change.
updateMask are applied. Anything else in the body is ignored. This keeps two clients writing to different fields from clobbering each other, and it removes a class of “I forgot to send a field, did it get cleared?” bugs.
Subresources nest
Anything that conceptually lives under something else is a subresource with a nested path:external_id: form independently. Join records (variation assignments, memory-layer assignments) get their own row id since they need to be addressable for removal, but no externalId since you don’t usually create them by hand.
Lightweight references
Two types show up when something needs to point at another resource without dragging the whole thing in:ResourceReferenceis{type, id, name}. Used when the type isn’t obvious from context: events, audit logs, and any place a tool could be a regular tool, an agent-as-tool, or a Cadenya-provided tool.BareMetadatais{id, name?}. Used when the type is implied: the tool inside aCallableTool, the agent inside an objective.
Labels for everything you want to filter
ResourceMetadata and OperationMetadata both carry a labels map of string-to-string pairs:
prefix (matches names starting with…) and a query (free-text search across name and description) so you can find things without having tagged them up front.
Snapshot isolation
When you create an objective, the agent, variation, and tools are snapshotted at that instant. Updating the underlying tool the next day doesn’t change the in-flight objective’s behavior. Webhook payloads carry the agent, variation, and objective metadata alongside the event, so consumers don’t need to fan out and refetch every referenced thing to render a notification. The mental model is: there’s “what does this resource look like right now” (the live record) and “what did this objective execute against” (the snapshot). Both are queryable. They’re not always equal.Webhooks: Standard Webhooks, with delivery records
Agent webhook endpoints follow the Standard Webhooks spec, so you can verify payloads with any compliant library and unwrap them into a discriminated union (the TypeScript and Go SDKs do this for you). Each delivery is also a tracked resource. AWebhookDelivery records:
webhookIdfor idempotent dedup of retries.- HTTP status, latency, and response headers from your endpoint.
- A status of
PENDING,COMPLETED,FAILED, orDISABLED(the last meaning Cadenya gave up after enough failures).
Big payloads ride on uploads
Anything large (memory entries over ~1 MB, file attachments) goes through a presigned upload:POST /v1/uploadsto get a presigned URL.PUTthe bytes to that URL directly.- Reference the resulting
uploadIdwhen you create the resource that needs the content.