---
title: Sessions
description: Generated HTTP reference for the 12 operations the published OpenAPI document describes under sessions.
---

# Sessions

This page is generated from the published OpenAPI document. It is complete with respect to that document and says nothing about surfaces the document does not describe yet. See [what is generated here](/docs/api/reference) for what that means.

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", "mercadopago"]`) |
| `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", "mercadopago"]
  }'
```

#### Response -- `201 Created`

```json
{
  "id": "ses_abc123def456",
  "user_id": "user_123",
  "status": "active",
  "servers": [
    { "id": "stripe", "name": "Stripe", "status": "connected" },
    { "id": "mercadopago", "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", "mercadopago"],
});
```

<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", "mercadopago"],
      "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"],
      "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": "mercadopago", "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

#### 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

#### 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 '{
    "name": "codespar_pay",
    "arguments": {
      "method": "pix",
      "amount": 9990,
      "currency": "BRL",
      "description": "Order #1234",
      "customer_email": "maria@example.com"
    }
  }'
```

#### Response -- `200 OK`

```json
{
  "tool_call_id": "tc_xyz789abc",
  "name": "codespar_pay",
  "result": {
    "payment_id": "pay_xyz789",
    "status": "pending",
    "method": "pix",
    "provider": "asaas",
    "pix_code": "00020126580014br.gov.bcb.pix0136a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "qr_code_url": "https://api.codespar.dev/qr/pay_xyz789.png",
    "amount": 9990,
    "currency": "BRL",
    "expires_at": "2026-04-15T11:00:00Z"
  },
  "execution_time_ms": 342
}
```

#### SDK equivalent

```typescript
const result = await session.execute("codespar_pay", {
  method: "pix",
  amount: 9990,
  currency: "BRL",
});
```

<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 Claude tool-use loop using the session's connected servers, calling whichever tools the agent 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": "mercadopago",
      "status": "pending",
      "authUrl": "https://codespar.dev/connect/mercadopago?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 |
|-------|------|----------|-------------|
| `name` | `string` | Yes | Tool name that was called |
| `arguments` | `object` | Yes | Arguments that were passed to the tool |
| `result` | `object` | Yes | 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 '{
    "name": "codespar_pay",
    "arguments": {"method": "pix", "amount": 5000, "currency": "BRL"},
    "result": {"payment_id": "pay_ext_001", "status": "completed"}
  }'
```

#### Response -- `201 Created`

```json
{
  "id": "tc_rec_abc123",
  "session_id": "ses_abc123",
  "name": "codespar_pay",
  "status": "recorded",
  "recorded_at": "2026-04-15T10:50:00Z"
}
```

---

### 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. Reports attempt counts, success counts, and p50 latency for each rail in `META_TOOL_CATALOG`. Powers the `/dashboard/router` observability page.

**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.

#### Request

```http
GET /v1/meta-tools/stats
```

#### Response -- `200 OK`

```json
{
  "rows": [
    {
      "canonical_tool": "codespar_pay",
      "provider_id": "asaas",
      "attempts": 1284,
      "successes": 1271,
      "p50_ms": 412
    },
    {
      "canonical_tool": "codespar_charge",
      "provider_id": "mercadopago",
      "attempts": 902,
      "successes": 897,
      "p50_ms": 318
    }
  ]
}
```

---

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

Same shape as `/v1/meta-tools/stats` but bucketed into the last 24 hourly windows for a single provider+tool pair. Used to render router latency/error sparklines.

**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 | Canonical meta-tool (e.g., `codespar_pay`) |

#### Request

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

#### Response -- `200 OK`

```json
{
  "provider_id": "asaas",
  "canonical_tool": "codespar_pay",
  "buckets": [
    { "hour": "2026-05-04T18:00:00Z", "attempts": 54, "successes": 53, "p50_ms": 408 },
    { "hour": "2026-05-04T17:00:00Z", "attempts": 61, "successes": 60, "p50_ms": 412 }
  ]
}
```

---

### 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`

List sessions

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `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 [Session](/docs/api/reference/schemas#session) | yes | — |

**Example response**

```json
{
  "sessions": [
    {
      "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"
    }
  ]
}
```

**Example request**

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

### POST `/v1/sessions`

Open a session

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `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 | — |

**Example response**

```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"
}
```

**Example request**

```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",
       "mocks": {},
       "chaos": {}
     }'
```

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

Read one 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)` | 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 | — |

**Example response**

```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",
  "tool_calls_count": 1
}
```

**Example request**

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

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

Close a 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 | — |

**Example response**

```json
{
  "id": "obj_0000000000000000",
  "status": "closed"
}
```

**Example request**

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

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

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

**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 | — |

**Example response**

```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"
    }
  ]
}
```

**Example request**

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

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

Run one tool in the session

**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", ... \}`. |
| `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. |

**Example response**

```json
{
  "success": true,
  "duration": 0,
  "server": "string",
  "tool": "string",
  "tool_call_id": "tc_0000000000000000",
  "called_at": "string"
}
```

**Example request**

```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
     }'
```

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

Read a session's declared mocks and their consume counters

**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 | — |

**Example response**

```json
{
  "counters": {}
}
```

**Example request**

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

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

Make one raw HTTP call to a connected server

**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", ... \}`. |
| `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". |
| `424` | object | Failed Dependency — no usable credential for this server. `reason` is the resolver's own outcome. |
| `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. |

**Example response**

```json
{
  "status": 0,
  "headers": {},
  "duration": 0,
  "proxy_call_id": "proxycall_0000000000000000"
}
```

**Example request**

```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
     }'
```

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

Drive a model loop that may call tools

**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. |
| `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 | — |

**Example response**

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

**Example request**

```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"
     }'
```

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

List one session's tool calls

**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 | — |

**Example response**

```json
{
  "tool_calls": [
    {
      "id": "tc_0000000000000000",
      "session_id": "ses_0000000000000000",
      "server_id": "srv_0000000000000000",
      "tool_name": "Example",
      "status": "running",
      "called_at": "2026-01-15T12:00:00.000Z"
    }
  ]
}
```

**Example request**

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

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

Record a tool call the client executed

**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 | — |

**Example response**

```json
{
  "id": "tc_0000000000000000",
  "session_id": "ses_0000000000000000",
  "server_id": "srv_0000000000000000",
  "tool_name": "Example",
  "status": "running",
  "called_at": "2026-01-15T12:00:00.000Z"
}
```

**Example request**

```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"
     }'
```

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

Finalize a recorded tool call

**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 | — |

**Example response**

```json
{
  "id": "tc_0000000000000000",
  "session_id": "ses_0000000000000000",
  "server_id": "srv_0000000000000000",
  "tool_name": "Example",
  "status": "running",
  "called_at": "2026-01-15T12:00:00.000Z"
}
```

**Example request**

```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
     }'
```

