Open the console

API

Projects and secrets

A project is one repository producing one image, and the unit everything else is addressed by. These routes create and list projects, report what each is running, hold the secrets agents read, validate an elan.yaml before you deploy it, and tear down what you no longer need.

Most of these answer for the whole tenant

Only the routes with the project in their path — deleting and destroying a project — check a project-scoped credential's claim. Listing projects, listing agents, destroying one agent, the doctor report, secrets and validation answer for the whole tenant, whatever project the credential was issued for. The overview's project-scoping table covers every route.

Secrets belong to the tenant, not to a project

One tree per tenant, referenced by name from each project's elan.yaml. Any credential in the tenant can set or delete one — the admin role is not required — so treat secret writes as something every service user you issue can do.

Removing a project and destroying one are different calls

Destroying removes the agents and everything they use, and is admin only. Deleting the project then removes its registration, and is refused while anything is still deployed.

The samples use $API and $TOKEN, which the API overview sets.

Create a project

POST/v1/projectsdeploy-service

Registers a project: one repository, one elan.yaml, one image, and a name nobody else in your tenant has taken.

You do not have to call this before deploying

A deploy registers the project it names, so the CLI and a CI job can skip this. What it adds is a name, a display name and a repository to show before the first build has run.

Any member may create one, and the name is checked here

No admin role is needed. The slug follows the same rule elan.yaml is validated against — it becomes a Kubernetes namespace and an image path — so a name the spec would refuse is refused now rather than at the end of a build. A credential scoped to one project is not stopped from creating another.

Request
curl -X POST "$API/v1/projects" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"slug": "salmon", "display_name": "Salmon",
       "repo_url": "https://github.com/acme/salmon"}'

Body

FieldTypeDescription
slugstringrequired3–16 characters, lowercase letters, digits and single hyphens, starting with a letter: [a-z][a-z0-9]*(-[a-z0-9]+)*.
display_namestringoptionalWhat people see. Free text, never used to derive a name.
repo_urlstringoptionalThe git remote it builds from, up to 256 characters. Filled in from elan.yaml by the first deploy if you leave it out.

Responses

StatusMeaning
201Created. {project}.
401No token, or one that did not verify.
409That slug is already a project in this tenant. Nothing was changed.
422A slug that does not match the pattern, or an unknown field — named in problems.
503This deploy service has no store configured.

Response body

201 {"project": "salmon"}

List projects

GET/v1/projectsdeploy-service

Every project in your tenant, each with enough to draw a row: how many agents it runs, how many deploys it has had, and the last one.

A project nobody registered still appears

One that exists only because something was deployed to it is listed with registered: false, so the list is the index of everything rather than of what somebody typed into a form.

The whole tenant, whatever the credential

A credential scoped to one project still lists every project.

Request
curl "$API/v1/projects" -H "Authorization: Bearer $TOKEN"

Responses

StatusMeaning
200{projects}, sorted by slug.
401No token, or one that did not verify.
503This deploy service has no store configured.

Response body

{
  "projects": [
    {
      "slug": "salmon",
      "display_name": "Salmon",
      "repo_url": "https://github.com/acme/salmon",
      "created_at": "2026-10-02T08:11:40+00:00",
      "registered": true,
      "agents": 3,
      "deployments": 9,
      "last_deployment_id": "dep_b6a540abdb18482e",
      "last_state": "succeeded",
      "last_started_at": "2026-10-23T13:06:05+00:00"
    }
  ]
}

Remove a project's registration

DELETE/v1/projects/{project}deploy-service

Forgets the project's record. Nothing is torn down — that is what destroying a project does.

Refused while anything is still deployed

Removing the record of a project whose agents are running would leave workloads, a database and an egress port that nothing claims. Destroy the project first, then remove it.

Request
curl -X DELETE "$API/v1/projects/salmon" \
  -H "Authorization: Bearer $TOKEN"

Responses

StatusMeaning
204Gone — or was never there. Repeating it is harmless.
401No token, or one that did not verify.
403Not a tenant admin, or a credential scoped to another project.
409It still has deployed agents, named in the message.
503This deploy service has no store configured.

List what is deployed

GET/v1/agentsdeploy-service

Every deployed agent and workflow, what each one is running, and what its manifest declared — read from what the cluster applies, not from the deploy history.

digest is per agent, and agents move independently

A deploy moves only the targets it changed, so two agents of one project can be running different images. This is the answer to "what is my agent running", one row per agent.

manifest is a summary, and absent means not recorded

kind says agent or workflow; input_names lists a workflow's declared inputs by name (their types and defaults are in your own manifest); billing, limits and tools are what the agent declared. An agent last deployed before a field was recorded simply lacks it — read that as unknown, not as "declares nothing".

workload is the pod it runs in

Its own slug for an isolated agent; its group's name for agents that share a pod, restart together and scale as one.

Request
curl "$API/v1/agents?project=salmon" \
  -H "Authorization: Bearer $TOKEN"

Parameters

FieldTypeDescription
projectqueryoptionalOne project. Omitted, every project in the tenant — whatever project the credential was issued for.

Responses

StatusMeaning
200{agents}.
401No token, or one that did not verify.
422A project that could not be a project slug.
503This deploy service has no config repository configured.

Response body

{
  "agents": [
    {
      "project": "salmon",
      "slug": "hotel-concierge",
      "version": "1.4.0",
      "digest": "sha256:60a23ddf69f4bc62...",
      "workload": "hotel-concierge",
      "manifest": {
        "kind": "agent",
        "display_name": "Hotel concierge",
        "description": "Answers guests and books late checkout.",
        "model": "gemini-flash-latest",
        "limits": {"max_concurrent": 4, "max_llm_calls": 20, "timeout_s": 120},
        "tools": [{"ref": "google_search", "kind": "builtin"}]
      }
    }
  ]
}

Destroy an agent

DELETE/v1/agents/{agent}deploy-service

Takes one agent out of the cluster and removes what it was using: its database and roles, topics and subscriptions, secrets, grants and file storage.

project is required, and that is not pedantry

Agent slugs are unique within a project, not within a tenant, so two projects may each have a worker. Leaving project out is a 422 rather than a guess.

Type the name back

confirm must equal the agent's slug, or nothing happens and you get a 409. The workload leaves the cluster first and its dependencies go second, so nothing restarts against a database that no longer exists. A database is snapshotted before it is dropped. The image is left alone: the agent's siblings are running it.

A grouped agent leaves the shared database behind

Agents that share a pod share its database, which outlives every member but the last. The response names them in kept_for, so "destroyed" does not read as "all of it gone".

The project is a query parameter, so a scoped credential's claim is not checked

Unlike destroying a project, this route checks the tenant and the admin role and not which project the credential was issued for.

Request
curl -X DELETE "$API/v1/agents/hotel-concierge?project=salmon" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"confirm": "hotel-concierge"}'

Parameters

FieldTypeDescription
projectqueryrequiredThe project the agent is in.
confirmstringrequiredThe agent's slug, typed back. The body accepts nothing else.

Responses

StatusMeaning
200Everything it set out to remove is gone.
207Some of it survived; failed lists each resource and why. Run it again — what is already gone is skipped.
401No token, or one that did not verify.
403Not a tenant admin — a service user needs Can write.
409confirm does not match the agent's slug.
422No project, or a body with an unknown field.
503This deploy service has no teardown configured.

Response body

{
  "destroyed": "hotel-concierge",
  "commit": "9c1e5d2…",
  "removed": 7,
  "already_gone": 0,
  "failed": []
}

Destroy a project

POST/v1/projects/{project}/destroydeploy-service

Every agent in the project, everything they were using, and the project's own resources and namespace.

The same teardown as one agent, for all of them

confirm must equal the project's slug. The response has the same shape, and re-running is safe. Removing the project's registration afterwards is a separate call.

Request
curl -X POST "$API/v1/projects/salmon/destroy" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"confirm": "salmon"}'

Body

FieldTypeDescription
confirmstringrequiredThe project's slug, typed back.

Responses

StatusMeaning
200All of it is gone.
207Some of it survived; see failed, and run it again.
401No token, or one that did not verify.
403Not a tenant admin, or a credential scoped to another project.
409confirm does not match the project's slug.
422A body with an unknown field.
503This deploy service has no teardown configured.

Response body

{
  "destroyed": "salmon",
  "commit": "4b7a01e…",
  "removed": 23,
  "already_gone": 1,
  "failed": []
}

Check what is provisioned

GET/v1/doctordeploy-service

What the platform's record of your resources claims, checked against the cloud. It reports and changes nothing.

unprovisioned is the one to read first

An agent that is running and has no invoke topic recorded: its pod is healthy, the console shows it, and every invocation goes nowhere. Redeploying the project finishes provisioning it.

Ghosts, not orphans

A ghost is a resource the record lists and the cloud no longer has. Something that exists with no record cannot be found by asking the record, so it is not reported here. unknown is what could not be checked at all.

Request
curl "$API/v1/doctor" -H "Authorization: Bearer $TOKEN"

Responses

StatusMeaning
200The report, for the whole tenant.
401No token, or one that did not verify.
503This deploy service has no reconciler configured.

Response body

{
  "checked": 31,
  "healthy": 30,
  "unprovisioned": [
    {"project": "salmon", "agent": "fish-facts",
     "detail": "deployed with no invoke topic recorded, so invocations of it go nowhere. Re-deploy the project to finish provisioning it."}
  ],
  "ghosts": [],
  "unknown": []
}

Set a secret

POST/v1/secretsdeploy-service

Stores one credential in your tenant's own tree. The value is write-only: no route ever returns it.

Two kinds, and elan.yaml decides where each lands

llm is a model provider key, referenced from llm.keys. secrets is anything else, referenced from a target's secrets: list and read from the environment inside the pod.

A running pod keeps the value it started with

Setting a new value is not live until something replaces the pod — the next deploy that moves it, or a restart.

Tenant-wide, and no admin role needed

Secrets belong to the tenant, not to a project, and any credential in the tenant can set one — including a service user issued for a single project without Can write.

Request
curl -X POST "$API/v1/secrets" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"kind": "llm", "name": "default", "value": "'"$MODEL_KEY"'"}'

Body

FieldTypeDescription
kindstringrequiredllm or secrets.
namestringrequired1–63 characters of lowercase letters, digits and hyphens, not starting or ending with a hyphen. No slash and no dot, so it cannot be read as a path; the platform prefixes it with your tenant.
valuestringrequiredThe secret. Not empty.

Responses

StatusMeaning
204Stored. Setting it again adds a new version.
401No token, or one that did not verify.
422An unknown kind, a name off the pattern, an empty value or an unknown field.
503This deploy service has no secret store configured.

List secrets

GET/v1/secretsdeploy-service

The names that are set and when each last changed. Never a value.

Only what was set through this API

The list is the platform's own record of what it stored, so a value written into the secret store some other way works and is not listed.

Request
curl "$API/v1/secrets" -H "Authorization: Bearer $TOKEN"

Responses

StatusMeaning
200{secrets}, for the whole tenant.
401No token, or one that did not verify.
503This deploy service has no store configured.

Response body

{
  "secrets": [
    {"kind": "llm", "name": "default", "updated_at": "2026-10-21T10:02:11+00:00"},
    {"kind": "secrets", "name": "pms-api-key", "updated_at": "2026-10-22T16:40:03+00:00"}
  ]
}

Delete a secret

DELETE/v1/secrets/{kind}/{name}deploy-service

Removes the secret and every version of it.

The failure it causes is delayed

A pod already running keeps the value it read at startup, so nothing breaks at once — and then the next restart does not start at all, because there is nothing left to fetch. Remove the reference from elan.yaml and deploy before deleting a secret something uses.

Request
curl -X DELETE "$API/v1/secrets/secrets/pms-api-key" \
  -H "Authorization: Bearer $TOKEN"

Responses

StatusMeaning
204Gone, or was never set. Neither kind nor name is checked here, so a typo also answers 204 — list afterwards to be sure.
401No token, or one that did not verify.
503This deploy service has no secret store configured.

Validate an elan.yaml

POST/v1/validatedeploy-service

A dry run with no side effects: nothing is stored, built or provisioned. The same check the CLI's validate command and a deploy both run.

Send the manifests to get the crosscheck

With spec_yaml alone only the file is checked: that it parses and that its tenant is yours. With manifests — what your agents export about themselves — it is also checked against them, which is where most refusals come from: a target declared with no agent behind it, a secret referenced and never declared.

Every problem, not the first

An empty problems means a deploy would be accepted. A non-empty one lists all of them, each with where and message, so one round of fixes is enough.

Request
curl -X POST "$API/v1/validate" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile spec elan.yaml '{spec_yaml: $spec}')"

Body

FieldTypeDescription
spec_yamlstringrequiredThe contents of elan.yaml, not a path to it.
manifestsarrayoptionalThe exported manifests of the targets, one object each.

Responses

StatusMeaning
200{ok, problems} — 200 whether or not there are problems.
401No token, or one that did not verify.
422A body with a missing spec_yaml or an unknown field. A YAML file that does not parse is a 200 with one problem, not this.

Response body

{
  "ok": false,
  "problems": [
    {"where": "targets.receipt-ocr",
     "message": "declared here but no agent or workflow of that slug exists"}
  ]
}

Reading this as a model? llms.txt indexes every page, and each page has a “Copy for AI” control.