Skip to main content

API overview

This page covers the ground rules that apply to every Crusoe AI Platform API call: where to send requests, how to authenticate, and how errors, paging, rate limits, and request IDs work. Every other API reference page assumes you have read this one.

Base URL

Every API on this platform is served on one public hostname:

https://api.codyhill.dev

The web console at https://console.codyhill.dev is the same platform with a UI on top.

Behind that hostname are five separate services, split by URL path. Each reads its own environment variable, so you can point one of them somewhere else without moving the rest:

EndpointWhat it servesEnv var convention
Core platform APIAgents, functions, sessions, projects, organizations, users, API keys, service accounts, quotas, audit, invitations, search, MCP servers$CAI_API
Serverless APIScale-to-zero container services and their triggers$CAI_SERVERLESS_API
Pub/Sub APITopics, subscriptions, publish, pull, acknowledge$CAI_PUBSUB_API
VectorDB APIIndexes, points, query, scroll$CAI_VECTORDB_API
MemoryStore APIInstances, stats, credential rotation$CAI_MEMORYSTORE_API

Set them in your shell before you run any example:

export CAI_API="https://api.codyhill.dev"
export CAI_SERVERLESS_API="$CAI_API"
export CAI_PUBSUB_API="$CAI_API"
export CAI_VECTORDB_API="$CAI_API"
export CAI_MEMORYSTORE_API="$CAI_API"

You only need these for raw HTTP calls. platformctl defaults to the same hostname, so the CLI works with no configuration at all. See the CLI overview.

Authentication

Every authenticated request carries one header:

Authorization: Bearer <token-or-api-key>

These credential shapes are accepted, dispatched by prefix:

CredentialShapeLifetimeWhere it comes from
API keycai_<keyid>_<secret>Until revoked (optional expiry, 1–3650 days)Personal keys or service accounts
Session tokenOpaque signed token12 hours, not revocablePOST /v1/auth/login, or platformctl login
NothingReaches only /healthz, agent invoke (the data plane, open by default), invitation preview/accept, and the public embed routes

Authorization is live: your roles and grants are re-read from the platform database on every request, so a demotion or key revocation takes effect on your very next call. The token's role claim is only a hint.

An unauthenticated call to a management endpoint returns 401, and the message starts like this:

this is a management endpoint and requires authentication. Sign in (POST /v1/auth/login) and send 'Authorization: Bearer <token>'

The message goes on to describe a fallback meant for the people who run the platform; you can ignore that part. To fix the error, sign in with platformctl login, or send an API key — see service accounts and API keys.

An invalid or expired credential is treated as no credential: identity-optional routes still work, and management routes return that same 401.

The platform splits its surface into two planes:

  • Management plane — anything that creates, changes, or inspects state (deploy, logs, secrets, members, keys). Always requires a credential.
  • Data plane — talking to an agent (POST /v1/agents/{name}/invoke). Open to anonymous callers by default; your platform administrator can close it so that invoking also requires a credential. Writing to an agent's memory bank (memorize) always requires a credential.

For the full authentication model, including service-account fences and the console's cookie flow, see API authentication.

Finding your project ID with a service-account key

Nearly every path is /v1/projects/{projectID}/..., so a machine credential needs its own project ID before it can do anything. A service account cannot get it from GET /v1/projects — that endpoint acts on behalf of a person and refuses a machine credential with 403:

{"error": "this endpoint acts on behalf of a person and cannot be used by a service_account credential; use a user session or a user API key"}

Ask whoami instead. For a credential that belongs to a project, it returns the project's ID, slug and short:

curl -s "$CAI_API/v1/auth/me" -H "Authorization: Bearer $CAI_KEY"
{
"email": "ci@h86df4.cai.local",
"role": "user",
"project_id": "01a003d3-9e11-7e24-9975-e18c77902994",
"project_slug": "payments",
"project_short": "h86df4"
}

So a script needs only the key:

PROJECT="$(curl -s "$CAI_API/v1/auth/me" -H "Authorization: Bearer $CAI_KEY" \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["project_id"])')"

curl -s "$CAI_API/v1/projects/$PROJECT/secrets" -H "Authorization: Bearer $CAI_KEY"

The three project fields appear only for a credential that has a single project — a service account or an injected workload credential. A signed-in person may belong to several, so their whoami carries none of them; list them with GET /v1/projects instead.

state, ready, message — on every resource

Every resource the platform hands you — agents, functions, MCP servers, gateway endpoints, memorystores, indexes, topics, subscriptions, serverless services, triggers — answers "can I use this yet" with the same three fields, at the top level, spelled the same way every time:

{"name": "orders", "state": "ready", "ready": true}
  • state is a lowercase string in that resource's own vocabulary, and the vocabulary differs on purpose: an agent is buildingdeployingreadyfailed, a memorystore is provisioningreadydegraded, a gateway endpoint is unpublishedpending_verificationprogrammingready. It carries meaning specific to the thing it describes, so it is the field to show a human. It is never an object.
  • ready is a boolean and means exactly the same thing everywhere: can I use this right now. It is the field to branch on. A client that only wants to know whether to proceed never has to learn a per-resource vocabulary.
  • message says why it is not ready, when it is not. It is omitted when empty.

Everything else a resource has to say — counters, addresses, versions, conditions — lives in its own named fields beside these three. Topics and subscriptions put their live counters under stats; a serverless service puts its URLs and revisions at the top level next to state.

If you have older code

status as a key is gone, and state is never an object. A resource that used to answer with "status": "ready" now answers with "state": "ready"; one that nested {"state": {"phase": "Pending", "ready": false}} now answers "state": "pending", "ready": false with the counters moved to stats; one that dissolved a "status": {...} object now publishes those fields at the top level. Capitalised states ("Ready", "Provisioning") are lowercase now, and ready is a boolean everywhere it appears — including on revisions, where it used to be the string "True".

Two things that look similar but are unchanged: a condition object's own status ({"type": "Ready", "status": "True"}) is a verbatim passthrough, and the state on an API key (live / expired / revoked) or an invitation is a credential lifecycle, not readiness.

Error envelope

Every non-2xx response from any of these APIs uses one JSON envelope:

{"error": "<message>", "request_id": "<id>"}
  • error is a plain-English message meant to be shown to a human. These docs quote them verbatim so you can search for them.
  • request_id matches the X-Request-Id response header (it is omitted only if the request-ID middleware is absent).
  • Unrouted paths (404) and wrong methods (405) render this same envelope — you never get bare HTML from the API.

Unknown fields are rejected

If a request body carries a field the API does not know, the call fails with 400 and the field is named:

{"error": "request body is not valid JSON: json: unknown field \"dimension\"", "request_id": "..."}

This is deliberate, and it exists because the alternative is worse. Unknown fields used to be dropped silently, which turned a typo into a resource that was quietly not what you asked for:

# The field is `dimensions`. This asks for a 4-dimensional index...
curl -X POST "$CAI_VECTORDB_API/v1/projects/$PROJECT/indexes" \
-H "Authorization: Bearer $CAI_TOKEN" \
-d '{"name":"docs","dimension":4}'

…and before this change it answered 201 Created with a 1536-dimensional index. You found out at your first upsert — "point 0 has 4 dimensions; index expects 1536" — and could not repair it, because an index's width is fixed at creation. Deleting and starting over was the only cure, on a resource that may already hold data.

Being told at once is cheaper than being told later, so the API now refuses rather than guesses.

What this means for you: send only documented fields. If you get this error, the message names the field — check it against the reference page for that endpoint. An extra field is never ignored, so you can trust that a 2xx means every field you sent was understood and applied.

This applies to the Serverless, Pub/Sub, VectorDB, MemoryStore and gateway publishing APIs. The core platform API is not yet strict, so treat "extra fields are refused" as the rule and do not rely on the exception.

Request IDs

Every response carries an X-Request-Id header. Error bodies echo it as request_id. Include it when you report a problem — it lets support find your exact request in the logs.

Pagination

Every list endpoint pages the same way:

ParameterTypeDefaultNotes
page_sizeinteger query param50Maximum 200. limit is accepted as an alias. A value over 200 is rejected, not clamped.
page_tokenstring query paramOpaque cursor from the previous page. Tamper-evident and bound to the list that issued it.

Responses include next_page_token; it is empty (or absent) on the last page. Keep requesting with the returned token until it comes back empty.

Errors:

  • 400 — page_size must be between 1 and 200
  • 400 — page_token is invalid or was issued for a different list; start from the first page

Rate limits

LimitValueWhat you see
Core platform API rate limitPer-principal token bucket, sized by your platform administrator; anonymous callers are limited per IP429
Sign-in throttle10 attempts per IP and 50 per account, per 15 minutes429 — too many sign-in attempts; try again in <duration> with a Retry-After header
Serverless API rate limit100 requests/sec, burst 200, per principal — counted separately by each API server, so the effective ceiling can be higher; /healthz exempt429

The 404-vs-403 rule

This rule surprises people coming from other clouds, so learn it once:

  • Any request against a project you hold no grant on returns 404 not found — never 403. Project existence is deliberately not discoverable, and "someone else's agent" looks identical to "no such agent".
  • 403 appears only when you already have some access but lack the role for that specific action. Examples: a project member calling the admin-only serverless DELETE; a user who still owes a first-login password change (403 — password change required before using this API on every management route); a service-account key trying to mint credentials.

Do not write retry logic that treats 403 as "the resource exists but I can't see it" — on project paths you will get 404 for that case.

In this section