Skip to main content

Agents

6 operations under /v1/agents (GET POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.

8 min read
View MarkdownEdit on GitHub

Base URL: https://api.codespar.dev

Every operation below requires a Bearer token. See Authentication.

GET /v1/agents

GEThttps://api.codespar.dev/v1/agents

List registered agents

The agents registered in the organization, most recent first.

The row carries has_principal, not principal_ref. The principal reference points at the document proved in KYC and does not go on the wire; the boolean answers the question an operator asks (does this agent have a principal bound?) without publishing the value.

Query parameters

NameTypeRequiredDescription
limitintegerno1 to 200; the default is 50.
project_idstringnoOnly the agents whose projects include this project.
status"active" | "suspended" | "retired" | "revoked" | "unregistered"no—

Responses

StatusBodyDescription
200objectOK
400objectThe query did not match the schema.

Response 200

FieldTypeRequiredDescription
agentsarray of objectyes—
Example request
curl -X GET https://api.codespar.dev/v1/agents \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/agents HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/agents",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/agents", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/agents");
Example response 200
application/json
{
  "agents": [
    {
      "did": "did:web:codespar.dev:org:agent",
      "agent_id": "agt_0000000000000000",
      "display_name": "Example",
      "status": "active",
      "has_principal": true,
      "created_at": "2026-01-15T12:00:00.000Z",
      "runtime": {
        "kind": "sdk",
        "detail": "string"
      },
      "role": "string",
      "projects": [
        {
          "project_id": "prj_0000000000000000",
          "environment": "live"
        }
      ]
    }
  ]
}

POST /v1/agents

POSThttps://api.codespar.dev/v1/agents

Register an agent identity and mint its first signing key

Creates a did:web identity for this org and returns its first key. The DID is DERIVED from the org and the agent_id, which is why both must be URL-safe: the accepted alphabet is letters, digits, ., _ and -, and anything else is a 400 rather than a mangled identifier.

principal_ref IS AN OPAQUE HANDLE, NEVER A DOCUMENT. This value is signed into a mandate and served to counterparties without auth. A run of eleven digits, punctuation ignored, is refused for that reason. Passing it is not proof of KYC either; verification is a separate record.

pubkey is base64. The key material's private half never leaves the server and is not returned here or anywhere else.

Both 409s are caller-state, not server fault: the handle is already registered with an active key, or the derived DID belongs to another org's row. Registering under a different handle is the way out of either.

Errors on this operation use the { error: { code, message }, request_id } envelope, not the bare { error } the session routes use.

Request body

FieldTypeRequiredDescription
agent_idstringyes—
display_namestringno—
principal_refstringyes—
rolestringno—
runtimeobjectno—

Responses

StatusBodyDescription
201objectOK
400objectBad Request — schema failure, or a DID segment outside the URL-safe alphabet.
409objectConflict — the handle or the derived DID is taken.

Response 201

FieldTypeRequiredDescription
agent_didstringyes—
kidstringyes<did>#<namespace>-<n>: the namespace names the deployment that minted the key (ent#1641), n is its ordinal.
pubkeystringyesbase64.
status"active"yes—
Example request
curl -X POST https://api.codespar.dev/v1/agents \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "agent_id": "agt_0000000000000000",
       "display_name": "Example",
       "principal_ref": "string",
       "runtime": {
         "kind": "sdk",
         "detail": "string"
       },
       "role": "string"
     }'
POST /v1/agents HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "agent_id": "agt_0000000000000000",
  "display_name": "Example",
  "principal_ref": "string",
  "runtime": {
    "kind": "sdk",
    "detail": "string"
  },
  "role": "string"
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/agents",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "agent_id": "agt_0000000000000000",
      "display_name": "Example",
      "principal_ref": "string",
      "runtime": {
        "kind": "sdk",
        "detail": "string"
      },
      "role": "string"
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/agents", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "agent_id": "agt_0000000000000000",
    "display_name": "Example",
    "principal_ref": "string",
    "runtime": {
      "kind": "sdk",
      "detail": "string"
    },
    "role": "string"
  }),
});

const data = await res.json();
const result = await cs.api.post("/v1/agents", {
  body: {
    agent_id: "agt_0000000000000000",
    display_name: "Example",
    principal_ref: "string",
    runtime: {
      kind: "sdk",
      detail: "string"
    },
    role: "string"
  }
});
Example response 201
application/json
{
  "agent_did": "did:web:codespar.dev:org:agent",
  "kid": "string",
  "pubkey": "string",
  "status": "active"
}

POST /v1/agents/{agentId}/keys/rotate

POSThttps://api.codespar.dev/v1/agents/{agentId}/keys/rotate

Mint a new signing key and retire the current one

Retires the active key and mints a fresh one in the same transaction. retired_kid names what was retired.

ROTATION DOES NOT INVALIDATE THE PAST. A signature verifies when its key was active AT ISSUANCE and is not revoked NOW, so mandates signed before the retirement keep verifying against the retired key. That is the whole difference between retiring and revoking, and it is why rotating on a schedule is safe while revoking is not.

The new kid's ordinal counts EVERY key row the identity has ever had, retired ones included: register gives #1, the first rotation #2. Ordinals are never reused, so a kid identifies one key for good.

{agentId} is the handle, not the DID; the DID is rebuilt from the authenticated org. An agent registered by another org is therefore 404 here, never 403 — 403 would confirm that it exists.

Path parameters

NameTypeRequiredDescription
agentIdstringyes—

Responses

StatusBodyDescription
201objectOK
404objectNot Found — no such agent in this org.
409objectConflict — the agent has no active key to rotate.

Response 201

FieldTypeRequiredDescription
agent_didstringyes—
kidstringyes—
pubkeystringyesbase64.
retired_kidstringyes—
status"active"yes—
Example request
curl -X POST https://api.codespar.dev/v1/agents/{agentId}/keys/rotate \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
POST /v1/agents/{agentId}/keys/rotate HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/agents/{agentId}/keys/rotate",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/agents/{agentId}/keys/rotate", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.post("/v1/agents/{agentId}/keys/rotate", {
  path: {
    agentId: "agt_0000000000000000"
  }
});
Example response 201
application/json
{
  "agent_did": "did:web:codespar.dev:org:agent",
  "kid": "string",
  "pubkey": "string",
  "retired_kid": "string",
  "status": "active"
}

POST /v1/agents/{agentId}/keys/{kid}/revoke

POSThttps://api.codespar.dev/v1/agents/{agentId}/keys/{kid}/revoke

Revoke one signing key

Withdraws a key's authority. Unlike retirement, this is retroactive: a revoked key verifies nothing afterwards, including signatures it produced while it was active.

It also changes what counterparties can see. The published did:web document lists the identity's keys where status is not revoked, so a REVOKED key disappears from it while a RETIRED one stays and keeps verifying its own past issuance. Revoking is the verb for a key you believe is compromised; rotating is the verb for hygiene.

{kid} is a full kid (<did>#<namespace>-<n>), and its # MUST be percent-encoded as %23 so the whole kid arrives as one path segment.

Idempotency is NOT offered: revoking an already-revoked key is 409, not a repeat of the 200.

Path parameters

NameTypeRequiredDescription
agentIdstringyes—
kidstringyesA full kid; the # must be sent as %23.

Responses

StatusBodyDescription
200objectOK
404objectNot Found — no such key on this org's agent.
409objectConflict — the key is already revoked.

Response 200

FieldTypeRequiredDescription
kidstringyes—
revoked_atstringyes—
status"revoked"yes—
Example request
curl -X POST https://api.codespar.dev/v1/agents/{agentId}/keys/{kid}/revoke \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
POST /v1/agents/{agentId}/keys/{kid}/revoke HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/agents/{agentId}/keys/{kid}/revoke",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/agents/{agentId}/keys/{kid}/revoke", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.post("/v1/agents/{agentId}/keys/{kid}/revoke", {
  path: {
    agentId: "agt_0000000000000000",
    kid: "string"
  }
});
Example response 200
application/json
{
  "kid": "string",
  "status": "revoked",
  "revoked_at": "string"
}

POST /v1/agents/{did}/revoke

POSThttps://api.codespar.dev/v1/agents/{did}/revoke

Revoke an agent identity

Take authority away from an agent identity: sets its status to revoked and cascades to every one of its non-revoked signing keys, in one transaction.

revoked is not retired. Retired is a graceful exit; revoked is authority withdrawn, possibly adversarial. Both are reachable states and this verb writes the second one.

WHAT IT STOPS: the agent can no longer be paid as an agent:<id> payee, can no longer mint a new KYA-bearing mandate, and can no longer spend under any mandate issued to it, bound to its key or not: every consumer lane, card included, and the org wallet refuse with agent_inactive, and so do the money-moving calls of a session that acts as it. The mandates themselves are not revoked -- the consumer signed them -- they stop clearing the spend-time check.

WHAT IT DOES NOT STOP: money moved by a caller that does not act as this agent -- a session that declared no agent, or another one, spending under another agent's mandate. The API key is not the agent: revoke or rescope keys separately.

To stop an agent without ending it, suspend it instead (POST /v1/agents/{did}/suspend).

A {did} belonging to another org returns 404, not 403: 403 would confirm that the identity exists, which is what a caller sweeping DIDs is asking.

Path parameters

NameTypeRequiredDescription
didstringyes—

Responses

StatusBodyDescription
200objectOK
404objectNot Found
409objectConflict — the agent is already revoked.

Response 200

FieldTypeRequiredDescription
didstringyes—
limitsobjectyesWhat this verb reaches and what it does not. Served with every 200 so the limit travels with the act.
revoked_atstringyes—
revoked_kidsarray of stringyesThe signing keys the cascade revoked, in creation order.
status"revoked"yes—
Example request
curl -X POST https://api.codespar.dev/v1/agents/{did}/revoke \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
POST /v1/agents/{did}/revoke HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/agents/{did}/revoke",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/agents/{did}/revoke", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.post("/v1/agents/{did}/revoke", {
  path: {
    did: "did:web:codespar.dev:org:agent"
  }
});
Example response 200
application/json
{
  "did": "did:web:codespar.dev:org:agent",
  "status": "revoked",
  "revoked_at": "string",
  "revoked_kids": [
    "string"
  ],
  "limits": {
    "stops": [
      "string"
    ],
    "does_not_stop": "string",
    "issue": "string"
  }
}

POST /v1/agents/{did}/suspend

POSThttps://api.codespar.dev/v1/agents/{did}/suspend

Suspend one agent's spend

Stops every money door from spending on this agent's behalf while the rest of the organization keeps running: spends under any mandate issued to it (every consumer lane, card included, and the org wallet) and the money-moving calls of a session that acts as it refuse with agent_suspended (403), with nothing reserved or sent. Once this call answers, no new reservation is taken for the agent; a spend that already reserved its funds finishes. Reads keep working, and the agent reads back as status: "suspended" on GET /v1/agents.

A bearer key needs agents:write; the dashboard path needs an admin. Idempotent: suspending a suspended agent answers changed: false. Only an active agent can be suspended (agent_not_suspendable otherwise). Registering the agent again does not lift it. Resuming is NOT an API-key operation: a verified admin does it from the dashboard. Appends agent_suspended to the organization's audit chain.

Card authorizations are refused at the issuer that waits for our answer (Pomelo). Bridge decides before we hear of it: freeze those cards at the issuer as well.

Path parameters

NameTypeRequiredDescription
didstringyes—

Request body

FieldTypeRequiredDescription
reasonstringno—

Responses

StatusBodyDescription
200objectOK
400objectThe body did not parse, or carried a key it does not declare.
404objectNo agent with this DID in the authenticated organization.
409objectThe agent is not active: revoked, retired or unregistered. details.status names it.

Response 200

FieldTypeRequiredDescription
agentobjectyes—
changedbooleanyes—
Example request
curl -X POST https://api.codespar.dev/v1/agents/{did}/suspend \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "reason": "string"
     }'
POST /v1/agents/{did}/suspend HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "reason": "string"
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/agents/{did}/suspend",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "reason": "string"
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/agents/{did}/suspend", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "reason": "string"
  }),
});

const data = await res.json();
const result = await cs.api.post("/v1/agents/{did}/suspend", {
  path: {
    did: "did:web:codespar.dev:org:agent"
  },
  body: {
    reason: "string"
  }
});
Example response 200
application/json
{
  "changed": true,
  "agent": {
    "did": "did:web:codespar.dev:org:agent",
    "agent_id": "agt_0000000000000000",
    "status": "suspended"
  }
}
Agents | CodeSpar