---
title: "Sessions"
description: "12 operations under /v1/sessions (GET POST DELETE PATCH): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript."
---

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

Every operation below requires a Bearer token. See [Authentication](/docs/concepts/authentication).

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

All endpoints require authentication via Bearer token. See [Authentication](/docs/concepts/authentication) for details on API key types and scopes.

```
Authorization: Bearer csk_live_your_api_key
```

### Project scoping

Every `/v1` endpoint accepts an optional `x-codespar-project` header that pins the request to a specific [project](/docs/concepts/projects) within your account.

| Header | Required | Description |
|--------|----------|-------------|
| `x-codespar-project` | No | Project ID in the form `prj_<16chars>`. If omitted, requests resolve to the account's default project (auto-created at signup). Project-scoped API keys ignore this header and always use the key's pinned project. |

```bash
# Explicit project
curl -X POST https://api.codespar.dev/v1/sessions \
  -H "Authorization: Bearer csk_live_abc123..." \
  -H "x-codespar-project: prj_a1b2c3d4e5f6g7h8" \
  -H "Content-Type: application/json" \
  -d '{"servers": ["stripe"]}'

# Omitted -- falls back to the account's default project
curl -X POST https://api.codespar.dev/v1/sessions \
  -H "Authorization: Bearer csk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{"servers": ["stripe"]}'
```

Sessions, triggers, API keys, connections, and events are all scoped to the resolved project. See [Projects concept](/docs/concepts/projects) and the [Projects API](/docs/api/projects) for details.

---

### POST /v1/sessions

Creates a new session connected to the specified MCP servers. Returns the session object with connection status for each server.

**Auth required:** Yes (scope: `sessions:create`)

#### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `user_id` | `string` | Yes | End-user identifier on behalf of whom the agent acts |
| `servers` | `string[]` | Yes* | List of server identifiers (e.g., `["stripe", "mercado-pago"]`) |
| `preset` | `string` | No | Named preset: `brazilian`, `mexican`, `argentinian`, `colombian`, `all` |
| `manageConnections` | `object` | No | Connection-management options (see [Sessions](/docs/concepts/sessions)) |
| `metadata` | `object` | No | Key-value metadata attached to every tool call |

*Required unless `preset` is specified. Can be combined with `preset` to add additional servers.

#### curl example

```bash
curl -X POST https://api.codespar.dev/v1/sessions \
  -H "Authorization: Bearer csk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "user_123",
    "servers": ["stripe", "mercado-pago"]
  }'
```

#### Response -- `201 Created`

```json
{
  "id": "ses_abc123def456",
  "user_id": "user_123",
  "status": "active",
  "servers": [
    { "id": "stripe", "name": "Stripe", "status": "connected" },
    { "id": "mercado-pago", "name": "Mercado Pago", "status": "connected" }
  ],
  "tool_calls": 0,
  "created_at": "2026-04-15T10:30:00Z",
  "expires_at": "2026-04-15T11:00:00Z"
}
```

#### SDK equivalent

```typescript
const session = await codespar.create("user_123", {
  servers: ["stripe", "mercado-pago"],
});
```

<Callout type="info">
Sessions expire after 30 minutes of inactivity. The `expires_at` timestamp is updated with each tool call. If a server fails to connect, the session is still created with the remaining servers -- check the `status` field on each server entry.
</Callout>

---

### GET /v1/sessions

Lists all sessions for your account. Supports pagination and filtering by status.

**Auth required:** Yes (scope: `sessions:read`)

#### Query parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `status` | `string` | -- | Filter by status: `active`, `closed`, `error` |
| `limit` | `number` | `20` | Results per page (max 100) |
| `offset` | `number` | `0` | Pagination offset |

#### curl example

```bash
curl "https://api.codespar.dev/v1/sessions?status=active&limit=10" \
  -H "Authorization: Bearer csk_live_abc123..."
```

#### Response -- `200 OK`

```json
{
  "data": [
    {
      "id": "ses_abc123def456",
      "status": "active",
      "servers": ["stripe", "mercado-pago"],
      "tool_calls": 12,
      "created_at": "2026-04-15T10:30:00Z",
      "expires_at": "2026-04-15T11:00:00Z"
    },
    {
      "id": "ses_def789ghi012",
      "status": "active",
      "servers": ["correios", "nfe-io"],
      "tool_calls": 3,
      "created_at": "2026-04-15T10:15:00Z",
      "expires_at": "2026-04-15T10:45:00Z"
    }
  ],
  "total": 47,
  "limit": 10,
  "offset": 0
}
```

---

### GET /v1/sessions/:id

Retrieves details about an existing session, including connected servers, tool call count, and expiry.

**Auth required:** Yes (scope: `sessions:read`)

#### curl example

```bash
curl https://api.codespar.dev/v1/sessions/ses_abc123def456 \
  -H "Authorization: Bearer csk_live_abc123..."
```

#### Response -- `200 OK`

```json
{
  "id": "ses_abc123def456",
  "status": "active",
  "servers": [
    { "id": "stripe", "name": "Stripe", "status": "connected" },
    { "id": "mercado-pago", "name": "Mercado Pago", "status": "connected" }
  ],
  "tool_calls": 12,
  "created_at": "2026-04-15T10:30:00Z",
  "expires_at": "2026-04-15T11:00:00Z"
}
```

---

### DELETE /v1/sessions/:id

Closes an active session and releases all server connections. Closed sessions cannot be reopened; create a new session instead.

**Auth required:** Yes (scope: `sessions:create`)

#### curl example

```bash
curl -X DELETE https://api.codespar.dev/v1/sessions/ses_abc123def456 \
  -H "Authorization: Bearer csk_live_abc123..."
```

#### Response -- `200 OK`

```json
{
  "id": "ses_abc123def456",
  "status": "closed",
  "tool_calls": 12,
  "closed_at": "2026-04-15T10:45:00Z"
}
```

#### SDK equivalent

```typescript
await session.close();
```

<Callout type="info">
Closing a session is idempotent. Calling DELETE on an already-closed session returns `200 OK` with the existing closed state.
</Callout>

---

### POST /v1/sessions/:id/execute

Executes a tool call synchronously. The request blocks until the tool completes and the result is available. Use this for operations where the agent needs the response before continuing.

**Auth required:** Yes (scope: `tools:execute`)

#### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | `string` | Yes | Tool name (e.g., `codespar_pay`, `stripe_create_refund`) |
| `arguments` | `object` | Yes | Tool arguments matching the tool's `input_schema` |

#### curl example

```bash
curl -X POST https://api.codespar.dev/v1/sessions/ses_abc123/execute \
  -H "Authorization: Bearer csk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "tool": "codespar_pay",
    "params": {
      "method": "pix",
      "amount": 9990,
      "currency": "BRL",
      "description": "Order #1234",
      "recipient": "maria@example.com"
    }
  }'
```

#### Response -- `200 OK`

```json
{
  "success": true,
  "data": {
    "id": "pay_xyz789",
    "status": "PENDING",
    "amount": 99.9,
    "currency": "BRL",
    "method": "pix",
    "charge_url": "https://sandbox.asaas.com/i/pay_xyz789",
    "raw": { "...": "the provider's own payload, verbatim" }
  },
  "error": null,
  "duration": 342,
  "server": "asaas",
  "tool": "codespar_pay",
  "tool_call_id": "tc_9545",
  "called_at": "2026-09-23T14:35:11.606Z"
}
```

A tool that fails still answers `200`: read `success`, not the HTTP status. `server` names the provider the router picked.

#### SDK equivalent

```typescript
const result = await session.execute("codespar_pay", {
  method: "pix",
  amount: 9990, // minor units: R$ 99,90
  currency: "BRL",
  description: "Order #1234",
  recipient: "maria@example.com", // a Pix key
});
```

<Callout type="warn">
Each call to this endpoint counts as one [tool call](/docs/concepts/billing) for billing purposes. If the tool fails (e.g., provider error), it still counts as a tool call.
</Callout>

---

### POST /v1/sessions/:id/send

Sends a natural-language message. The CodeSpar backend runs a tool-use loop over the 14 meta-tools, calling whichever the model decides it needs, and returns the final response along with every tool call made. Use this for the simplest possible agent integration, with no client-side orchestration.

**Auth required:** Yes

#### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `message` | `string` | Yes | Natural-language instruction for the agent |

#### curl example

```bash
curl -X POST https://api.codespar.dev/v1/sessions/ses_abc123/send \
  -H "Authorization: Bearer csk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Charge R$150 via Pix for order #5678 and send a WhatsApp confirmation"
  }'
```

#### Response -- `200 OK`

```json
{
  "message": "Done. Pix QR code generated and WhatsApp message sent.",
  "tool_calls": [
    { "id": "tc_1", "tool_name": "codespar_pay", "status": "success", "duration_ms": 412 },
    { "id": "tc_2", "tool_name": "codespar_notify", "status": "success", "duration_ms": 287 }
  ],
  "iterations": 2
}
```

#### Streaming variant

Streaming is content-negotiated on the same endpoint: send `Accept: text/event-stream` on `POST /v1/sessions/:id/send` to receive an SSE stream of `StreamEvent`s (assistant text, tool use, tool result, done) -- see the [SDK reference](/docs/api/sdk) for event shapes.

#### SDK equivalent

```typescript
const result = await session.send(
  "Charge R$150 via Pix for order #5678 and send a WhatsApp confirmation",
);
```

---

### GET /v1/sessions/:id/connections

Lists OAuth connections for a session created with `manageConnections: true`. Returns the connection status and authorization URL for each server.

**Auth required:** Yes

#### curl example

```bash
curl https://api.codespar.dev/v1/sessions/ses_abc123/connections \
  -H "Authorization: Bearer csk_live_abc123..."
```

#### Response -- `200 OK`

```json
{
  "connections": [
    {
      "server": "stripe",
      "status": "connected",
      "connected_at": "2026-04-15T10:31:00Z"
    },
    {
      "server": "mercado-pago",
      "status": "pending",
      "authUrl": "https://codespar.dev/connect/mercado-pago?session=ses_abc123"
    }
  ]
}
```

| Connection status | Description |
|-------------------|-------------|
| `pending` | User has not yet authenticated. The `authUrl` field contains the OAuth URL. |
| `connected` | User has authenticated. Tool calls to this server use the user's credentials. |
| `expired` | OAuth token has expired. A new `authUrl` is provided for re-authentication. |
| `revoked` | User revoked access. A new `authUrl` is provided. |

#### SDK equivalent

```typescript
const connections = await session.connections();
```

---

### POST /v1/sessions/:id/tool-calls

Records a tool call result from an external execution. This is used in advanced workflows where the tool is executed outside of CodeSpar (e.g., by the LLM framework) and the result needs to be recorded for auditing and billing.

**Auth required:** Yes (scope: `tools:execute`)

#### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `server_id` | `string` | Yes | Server the tool belongs to |
| `tool_name` | `string` | Yes | Tool name that was called |
| `status` | `string` | No | `success` or `error` |
| `duration_ms` | `number` | No | How long the call took |
| `error_code` | `string` | No | Provider error code, when it failed |
| `input` | `object` | No | Arguments that were passed to the tool |
| `output` | `object` | No | Result returned by the tool |

#### curl example

```bash
curl -X POST https://api.codespar.dev/v1/sessions/ses_abc123/tool-calls \
  -H "Authorization: Bearer csk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "server_id": "asaas",
    "tool_name": "create_payment",
    "status": "success",
    "duration_ms": 430,
    "input": {"method": "pix", "amount": 5000},
    "output": {"payment_id": "pay_ext_001"}
  }'
```

#### Response -- `201 Created`

```json
{
  "id": "tc_9544",
  "session_id": "ses_abc123",
  "server_id": "asaas",
  "tool_name": "create_payment",
  "status": "success",
  "duration_ms": 430,
  "error_code": null,
  "input": { "method": "pix", "amount": 5000 },
  "output": { "payment_id": "pay_ext_001" },
  "called_at": "2026-09-23T14:35:11.360Z"
}
```

---

### GET /v1/sessions/:id/tool-calls

Lists all tool calls made within a session. Useful for auditing and debugging agent behavior.

**Auth required:** Yes (scope: `sessions:read`)

#### Query parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | `number` | `100` | Rows to return, newest first (min 1, max 500) |

`limit` is the only query parameter. There is no `offset` and no server-side filter by status, server, tool, or date range: the endpoint returns the newest `limit` rows for the session and you filter client-side.

#### curl example

```bash
curl "https://api.codespar.dev/v1/sessions/ses_abc123/tool-calls?limit=5" \
  -H "Authorization: Bearer csk_live_abc123..."
```

#### Response -- `200 OK`

```json
{
  "tool_calls": [
    {
      "id": "tc_001",
      "session_id": "ses_abc123",
      "server_id": "codespar",
      "tool_name": "codespar_discover",
      "status": "success",
      "duration_ms": 67,
      "error_code": null,
      "input": { "domain": "payments" },
      "output": { "servers": ["asaas", "stripe"] },
      "called_at": "2026-04-15T10:30:05Z"
    },
    {
      "id": "tc_002",
      "session_id": "ses_abc123",
      "server_id": "asaas",
      "tool_name": "codespar_pay",
      "status": "success",
      "duration_ms": 342,
      "error_code": null,
      "input": { "method": "pix", "amount": 9990, "currency": "BRL" },
      "output": { "id": "pay_z8rwa1qnr0twili5", "status": "PENDING" },
      "called_at": "2026-04-15T10:30:12Z"
    }
  ]
}
```

The envelope carries `tool_calls` and nothing else: no `total`, no `limit`, no `offset` echo. `status` is `running` on a call recorded but not yet finalized, then `success` or `error`. Two further keys, `routing` and `failover_trail`, appear on a row only when a routing decision was recorded for it.

---

### PATCH /v1/sessions/:id/tool-calls/:tc_id

Updates the status or metadata of a recorded tool call. Used to mark externally-executed tool calls as completed or failed.

**Auth required:** Yes

#### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | `string` | No | New status: `completed`, `failed` |
| `result` | `object` | No | Updated result object |
| `error` | `object` | No | Error details if the tool call failed |

#### curl example

```bash
curl -X PATCH https://api.codespar.dev/v1/sessions/ses_abc123/tool-calls/tc_rec_abc123 \
  -H "Authorization: Bearer csk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "status": "completed",
    "result": {
      "payment_id": "pay_ext_001",
      "status": "paid",
      "paid_at": "2026-04-15T10:51:00Z"
    }
  }'
```

#### Response -- `200 OK`

```json
{
  "id": "tc_rec_abc123",
  "session_id": "ses_abc123",
  "name": "codespar_pay",
  "status": "completed",
  "result": {
    "payment_id": "pay_ext_001",
    "status": "paid",
    "paid_at": "2026-04-15T10:51:00Z"
  },
  "updated_at": "2026-04-15T10:51:05Z"
}
```

---

### GET /v1/health

Consolidated health check across the six router subsystems: `db`, `vault`, `embeddings`, `fx_rates`, `telemetry`, `connections`. Always returns `200 OK` regardless of individual check status: monitors should parse the `status` field, not the HTTP code.

**Auth required:** Yes (dual-auth: API key **or** service key).

#### curl example

```bash
curl https://api.codespar.dev/v1/health \
  -H "Authorization: Bearer csk_live_abc123..."
```

#### Response -- `200 OK`

```json
{
  "status": "healthy",
  "checks": {
    "db": { "status": "healthy", "latency_ms": 12 },
    "vault": { "status": "healthy", "latency_ms": 8 },
    "embeddings": { "status": "healthy", "latency_ms": 41 },
    "fx_rates": { "status": "healthy", "stale_seconds": 312 },
    "telemetry": { "status": "healthy" },
    "connections": { "status": "healthy", "count": 47 }
  },
  "schema_version": { "current": 64, "expected": 64 },
  "observed_at": "2026-05-04T18:30:00Z"
}
```

The top-level `status` rolls up to `healthy`, `degraded`, or `down`. State transitions emit `system.health.degraded` and `system.health.recovered` events to operator webhooks.

---

### GET /health

Unauthenticated liveness probe. No subsystem detail; use `/v1/health` for that.

#### curl example

```bash
curl https://api.codespar.dev/health
```

#### Response -- `200 OK`

```json
{
  "status": "ok",
  "uptime_ms": 8421305
}
```

---

### Async settlement

`codespar_charge`, `codespar_pay`, and `codespar_kyc` return immediately with a `tool_call_id`. Settlement happens on the provider's clock: Pix in seconds, cards in minutes, Boleto and ACH in days. Blocking the agent's request for that window is not workable, so the tool call and the settlement are decoupled. You poll or stream the status endpoints below until a terminal state.

#### Correlation chain

Three identifiers connect a tool call to the provider webhook that settles it:

1. `tool_call_id`: returned by the SDK or `POST /v1/sessions/:id/execute`. Opaque. Use it as the path parameter on every status endpoint.
2. `idempotency_key`: a UUID the backend generates, stores on the tool-call record, and forwards upstream in the provider's idempotency or external-reference field.
3. `external_reference`: what the provider echoes back in its webhook payload.

When the webhook arrives, the backend normalizes it into an event keyed by `external_reference` and matches that against the tool call's `idempotency_key`. Until a matching event lands, the status endpoints return `{ "status": "pending" }`.

#### Where the idempotency key lands, per provider

The backend writes the same UUID into whichever field the provider supports. You only need this table when debugging a webhook that did not correlate.

| Provider | Field |
|----------|-------|
| Asaas | `externalReference` body field, echoed on every webhook |
| Mercado Pago | `X-Idempotency-Key` header plus `external_reference` body field |
| Stripe | `Idempotency-Key` header, surfaced on the resulting object's metadata |
| iugu | `idempotency_key` body field, echoed on webhooks |
| Stone | `idempotency_key` body field, echoed on webhooks |

One caveat: Mercado Pago's webhook payload omits `external_reference` and carries only the payment ID. On receipt, the backend fetches the payment from the Mercado Pago API (5 second timeout) to recover the reference before normalizing. If that fetch fails, the event lands without a reference and the status endpoint keeps returning `pending` until reconciled. Nothing to configure; noted here so the missing field in MP's own webhook docs does not surprise you.

#### Polling or streaming

Both read the same events, so they always agree. Poll (`payment-status`, `verification-status`) for one-off checks and batch reconciliation jobs that walk many tool calls on a schedule. Stream for live UX and long settlement windows where latency to terminal matters; see [Streaming status](#streaming-status).

---

### Streaming status

Two SDK methods push settlement state over Server-Sent Events instead of polling:

- `session.paymentStatusStream(toolCallId, opts)` for `codespar_charge` / `codespar_pay`
- `session.verificationStatusStream(toolCallId, opts)` for `codespar_kyc`

Both wrap a single GET to `/v1/tool-calls/:id/<status>/stream` that opens a long-lived `text/event-stream` response. The wrapper parses frames, fires callbacks, and resolves on terminal.

#### Event shape

| Event | When | Payload |
|-------|------|---------|
| `snapshot` | Once, immediately on connect | Current state of the tool call, often `pending` |
| `update` | On each state change after connect; zero or more | The new status envelope |
| `done` | Once, 5 seconds after the state goes terminal, then the connection closes | The final envelope |

`snapshot` is guaranteed first, `done` guaranteed last. A frame on the wire:

```
event: update
data: {"status":"completed","tool_call_id":"tc_xyz789","final_amount_minor":9990,"settled_at":"2026-04-15T10:35:12Z"}
```

#### Heartbeats

Every 15 seconds the server emits a comment frame:

```
: heartbeat 1713178215000
```

Heartbeats keep proxies from idle-closing quiet streams. Comment frames are not events: clients scanning for `event:` lines skip them naturally, and the TypeScript and Python wrappers filter them so your update callback never fires on a heartbeat.

#### Callbacks and cancellation

Awaiting the method resolves with the final envelope. Pass `onUpdate` to observe every transition, and an `AbortSignal` to drop the stream before terminal:

```typescript
const ac = new AbortController();
setTimeout(() => ac.abort(), 60_000); // give up after 60s

const final = await session.paymentStatusStream(charge.tool_call_id, {
  onUpdate: (env) => console.log(env.observed_at, env.status),
  signal: ac.signal, // rejects with AbortError if aborted before terminal
});
```

In Python, `payment_status_stream` takes `on_update` and cancels like any awaitable:

```python
task = asyncio.create_task(
    session.payment_status_stream(charge["tool_call_id"], on_update=print),
)
try:
    final = await asyncio.wait_for(task, timeout=60)
except asyncio.TimeoutError:
    task.cancel()  # status not yet terminal
```

A synchronous wrapper on `Session` blocks the calling thread until terminal, for non-async callers.

Aborting the stream does not cancel the payment or the verification; it only stops watching. State lives server-side, so you can reconnect or fall back to polling at any time.

---

### GET /v1/tool-calls/:id/payment-status

Polling endpoint for async payment settlement against `codespar_charge` and `codespar_pay` tool calls. Returns `{ status: "pending" }` until the provider webhook correlates the call (see [Async settlement](#async-settlement)); on settlement, returns the final amount and settlement timestamp.

**Auth required:** Yes

#### curl example

```bash
curl https://api.codespar.dev/v1/tool-calls/tc_xyz789/payment-status \
  -H "Authorization: Bearer csk_live_abc123..."
```

#### Response -- `200 OK` (settled)

```json
{
  "status": "completed",
  "idempotency_key": "idem_5f3a...",
  "external_reference": "asaas_pay_abc123",
  "final_amount_minor": 9990,
  "settled_at": "2026-04-15T10:35:12Z"
}
```

#### Response -- `200 OK` (pending)

```json
{
  "status": "pending"
}
```

#### SDK equivalent

```typescript
const status = await session.paymentStatus("tc_xyz789");
```

---

### GET /v1/tool-calls/:id/payment-status/stream

Server-Sent Events variant of `payment-status`. Emits `snapshot`, `update`, and `done` events with a 15s comment heartbeat; see [Streaming status](#streaming-status) for the full event contract, callbacks, and cancellation. The polling sibling stays available, clients pick whichever fits their runtime.

**Auth required:** Yes

#### curl example

```bash
curl -N https://api.codespar.dev/v1/tool-calls/tc_xyz789/payment-status/stream \
  -H "Authorization: Bearer csk_live_abc123..."
```

#### Stream output

```
event: snapshot
data: {"status":"pending"}

event: update
data: {"status":"completed","final_amount_minor":9990,"settled_at":"2026-04-15T10:35:12Z"}

event: done
data: {"status":"completed"}
```

#### SDK equivalent

```typescript
await session.paymentStatusStream("tc_xyz789", {
  onUpdate: (s) => console.log(s.status),
});
```

---

### GET /v1/tool-calls/:id/verification-status

Polling endpoint for async KYC settlement against `codespar_kyc` inquiries. Status priority is `approved` > `rejected` > `review` > `expired` > `pending` when multiple events have been recorded.

**Auth required:** Yes

#### curl example

```bash
curl https://api.codespar.dev/v1/tool-calls/tc_kyc456/verification-status \
  -H "Authorization: Bearer csk_live_abc123..."
```

#### Response -- `200 OK`

```json
{
  "status": "approved",
  "idempotency_key": "idem_kyc_a1b2...",
  "external_reference": "persona_inq_xyz",
  "settled_at": "2026-04-15T10:36:00Z"
}
```

#### SDK equivalent

```typescript
const status = await session.verificationStatus("tc_kyc456");
```

---

### GET /v1/tool-calls/:id/verification-status/stream

SSE variant of `verification-status`. Same event contract as the payment-status stream (see [Streaming status](#streaming-status)), with the verification terminal vocabulary (`approved`, `rejected`, `review`, `expired`). Polling sibling stays available.

**Auth required:** Yes

#### curl example

```bash
curl -N https://api.codespar.dev/v1/tool-calls/tc_kyc456/verification-status/stream \
  -H "Authorization: Bearer csk_live_abc123..."
```

#### SDK equivalent

```typescript
await session.verificationStatusStream("tc_kyc456", {
  onUpdate: (s) => console.log(s.status),
});
```

---

### GET /v1/meta-tools/stats

Per-(provider × canonical_tool) router observability rollup over the rolling 24h window. Reports attempt counts, success counts, and **mean** latency for each eligible rail. Powers the `/dashboard/router` observability page.

Latency is a mean (`total_latency_ms / attempts`), not a percentile: the telemetry table keeps a sum and a count per hourly bucket and no distribution, so a median cannot be derived from it. `latency_p50_ms` is a deprecated alias that carries the **same mean** under the name it first shipped with; read `mean_latency_ms`.

**Auth required:** Operator/service auth only. This is a dashboard-internal observability rollup, **not** part of the agent (Bearer-key) surface: a `csk_` API key answers `401`, and the credential that does open it is CodeSpar's platform secret, which never leaves our infrastructure. Read this rollup at [/dashboard/router](https://codespar.dev/dashboard/router); the shape below is what that page renders.

#### Query parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `meta_tool` | `string` | No | Only rows for this meta-tool (e.g., `codespar_charge`) |
| `rail` | `string` | No | Only rows for this rail (e.g., `pix`) |

#### Request

```http
GET /v1/meta-tools/stats?meta_tool=codespar_charge&rail=pix
```

#### Response -- `200 OK`

A zero-traffic row reports `success_rate: 1.0` (the router's fresh-provider grace), `mean_latency_ms: null`, and `fresh_provider_grace: true`.

```json
{
  "org_id": "org_abc123",
  "window_start": "2026-05-03T19:00:00.000Z",
  "window_end": "2026-05-04T18:00:00.000Z",
  "stats": [
    {
      "meta_tool": "codespar_charge",
      "rail": "pix",
      "currency": "BRL",
      "country": "BR",
      "provider_id": "asaas",
      "canonical_tool": "asaas:create_payment",
      "arg_transform_id": "charge_pix_brl_asaas_v1",
      "enabled": true,
      "attempts_24h": 1284,
      "successes_24h": 1271,
      "success_rate": 0.9899,
      "mean_latency_ms": 412,
      "latency_p50_ms": 412,
      "cost_minor": 0,
      "fresh_provider_grace": false
    },
    {
      "meta_tool": "codespar_charge",
      "rail": "pix",
      "currency": "BRL",
      "country": "BR",
      "provider_id": "mercado-pago",
      "canonical_tool": "mercado-pago:create_payment",
      "arg_transform_id": "charge_pix_brl_mp_v1",
      "enabled": true,
      "attempts_24h": 0,
      "successes_24h": 0,
      "success_rate": 1.0,
      "mean_latency_ms": null,
      "latency_p50_ms": null,
      "cost_minor": 0,
      "fresh_provider_grace": true
    }
  ]
}
```

---

### GET /v1/meta-tools/stats/hourly

The same telemetry for a single provider+tool pair, bucketed into the 24 hourly windows of the rolling 24h window. Used to render router latency/error sparklines. Always exactly 24 buckets, oldest first; an hour with no traffic is `attempts: 0, successes: 0, mean_latency_ms: null`. Latency is the per-bucket mean, not a percentile (see `/v1/meta-tools/stats`).

**Auth required:** Operator/service auth only (dashboard-internal; not callable with a `csk_` API key).

#### Query parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `provider_id` | `string` | Yes | Provider identifier (e.g., `asaas`) |
| `canonical_tool` | `string` | Yes | The provider's tool, as in `/v1/meta-tools/stats` (e.g., `asaas:create_payment`) |

#### Request

```http
GET /v1/meta-tools/stats/hourly?provider_id=asaas&canonical_tool=asaas:create_payment
```

#### Response -- `200 OK`

Three of the 24 buckets shown.

```json
{
  "provider_id": "asaas",
  "canonical_tool": "asaas:create_payment",
  "buckets": [
    { "bucket_start": "2026-05-03T19:00:00.000Z", "attempts": 0, "successes": 0, "mean_latency_ms": null },
    { "bucket_start": "2026-05-03T20:00:00.000Z", "attempts": 61, "successes": 60, "mean_latency_ms": 412 },
    { "bucket_start": "2026-05-04T18:00:00.000Z", "attempts": 54, "successes": 53, "mean_latency_ms": 408 }
  ]
}
```

---

### POST /v1/connections/hmac-validate

Round-trip validation for the `hmac_signed` auth_type. The dashboard calls this when an operator pastes a key + secret pair into the connect modal: the backend signs a probe request against the provider and reports whether the credentials produced a valid signature on the wire.

**Auth required:** Yes

#### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `server_id` | `string` | Yes | Provider identifier (e.g., `foxbit`) |
| `key` | `string` | Yes | Signing key |
| `secret` | `string` | Yes | Signing secret |

#### curl example

```bash
curl -X POST https://api.codespar.dev/v1/connections/hmac-validate \
  -H "Authorization: Bearer csk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "server_id": "foxbit",
    "key": "fx_key_abc",
    "secret": "fx_secret_xyz"
  }'
```

#### Response -- `200 OK`

```json
{
  "valid": true,
  "probed_endpoint": "GET /rest/v3/me"
}
```

#### Response -- `200 OK` (invalid)

```json
{
  "valid": false,
  "probed_endpoint": "GET /rest/v3/me",
  "provider_status": 401,
  "provider_message": "Invalid signature"
}
```

---

### Error responses

All endpoints follow a consistent error format:

```json
{
  "error": "error_code",
  "message": "Human-readable error description.",
  "status": 400
}
```

| Status | Error code | Description | Resolution |
|--------|-----------|-------------|------------|
| `400` | `invalid_request` | Missing required fields or invalid values | Check the request body against the schema |
| `401` | `unauthorized` | Invalid or missing API key | Verify the `Authorization` header |
| `403` | `forbidden` | API key lacks the required scope | Check key scopes in the dashboard |
| `404` | `not_found` | Session or resource not found | Verify the session ID; the session may have been closed |
| `429` | `rate_limited` | Too many requests | Wait and retry; check `Retry-After` header |
| `429` | `quota_exceeded` | Monthly tool call quota exceeded | [Upgrade your plan](/docs/concepts/billing) or wait for the next billing cycle |
| `500` | `internal_error` | Server error | Retry with exponential backoff; contact support if persistent |
| `503` | `server_unavailable` | MCP server is temporarily unavailable | The specific provider may be down; retry or use an alternative server |

#### Rate limit headers

Every response includes rate limit information:

```
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 297
X-RateLimit-Reset: 1713178260
```

### Next steps

<NextStepsGrid items={[
  { label: "CONCEPT", title: "Sessions", description: "Lifecycle, presets, connection management: the mental model.", href: "/docs/concepts/sessions" },
  { label: "REFERENCE", title: "Tools API", description: "Execute tools, list them, search by intent within a session.", href: "/docs/api/tools" },
  { label: "REFERENCE", title: "Servers API", description: "Browse the full MCP server catalog.", href: "/docs/api/servers" },
  { label: "CONCEPT", title: "Authentication", description: "API key types, scopes, rotation, and service auth.", href: "/docs/concepts/authentication" },
]} />

## Every operation, from the spec

Generated from the published OpenAPI document, so it never drifts from what the API actually serves. The section above is written by hand and carries what a schema cannot: the object model, field rules, and the order to call things in.

### GET `/v1/sessions`

<Endpoint method="GET" path="/v1/sessions" base="https://api.codespar.dev" />

List sessions

Newest first, keyed on (created_at, id) so a tie does not drop a page. `next_before` is the cursor for the following call and is null on the last page. `agent_id` lists the sessions created as that agent; a handle this organization does not have answers an empty page.

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `agent_id` | `string` | no | Only sessions created with this agent (its handle). A session created without an agent is never listed under one. |
| `before` | `string` | no | — |
| `limit` | `integer` | no | — |
| `status` | `"active"` \| `"closed"` \| `"error"` | no | — |
| `user_id` | `string` | no | — |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `400` | object | Bad Request — the body or query did not match the schema. |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `next_before` | `string,null` | yes | — |
| `sessions` | array of object | yes | — |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X GET https://api.codespar.dev/v1/sessions \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
```

</Tab>
<Tab value="HTTP">

```http
GET /v1/sessions HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/sessions",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/sessions", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.get("/v1/sessions");
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "sessions": [
    {
      "id": "obj_0000000000000000",
      "org_id": "org_0000000000000000",
      "project_id": "prj_0000000000000000",
      "user_id": "user_0000000000000000",
      "servers": [
        "string"
      ],
      "status": "active",
      "created_at": "2026-01-15T12:00:00.000Z",
      "closed_at": "2026-01-15T12:00:00.000Z",
      "agent_did": "did:web:codespar.dev:org:agent",
      "agent_id": "agt_0000000000000000",
      "tool_calls_count": 1
    }
  ],
  "next_before": "string"
}
```

</SplitPane>
</Split>

<TryIt method="GET" path="/v1/sessions" />

### POST `/v1/sessions`

<Endpoint method="POST" path="/v1/sessions" base="https://api.codespar.dev" />

Open a session

A session is the unit an agent's tool calls, policy evaluations and audit entries hang off. `servers` names the connected MCP servers it may reach (0–20), and every id must be one `GET /v1/servers` returns for this project — an id that is not gets 400 `unknown_servers` with the offenders listed, which is a different body from the schema `invalid_body` above it.

`mocks` and `chaos` are accepted only on a project whose environment is `test`: a live-environment project gets 403 mocks_not_permitted, a payload over 64 KiB gets 413, and a malformed one gets 400 with RFC 6901 pointers pointing at the offending field.

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `agent_id` | `string` | no | The registered agent this session acts as (its handle). Approvals raised in the session are attributed to it. Must name an active agent of this org, or the request answers 422. |
| `chaos` | object | no | — |
| `mocks` | object | no | — |
| `servers` | array of `string` | yes | — |
| `user_id` | `string` | no | — |

**Responses**

| Status | Body | Description |
|---|---|---|
| `201` | [Session](/docs/api/reference/schemas#session) | OK |
| `400` | object \| object | Bad Request — schema failure, or an unknown server id. |
| `403` | object | OK |
| `413` | object | OK |

**Response `201`**

| Field | Type | Required | Description |
|---|---|---|---|
| `closed_at` | `string,null (date-time)` | no | — |
| `created_at` | `string (date-time)` | yes | — |
| `id` | `string` | yes | `ses_`-prefixed |
| `org_id` | `string` | yes | — |
| `project_id` | `string` | yes | — |
| `servers` | array of `string` | yes | — |
| `status` | `"active"` \| `"closed"` \| `"error"` | yes | — |
| `user_id` | `string` | yes | — |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X POST https://api.codespar.dev/v1/sessions \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "servers": [
         "string"
       ],
       "user_id": "user_0000000000000000",
       "agent_id": "agt_0000000000000000",
       "mocks": {},
       "chaos": {}
     }'
```

</Tab>
<Tab value="HTTP">

```http
POST /v1/sessions HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "servers": [
    "string"
  ],
  "user_id": "user_0000000000000000",
  "agent_id": "agt_0000000000000000",
  "mocks": {},
  "chaos": {}
}
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/sessions",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "servers": [
        "string"
      ],
      "user_id": "user_0000000000000000",
      "agent_id": "agt_0000000000000000",
      "mocks": {},
      "chaos": {}
    },
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/sessions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "servers": [
      "string"
    ],
    "user_id": "user_0000000000000000",
    "agent_id": "agt_0000000000000000",
    "mocks": {},
    "chaos": {}
  }),
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
import { CodeSpar } from "@codespar/sdk";

declare const userId: string;
declare const servers: string[];
// ---cut---
const cs = new CodeSpar({ apiKey: process.env.CODESPAR_API_KEY });
const session = await cs.create(userId, { servers });
```

The same operation on the generated REST client (`cs.api`, from 0.12.0):

```ts
const r = await cs.api.response("post", "/v1/sessions", {
  body: {
    servers: [
      "string"
    ],
    user_id: "user_0000000000000000",
    agent_id: "agt_0000000000000000",
    mocks: {},
    chaos: {}
  }
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
  console.log(r.data);
}
```

The installed `@codespar/sdk` does not carry `agent_id` in this request body yet, so this call is not compiled. The field is in the published document and the API accepts it over HTTP today; `cs.api` types it after the next release of the package.

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 201">

```json title="application/json"
{
  "id": "ses_0000000000000000",
  "org_id": "org_0000000000000000",
  "project_id": "prj_0000000000000000",
  "user_id": "user_0000000000000000",
  "servers": [
    "string"
  ],
  "status": "active",
  "created_at": "2026-01-15T12:00:00.000Z",
  "closed_at": "2026-01-15T12:00:00.000Z"
}
```

</SplitPane>
</Split>

<TryIt method="POST" path="/v1/sessions" body={"{\"servers\":[\"string\"],\"user_id\":\"user_0000000000000000\",\"agent_id\":\"agt_0000000000000000\",\"mocks\":{},\"chaos\":{}}"} />

### GET `/v1/sessions/{id}`

<Endpoint method="GET" path="/v1/sessions/{id}" base="https://api.codespar.dev" />

Read one session

The session plus `tool_calls_count`, the number of calls recorded against it. A session belonging to another project is indistinguishable from one that does not exist: both are 404.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | — |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `404` | object | Not Found |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `closed_at` | `string,null (date-time)` | no | — |
| `created_at` | `string (date-time)` | yes | — |
| `id` | `string` | yes | `ses_`-prefixed |
| `org_id` | `string` | yes | — |
| `project_id` | `string` | yes | — |
| `servers` | array of `string` | yes | — |
| `status` | `"active"` \| `"closed"` \| `"error"` | yes | — |
| `tool_calls_count` | `integer` | yes | — |
| `user_id` | `string` | yes | — |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X GET https://api.codespar.dev/v1/sessions/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
```

</Tab>
<Tab value="HTTP">

```http
GET /v1/sessions/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/sessions/{id}",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/sessions/{id}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.get("/v1/sessions/{id}", {
  path: {
    id: "ses_0000000000000000"
  }
});
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "id": "obj_0000000000000000",
  "org_id": "org_0000000000000000",
  "project_id": "prj_0000000000000000",
  "user_id": "user_0000000000000000",
  "servers": [
    "string"
  ],
  "status": "active",
  "created_at": "2026-01-15T12:00:00.000Z",
  "closed_at": "2026-01-15T12:00:00.000Z",
  "tool_calls_count": 1
}
```

</SplitPane>
</Split>

<TryIt method="GET" path="/v1/sessions/{id}" />

### DELETE `/v1/sessions/{id}`

<Endpoint method="DELETE" path="/v1/sessions/{id}" base="https://api.codespar.dev" />

Close a session

Idempotent: closing an already-closed session returns the same body with the original `closed_at` rather than an error. Publishes `session.closed`, which a trigger can subscribe to. Returns the three fields below, not the whole session.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | — |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `404` | object | Not Found |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `closed_at` | `string,null (date-time)` | yes | — |
| `id` | `string` | yes | — |
| `status` | `"closed"` | yes | — |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X DELETE https://api.codespar.dev/v1/sessions/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
```

</Tab>
<Tab value="HTTP">

```http
DELETE /v1/sessions/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.delete(
    "https://api.codespar.dev/v1/sessions/{id}",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/sessions/{id}", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
// `session` came from codespar.create(...)
await session.close();

// The same operation on the generated REST client (cs.api, from 0.12.0):
const result = await cs.api.delete("/v1/sessions/{id}", {
  path: {
    id: "ses_0000000000000000"
  }
});
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "id": "obj_0000000000000000",
  "status": "closed",
  "closed_at": "2026-01-15T12:00:00.000Z"
}
```

</SplitPane>
</Split>

<TryIt method="DELETE" path="/v1/sessions/{id}" />

### GET `/v1/sessions/{id}/connections`

<Endpoint method="GET" path="/v1/sessions/{id}/connections" base="https://api.codespar.dev" />

List a session's servers and the tools it can call

What this session may dispatch to. `servers` is the ids the session was opened on, enriched from the catalog; `tools` is the CodeSpar meta-tool set, which is catalog-wide rather than per-server, so every entry reports `server: "codespar"` — the routing layer, not a backend.

Four of the six server fields are FALLBACKS, not facts, when the id is not in the catalog: `name` falls back to the id itself, `category` to `"unknown"`, `country` to `"BR"`, and `auth_type` to `"api_key"`. A `BR`/`api_key` pair can therefore mean either "this is a Brazilian API-key server" or "nothing is known about this id", and the two are not distinguishable from this response.

`connected` is `false` on every entry today. It is a placeholder, not an auth-state read: do not treat `false` as evidence that a credential is missing.

A session in another project is 404, the same answer as one that never existed.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | — |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `404` | object | Not Found |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `servers` | array of object | yes | — |
| `tools` | array of object | yes | — |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X GET https://api.codespar.dev/v1/sessions/{id}/connections \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
```

</Tab>
<Tab value="HTTP">

```http
GET /v1/sessions/{id}/connections HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/sessions/{id}/connections",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/sessions/{id}/connections", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const servers = await session.connections();

// The same operation on the generated REST client (cs.api, from 0.12.0):
const result = await cs.api.get("/v1/sessions/{id}/connections", {
  path: {
    id: "ses_0000000000000000"
  }
});
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "servers": [
    {
      "id": "obj_0000000000000000",
      "name": "Example",
      "category": "string",
      "country": "string",
      "auth_type": "string",
      "connected": true
    }
  ],
  "tools": [
    {
      "name": "Example",
      "description": "string",
      "input_schema": {
        "type": "object",
        "properties": {},
        "required": [
          "string"
        ]
      },
      "server": "codespar"
    }
  ]
}
```

</SplitPane>
</Split>

<TryIt method="GET" path="/v1/sessions/{id}/connections" />

### POST `/v1/sessions/{id}/execute`

<Endpoint method="POST" path="/v1/sessions/{id}/execute" base="https://api.codespar.dev" mayMove="Resolves `tool` as a CodeSpar meta-tool first, then as a catalog tool" />

Run one tool in the session

Resolves `tool` as a CodeSpar meta-tool first, then as a catalog tool, and logs the call to the session's audit chain either way.

A FAILURE IS USUALLY A 200. `success: false` with an `error` string is the normal envelope for a tool that ran and did not work; the HTTP codes below are for calls that never reached a tool at all. An unregistered `tool` is also a 200, and its envelope is SHORTER — no `tool_call_id`, no `called_at`, `server` an empty string — because nothing was logged.

`data` IS NOT NULL ON FAILURE. On a catalog tool whose upstream answered non-2xx, `data` carries the upstream's own error body; on a strategy refusal it carries `{ error, code, details? }`. Reading `data === null` as "it failed" throws away the body that explains the failure. Read `success`.

Three refusals arrive IN BAND on the catalog branch, as a 200 whose `error` starts with `policy_denied: `, `approval_required: ` or `policy_engine_error: `. The policy 403 and the policy 503 below are the same decisions taken one layer earlier, by the route's guard, before the handler runs. A caller that only checks the status code will read an in-band refusal as a successful request.

413 is absent from this operation on purpose: the guard here does not hand the caller's tool input to the policy engine, so the engine's payload cap cannot fire. `POST /v1/sessions/{id}/proxy_execute` does hand it over, and does answer 413.

WHICH STRINGS ARE LEGAL IN `tool`. This operation takes the name opaquely and this document does not enumerate it. `GET /v1/meta-tools.json` does: one entry per tool, each with a JSON Schema for what goes in `params`, served with no credential. Read that before hardcoding a name or concluding a capability is missing, because the per-tool contracts live there and not here.

SEVERAL OF THOSE TOOLS MOVE MONEY, and the envelope above shows none of what they require. `codespar_pay` spends outbound under a mandate; `codespar_charge` issues an inbound receivable. Both answer in this same 200 envelope, so `success` is the field that says whether the money instruction was accepted, and a refusal that starts with `policy_denied: ` or `approval_required: ` is a refusal even though the status is 200.

THE COBRANÇA COM VENCIMENTO IS THE ONE THAT SURPRISES CALLERS. `codespar_charge` with `action: "create"`, `method: "boleto"` and a `due_date` issues ONE receivable the payer settles EITHER as a boleto (barcode and linha digitável) OR by Pix — one debt, two payable legs, never two documents. Three properties of it are not visible from this envelope and each one costs money to learn late:

1. `idempotency_key` is REQUIRED for that combination. A create without one is refused before anything reaches the provider, and a repeat with the same key returns the charge already issued rather than a second document for the same debt. An agent that retries on a timeout without it bills the payer twice.
2. The create answers `status: "PROCESSING"` with `payable: false` and NO document. The instrument registers with the clearing house first. Subscribe to `commerce.charge.created` instead of polling for a barcode that is not late, only not made yet.
3. `commerce.charge.paid` is the event that means the funds arrived, by either leg. A boleto payment can be preceded by `commerce.charge.payment_notified`, which is the clearing house posting its operational baixa and which the issuer states is not proof of payment; every convênio has a daily cut-off, so a boleto paid after it is notified on one day and credited on the next business day. Ship on `paid`.

An agreement in N instalments is N cobranças, one per parcela, each with its own `due_date` and its own `idempotency_key`. There is no single instalment charge.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | — |

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `estimatedCost` | `number` | no | — |
| `input` | object | no | — |
| `params` | object | no | — |
| `tool` | `string` | yes | — |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object \| object | OK. The full envelope, or the short unregistered-tool envelope that carries no `tool_call_id` and no `called_at`. |
| `400` | object | Bad Request — the body or query did not match the schema. |
| `403` | object \| object | Forbidden — either a policy rule refused the call, or the org's monthly tool-call allowance is spent. The two bodies are disjoint: the policy one is `{ reason, ruleType, ruleId }` and carries NO `error` key (plus `approval_id` and `expires_at` when the refusal opened a pending approval); the quota one is `{ error: "quota_exceeded", ... }`. The quota refusal is NOT produced by default: tool calls are free under the rate card, and the allowance only applies on a deployment that explicitly re-enables it. |
| `404` | object | Not Found |
| `409` | object | Conflict — the session is not `active`, so it dispatches nothing. |
| `422` | object | Unprocessable — the session declares mocks and this tool has none left, or none at all. `tool_name` is present only on `tool_not_mocked`. Reached from the catalog branch; a meta-tool never answers 422. |
| `503` | object \| object | Service Unavailable — the route's policy guard could not answer (`{ error: "policy_engine_error" }`), or the mock engine failed (`{ code: "mocks_engine_error", message }`). Two different bodies at the same status. |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X POST https://api.codespar.dev/v1/sessions/{id}/execute \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "tool": "string",
       "params": {},
       "input": {},
       "estimatedCost": 0
     }'
```

</Tab>
<Tab value="HTTP">

```http
POST /v1/sessions/{id}/execute HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "tool": "string",
  "params": {},
  "input": {},
  "estimatedCost": 0
}
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/sessions/{id}/execute",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "tool": "string",
      "params": {},
      "input": {},
      "estimatedCost": 0
    },
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/sessions/{id}/execute", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "tool": "string",
    "params": {},
    "input": {},
    "estimatedCost": 0
  }),
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
// `tool` names a meta-tool or a catalog tool as `<server>:<tool>`.
const result = await session.execute(tool, input);

// The same operation on the generated REST client (cs.api, from 0.12.0):
const r = await cs.api.response("post", "/v1/sessions/{id}/execute", {
  path: {
    id: "ses_0000000000000000"
  },
  body: {
    tool: "string",
    params: {},
    input: {},
    estimatedCost: 0
  }
});
// r.status is one of the documented statuses (200, 403, 422),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
  console.log(r.data);
}
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "success": true,
  "error": "string",
  "duration": 0,
  "server": "string",
  "tool": "string",
  "tool_call_id": "tc_0000000000000000",
  "called_at": "string"
}
```

</SplitPane>
</Split>

<TryIt method="POST" path="/v1/sessions/{id}/execute" body={"{\"tool\":\"string\",\"params\":{},\"input\":{},\"estimatedCost\":0}"} />

### GET `/v1/sessions/{id}/mocks`

<Endpoint method="GET" path="/v1/sessions/{id}/mocks" base="https://api.codespar.dev" />

Read a session's declared mocks and their consume counters

`mocks` is the session's declared mock document verbatim, or `null` when the session declared none. `counters` is keyed by canonical tool name: `n` is how many times that tool has been consumed, `cap` is how many the declaration allows.

`cap` is DERIVED, not stored: an array entry caps at its length; an object entry is uncapped and reports `null`. A tool with a counter row but no declared entry also reports `null`, so `cap: null` means "uncapped or undeclared", never "zero remaining".

Reading this is not gated by environment. DECLARING mocks is: opening a session with a `mocks` field on a live-environment project is refused there, not here. When `n` reaches `cap`, the next dispatch of that tool fails with 422 `mocks_exhausted` on `POST /v1/sessions/{id}/execute` rather than falling through to a real provider.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | — |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `404` | object | Not Found |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `counters` | object | yes | — |
| `mocks` | `object,null` | yes | — |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X GET https://api.codespar.dev/v1/sessions/{id}/mocks \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
```

</Tab>
<Tab value="HTTP">

```http
GET /v1/sessions/{id}/mocks HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/sessions/{id}/mocks",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/sessions/{id}/mocks", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.get("/v1/sessions/{id}/mocks", {
  path: {
    id: "ses_0000000000000000"
  }
});
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "mocks": {},
  "counters": {}
}
```

</SplitPane>
</Split>

<TryIt method="GET" path="/v1/sessions/{id}/mocks" />

### POST `/v1/sessions/{id}/proxy_execute`

<Endpoint method="POST" path="/v1/sessions/{id}/proxy_execute" base="https://api.codespar.dev" mayMove="`method` + `endpoint` are forwarded to the server's upstream with credentials injected server-side" />

Make one raw HTTP call to a connected server

The escape hatch under the meta-tools: `method` + `endpoint` are forwarded to the server's upstream with credentials injected server-side. `status` is the UPSTREAM's status code, so a 200 here can carry a 404 from the provider; only a transport failure becomes 502.

`server` and `endpoint` reject `:` because the policy engine canonicalises a tool name as `<server>:<method>:<endpoint>`, and a colon in either field would let a caller forge extra delimiters. That is a 400, not a 403: bad input, not a denied decision.

MOCKED RESPONSES ANNOUNCE THEMSELVES. While a deployment answers from the mock lane, `headers` carries `x-codespar-mock: "true"` and `data` carries `mocked: true` sealed into the object itself. The second marker is the one that matters: it is also what the durable record and the audit chain store, so a mocked call cannot later be read as a real one.

413 means the policy engine refused the tool input for size. The cap is `JSON.stringify(input).length > 1_000_000` — a count of UTF-16 code units of the serialized JSON, which is neither a byte count nor 1 MiB (1 048 576), and a payload of non-ASCII text hits it at fewer bytes than an ASCII one. The check runs only under a matched `approval-required` rule; with no such rule in play, size is not measured at all.

424 appears only where the deployment is configured to require a resolved connection. Its `reason` is a closed set of three, which is every non-ok outcome the credential resolver can return.

429 always carries `retry_after_ms`, and the same figure rounded up to whole seconds in the `Retry-After` header.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | — |

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `body` | — | no | — |
| `endpoint` | `string` | yes | — |
| `estimatedCost` | `number` | no | — |
| `headers` | object | no | — |
| `method` | `"GET"` \| `"POST"` \| `"PUT"` \| `"PATCH"` \| `"DELETE"` | yes | — |
| `params` | object | no | — |
| `server` | `string` | yes | — |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `400` | object \| object | Bad Request — the body did not match the schema, or `server` is not one of the ids this session was opened on. The second body names the connected servers and points at the pre-connected sandbox meta-tool instead. |
| `403` | object \| object | Forbidden — either a policy rule refused the call, or the org's monthly tool-call allowance is spent. The two bodies are disjoint: the policy one is `{ reason, ruleType, ruleId }` and carries NO `error` key (plus `approval_id` and `expires_at` when the refusal opened a pending approval); the quota one is `{ error: "quota_exceeded", ... }`. The quota refusal is NOT produced by default: tool calls are free under the rate card, and the allowance only applies on a deployment that explicitly re-enables it. |
| `404` | object | Not Found |
| `409` | object | Conflict — the session is not `active`, so it dispatches nothing. |
| `413` | object | Payload Too Large — the serialized tool input exceeded the policy engine's cap under an `approval-required` rule. Not a policy denial: 403 and 413 are kept apart so a client can tell "refused" from "too big". |
| `422` | object | Unprocessable — the project is in `environment="test"` and this server cannot be exercised there. `reason` is the provider's `test_venue` classification: `none` means the provider runs no test environment at all, so the call would land on production with a real effect (a Z-API WhatsApp message goes to a real number); `unclassified` means nobody has established what it does in test. Answered whether or not `PROXY_REQUIRE_CONNECTION` is set, because it is about the environment and not about a missing connection, and not retryable — the same call from a `live` project reaches the provider (ent#722). |
| `424` | object \| object | Failed Dependency — no usable credential for this server. `reason` is the resolver's own outcome. Or `shared_sandbox_live_project_refused`: the project's connection for this server is the platform's shared SANDBOX credential, which serves test projects only, and this is a live project or a live-mode call; `reason` is that environment. Answered whether or not `PROXY_REQUIRE_CONNECTION` is set, nothing is sent upstream, and the fix is to connect the project's own credential (p2-proxy#5). |
| `429` | object | Too Many Requests — the per-(org, server) bucket is empty. See the `Retry-After` header. |
| `502` | object | Bad Gateway — the upstream call itself failed (transport, not status). The attempt is still logged and chained, and `proxy_call_id` names the row. |
| `503` | object | Service Unavailable — the policy engine could not answer. Fail-closed: the call did not run. Distinct from a 403, which is a decision that was taken. |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | — | no | — |
| `duration` | `number` | yes | — |
| `headers` | object | yes | — |
| `proxy_call_id` | `string` | yes | `px_`-prefixed id of the logged call. |
| `status` | `integer` | yes | The UPSTREAM status code, not this call's. |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X POST https://api.codespar.dev/v1/sessions/{id}/proxy_execute \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "server": "string",
       "endpoint": "https://example.com/hook",
       "method": "GET",
       "params": {},
       "headers": {},
       "estimatedCost": 0
     }'
```

</Tab>
<Tab value="HTTP">

```http
POST /v1/sessions/{id}/proxy_execute HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "server": "string",
  "endpoint": "https://example.com/hook",
  "method": "GET",
  "params": {},
  "headers": {},
  "estimatedCost": 0
}
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/sessions/{id}/proxy_execute",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "server": "string",
      "endpoint": "https://example.com/hook",
      "method": "GET",
      "params": {},
      "headers": {},
      "estimatedCost": 0
    },
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/sessions/{id}/proxy_execute", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "server": "string",
    "endpoint": "https://example.com/hook",
    "method": "GET",
    "params": {},
    "headers": {},
    "estimatedCost": 0
  }),
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await session.proxyExecute({ server, endpoint, method, body });

// The same operation on the generated REST client (cs.api, from 0.12.0):
const r = await cs.api.response("post", "/v1/sessions/{id}/proxy_execute", {
  path: {
    id: "ses_0000000000000000"
  },
  body: {
    server: "string",
    endpoint: "https://example.com/hook",
    method: "GET",
    params: {},
    headers: {},
    estimatedCost: 0
  }
});
// r.status is one of the documented statuses (200, 403, 422),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
  console.log(r.data);
}
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "status": 0,
  "headers": {},
  "duration": 0,
  "proxy_call_id": "proxycall_0000000000000000"
}
```

</SplitPane>
</Split>

<TryIt method="POST" path="/v1/sessions/{id}/proxy_execute" body={"{\"server\":\"string\",\"endpoint\":\"https://example.com/hook\",\"method\":\"GET\",\"params\":{},\"headers\":{},\"estimatedCost\":0}"} />

### POST `/v1/sessions/{id}/send`

<Endpoint method="POST" path="/v1/sessions/{id}/send" base="https://api.codespar.dev" mayMove="a payment that settled on turn one is still visible when turn two died" />

Drive a model loop that may call tools

Sends one user message into a tool-use loop over the CodeSpar meta-tools, up to ten model turns. Content-negotiated: `Accept: text/event-stream` streams the turns as they happen and this JSON body is never sent; anything else gets the whole transcript at the end. The schema below describes the JSON form.

WHAT `tool_calls` IS. Every entry is a call the loop attempted and logged, in order. It is NOT a list of provider calls: a refusal never reached a provider and still appears here. `server_id` is what separates them — a refusal by deny-list, policy or approval is logged under `agentgate`, a strategy error under `unknown`, a consumed mock under `mock`, and a real dispatch under the server's own id. Read `server_id` before treating an entry as money moved.

`aborted: true` means the client hung up and the loop stopped rather than buying another turn; the transcript is short because it was cut, not because the model finished.

A 500 is NOT a rollback. The loop broke part-way and everything that already ran stands: the same `tool_calls` and `iterations` fields ride on the error body, so a payment that settled on turn one is still visible when turn two died.

This route carries no policy guard of its own, so it has no policy 403 and no policy 503; the meta-tool refusals above are how a denial surfaces here. Its only 403 is the quota, which is off by default.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | — |

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `message` | `string` | yes | — |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `400` | object | Bad Request — the body or query did not match the schema. |
| `403` | object | Forbidden — the org's monthly tool-call allowance is spent. NOT produced by default: tool calls are free under the rate card, and the allowance only applies on a deployment that explicitly re-enables it. |
| `404` | object | Not Found |
| `409` | object | Conflict — the session is not `active`, so it dispatches nothing. |
| `500` | object | Internal Server Error — the loop failed mid-flight. Carries the work that already completed, in the same fields as the 200. |
| `503` | object | Service Unavailable — this deployment has no model credential configured. |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `aborted` | `true` | no | — |
| `iterations` | `integer` | yes | — |
| `message` | `string` | yes | The model's last text turn. Empty when it ended on a tool call. |
| `tool_calls` | array of object | yes | — |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X POST https://api.codespar.dev/v1/sessions/{id}/send \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "message": "string"
     }'
```

</Tab>
<Tab value="HTTP">

```http
POST /v1/sessions/{id}/send HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "message": "string"
}
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/sessions/{id}/send",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "message": "string"
    },
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/sessions/{id}/send", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "message": "string"
  }),
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const answer = await session.send(message);

// The same call, streamed:
for await (const event of session.sendStream(message)) console.log(event);

// The same operation on the generated REST client (cs.api, from 0.12.0):
const r = await cs.api.response("post", "/v1/sessions/{id}/send", {
  path: {
    id: "ses_0000000000000000"
  },
  body: {
    message: "string"
  }
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
  console.log(r.data);
}
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "message": "string",
  "tool_calls": [
    {
      "id": "obj_0000000000000000",
      "tool_name": "Example",
      "server_id": "srv_0000000000000000",
      "status": "success",
      "duration_ms": 0,
      "error_code": "string"
    }
  ],
  "iterations": 0,
  "aborted": true
}
```

</SplitPane>
</Split>

<TryIt method="POST" path="/v1/sessions/{id}/send" body={"{\"message\":\"string\"}"} />

### GET `/v1/sessions/{id}/tool-calls`

<Endpoint method="GET" path="/v1/sessions/{id}/tool-calls" base="https://api.codespar.dev" />

List one session's tool calls

Newest first, capped by `limit` and NOT paginated: there is no cursor here, so a session with more calls than `limit` (max 500) cannot be walked past the first page from this route. `GET /v1/tool-calls` is the paginated one, across the whole project.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | — |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `limit` | `integer` | no | — |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `400` | object | Bad Request — the body or query did not match the schema. |
| `404` | object | Not Found |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `tool_calls` | array of [ToolCall](/docs/api/reference/schemas#toolcall) | yes | — |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X GET https://api.codespar.dev/v1/sessions/{id}/tool-calls \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
```

</Tab>
<Tab value="HTTP">

```http
GET /v1/sessions/{id}/tool-calls HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/sessions/{id}/tool-calls",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/sessions/{id}/tool-calls", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.get("/v1/sessions/{id}/tool-calls", {
  path: {
    id: "ses_0000000000000000"
  }
});
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "tool_calls": [
    {
      "id": "tc_0000000000000000",
      "session_id": "ses_0000000000000000",
      "server_id": "srv_0000000000000000",
      "tool_name": "Example",
      "status": "running",
      "duration_ms": 0,
      "error_code": "string",
      "called_at": "2026-01-15T12:00:00.000Z"
    }
  ]
}
```

</SplitPane>
</Split>

<TryIt method="GET" path="/v1/sessions/{id}/tool-calls" />

### POST `/v1/sessions/{id}/tool-calls`

<Endpoint method="POST" path="/v1/sessions/{id}/tool-calls" base="https://api.codespar.dev" />

Record a tool call the client executed

This RECORDS a call; it does not execute one. Two ways to use it, and the difference is `status`. One shot: post a terminal `success`/`error` with `duration_ms` and `output`. Two step: post `running` with `input` as the call begins, then PATCH the outcome — which is what the SDK does and why `running` is the default.

The audit chain is appended exactly once per call, on the transition INTO a terminal status: the one-shot POST appends it here, the two-step path appends it on the PATCH. `server_id: "mock"` appends nothing (the mock store emits its own events). Chain entries written from this route are marked `client_sdk` — self-reported, distinct from server-observed execution.

409 `session_not_active` when the session is closed. The returned `id` carries the `tc_` prefix and is what the PATCH path takes.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | — |

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `duration_ms` | `integer` | no | — |
| `error_code` | `string` | no | — |
| `input` | — | no | — |
| `output` | — | no | — |
| `server_id` | `string` | yes | — |
| `status` | `"running"` \| `"success"` \| `"error"` | no | — |
| `tool_name` | `string` | yes | — |

**Responses**

| Status | Body | Description |
|---|---|---|
| `201` | [ToolCall](/docs/api/reference/schemas#toolcall) | OK |
| `400` | object | Bad Request — the body or query did not match the schema. |
| `404` | object | Not Found |
| `409` | object | OK |

**Response `201`**

| Field | Type | Required | Description |
|---|---|---|---|
| `called_at` | `string (date-time)` | yes | — |
| `duration_ms` | `integer,null` | yes | — |
| `error_code` | `string,null` | yes | — |
| `failover_trail` | — | no | — |
| `id` | `string` | yes | `tc_`-prefixed: the bigserial with a `tc_` prefix prepended |
| `input` | — | no | — |
| `output` | — | no | — |
| `routing` | — | no | — |
| `server_id` | `string` | yes | — |
| `session_id` | `string` | yes | — |
| `status` | `"running"` \| `"success"` \| `"error"` | yes | — |
| `tool_name` | `string` | yes | — |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X POST https://api.codespar.dev/v1/sessions/{id}/tool-calls \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "server_id": "srv_0000000000000000",
       "tool_name": "Example",
       "status": "running",
       "duration_ms": 0,
       "error_code": "string"
     }'
```

</Tab>
<Tab value="HTTP">

```http
POST /v1/sessions/{id}/tool-calls HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "server_id": "srv_0000000000000000",
  "tool_name": "Example",
  "status": "running",
  "duration_ms": 0,
  "error_code": "string"
}
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/sessions/{id}/tool-calls",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "server_id": "srv_0000000000000000",
      "tool_name": "Example",
      "status": "running",
      "duration_ms": 0,
      "error_code": "string"
    },
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/sessions/{id}/tool-calls", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "server_id": "srv_0000000000000000",
    "tool_name": "Example",
    "status": "running",
    "duration_ms": 0,
    "error_code": "string"
  }),
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.post("/v1/sessions/{id}/tool-calls", {
  path: {
    id: "ses_0000000000000000"
  },
  body: {
    server_id: "srv_0000000000000000",
    tool_name: "Example",
    status: "running",
    duration_ms: 0,
    error_code: "string"
  }
});
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 201">

```json title="application/json"
{
  "id": "tc_0000000000000000",
  "session_id": "ses_0000000000000000",
  "server_id": "srv_0000000000000000",
  "tool_name": "Example",
  "status": "running",
  "duration_ms": 0,
  "error_code": "string",
  "called_at": "2026-01-15T12:00:00.000Z"
}
```

</SplitPane>
</Split>

<TryIt method="POST" path="/v1/sessions/{id}/tool-calls" body={"{\"server_id\":\"srv_0000000000000000\",\"tool_name\":\"Example\",\"status\":\"running\",\"duration_ms\":0,\"error_code\":\"string\"}"} />

### PATCH `/v1/sessions/{id}/tool-calls/{tc_id}`

<Endpoint method="PATCH" path="/v1/sessions/{id}/tool-calls/{tc_id}" base="https://api.codespar.dev" />

Finalize a recorded tool call

A sparse patch: every field is optional and an omitted one keeps its current value (COALESCE), but an EMPTY body is refused — a patch that changes nothing is a caller bug, not a no-op.

The audit-chain entry is appended here only on the `running` -\> terminal transition, and only once: patching an already-terminal call updates the row and appends nothing. `tc_id` is accepted with or without the `tc_` prefix; anything that is not a positive integer after stripping it is 404, not 400.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | — |
| `tc_id` | `string` | yes | `tc_`-prefixed |

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `duration_ms` | `integer` | no | — |
| `error_code` | `string,null` | no | — |
| `output` | — | no | — |
| `status` | `"success"` \| `"error"` | no | — |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | [ToolCall](/docs/api/reference/schemas#toolcall) | OK |
| `400` | object | Bad Request — the body or query did not match the schema. |
| `404` | object | Not Found |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `called_at` | `string (date-time)` | yes | — |
| `duration_ms` | `integer,null` | yes | — |
| `error_code` | `string,null` | yes | — |
| `failover_trail` | — | no | — |
| `id` | `string` | yes | `tc_`-prefixed: the bigserial with a `tc_` prefix prepended |
| `input` | — | no | — |
| `output` | — | no | — |
| `routing` | — | no | — |
| `server_id` | `string` | yes | — |
| `session_id` | `string` | yes | — |
| `status` | `"running"` \| `"success"` \| `"error"` | yes | — |
| `tool_name` | `string` | yes | — |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X PATCH https://api.codespar.dev/v1/sessions/{id}/tool-calls/{tc_id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "status": "success",
       "duration_ms": 0,
       "error_code": "string"
     }'
```

</Tab>
<Tab value="HTTP">

```http
PATCH /v1/sessions/{id}/tool-calls/{tc_id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "status": "success",
  "duration_ms": 0,
  "error_code": "string"
}
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.patch(
    "https://api.codespar.dev/v1/sessions/{id}/tool-calls/{tc_id}",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "status": "success",
      "duration_ms": 0,
      "error_code": "string"
    },
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/sessions/{id}/tool-calls/{tc_id}", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "status": "success",
    "duration_ms": 0,
    "error_code": "string"
  }),
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.patch("/v1/sessions/{id}/tool-calls/{tc_id}", {
  path: {
    id: "ses_0000000000000000",
    tc_id: "tc_0000000000000000"
  },
  body: {
    status: "success",
    duration_ms: 0,
    error_code: "string"
  }
});
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "id": "tc_0000000000000000",
  "session_id": "ses_0000000000000000",
  "server_id": "srv_0000000000000000",
  "tool_name": "Example",
  "status": "running",
  "duration_ms": 0,
  "error_code": "string",
  "called_at": "2026-01-15T12:00:00.000Z"
}
```

</SplitPane>
</Split>

<TryIt method="PATCH" path="/v1/sessions/{id}/tool-calls/{tc_id}" body={"{\"status\":\"success\",\"duration_ms\":0,\"error_code\":\"string\"}"} />

