---
title: Conventions
description: 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

:::warning 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 }
  }
}
```

:::security 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](/reference/errors), 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.

:::security 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](/reference/api).

:::danger 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](/build/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 |

:::note 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`.

:::note Two names worth checking
`required_capability`, not `capability`, when registering a tool.
`confirm_blast_radius`, not anything else, on an emergency control.

Both are `400`s 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](/scope)

## Rate limiting

```http
429 Too Many Requests
Retry-After: 2
```

Honour `Retry-After` and retry with backoff and jitter.

:::warning 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.

## Related

:::cards

:::card Authentication | /reference/authentication
Sessions, keys and agent tokens.
:::

:::card REST API | /reference/api
Every endpoint. Generated.
:::

:::card Errors and retries | /build/errors-and-retries
What to do with each failure.
:::

:::card Idempotency | /build/idempotency
Deriving a key correctly.
:::

:::
