Veefore docs

Conventions

Envelopes, pagination, idempotency, scope headers and correlation — the same everywhere, so they are not repeated per endpoint.

These hold across the whole API. The per-endpoint pages do not repeat them.

#Base URLs

Serves Called by
Control plane /v1/* People, console, CLI, your services
Gateway /gateway/v1/* Agents only

Different processes, different ingresses. The gateway additionally serves /.well-known/veefore-platform-key.

#Response envelopes

Three shapes, not one

The envelope varies by endpoint and it is worth knowing which is which, because assuming the wrong one is a silent undefined in your client.

A single resource — returned directly, with no wrapper:

json
{ "id": "agt_01JAV…", "name": "finance-agent", "status": "ACTIVE" }

A collection with a total:

json
{ "items": [ … ], "total": 42 }

Used by /v1/agents and /v1/tools.

A cursor-paginated collection:

json
{ "data": [ … ], "page": { "next_cursor": "eyJvZmZzZXQiOjUwfQ", "has_more": true } }

Used by /v1/actions and /v1/audit-events — the two that grow without bound.

A plain array, for collections that are small by nature:

json
[ { … }, { … } ]

Used by /v1/policies, /v1/delegations, /v1/credentials, /v1/approvals, /v1/incidents, /v1/mcp/servers, /v1/a2a/connections, /v1/roles, /v1/members and others.

#Errors

One shape, everywhere:

json
{
  "error": {
    "code": "AUTHORITY_EXCEEDED",
    "message": "Requested action exceeds delegated authority.",
    "request_id": "req_01JAV7YQ4T2K9M3E8H6R5N1XZ0",
    "details": { "capability": "refund.create", "ceiling": 50000, "requested": 120000 }
  }
}

Branch on `code`, never on `message`

Messages are written for humans and get refined over time. Codes are a contract.

Messages also never contain secret values, key material, internal hostnames or stack detail — which means they are safe to surface, and also that they will never carry the diagnostic you were hoping for. The action record carries that, behind authentication.

Every code is at Error codes, and live at GET /v1/meta/error-codes with no authentication.

#Pagination

Cursor-based:

http
GET /v1/actions?cursor=eyJvZmZzZXQiOjUwfQ&limit=50
json
{ "data": [ … ], "page": { "next_cursor": "…", "has_more": true } }

Treat the cursor as opaque. It is not an offset and its encoding is not part of the contract.

Some list endpoints additionally accept limit and offset, which is convenient for a UI and unreliable for iterating a growing collection — rows shift under you. Use the cursor for anything that must not miss or duplicate.

#Scope headers

http
GET /v1/agents
Authorization: Bearer aos_sk_…
x-veefore-workspace: wsp_01JAV7YQ4T2K9M3E8H6R5N1XZ0
x-veefore-environment: env_01JAV7YQ4T4ZC1P8W2M6B9K3QD

Optional. Omit them and you get the organization-wide view, subject to your permissions.

The tenant never comes from a header

It comes from the authenticated session. Scope headers select a scope within your organization and cannot select another one.

Supplying a workspace or environment you have no access to is refused. These are a filter over what you can already see.

#Idempotency

http
POST /v1/agents
Idempotency-Key: register-finance-agent
Situation Result
Same key, same body The original result is returned. Nothing runs again
Same key, different body 409 IDEMPOTENCY_KEY_REUSED
Different key, same body A new, separate operation

Endpoints that accept it are marked accepts Idempotency-Key in the API reference.

A fresh UUID per attempt is the same as no key

The key must identify the operation, not the attempt. crypto.randomUUID() looks correct in review because there is a key, and defeats the entire mechanism because the retry carries a different one.

Derive it from the business operation. → Idempotency

#Correlation

Every response:

http
x-request-id: req_01JAV7YQ4T2K9M3E8H6R5N1XZ0
x-trace-id: 4bf92f3577b34da6a3ce929d0e0e4736

Both appear on the action record, on the audit event, and in every structured log line the request produced.

Given a request_id you have the decision, its full lineage, the audit event and the logs. That is the intended debugging path, which is why the ids are on the response rather than only in the logs.

#Identifiers

ULID-based, with a type prefix. Sortable by creation time, and the prefix tells you what you are holding.

Prefix
org_ Organization
wsp_ Workspace
env_ Environment
agt_ Agent
pol_ Policy
del_ Delegation
tool_ Tool
cred_ Credential
act_ Action
apr_ Approval
evt_ Audit event
inc_ Incident
req_ Request
apk_ API key

An id is not a credential

Ids appear in URLs, logs and error messages. Knowing an agent id gets you nothing; acting as that agent requires its private key.

#Timestamps

ISO-8601, UTC, millisecond precision, always Z:

json
{ "created_at": "2026-10-03T12:38:04.123Z" }

Including in signed assertions, where iat and exp are ISO-8601 strings rather than numeric epoch seconds. That departs from common JWT practice deliberately — one format across the entire surface is worth more than conformance to a convention in one place.

#Field naming

Layer Convention
HTTP request and response bodies snake_case
TypeScript SDK camelCase
Python SDK snake_case

The SDKs convert. Writing against HTTP directly, use snake_case.

Two names worth checking

required_capability, not capability, when registering a tool. confirm_blast_radius, not anything else, on an emergency control.

Both are 400s that look like a schema problem and are a field-name problem.

#Amounts

Minor currency units, as integers. 250_00 is ₹250.00.

A policy input and recorded decision context, nothing more. Veefore reads the number, decides, records what it decided, and moves nothing. → Scope

#Rate limiting

http
429 Too Many Requests
Retry-After: 2

Honour Retry-After and retry with backoff and jitter.

Without Redis, limits are per instance

In a multi-node deployment with REDIS_URL unset, each instance keeps its own rate limit and nonce state. The effective limit is multiplied by your instance count, and behaviour depends on which instance a request lands on.

Set REDIS_URL for anything multi-node.

#Versioning

Everything is under /v1. Within v1 the contract does not break — new fields are added, existing fields keep their meaning.

A breaking change would arrive as v2, served alongside v1, with v1 supported through a deprecation window. There is no silent change to the meaning of an existing field, which is the one thing a client cannot defend against.

Identity, authority, policy and enforcement for autonomous AI agents.