Skip to main content

Sessions

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

22 min read
View MarkdownEdit on GitHub

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

Every operation below requires a Bearer token. See Authentication.

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

All endpoints require authentication via Bearer token. See 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 within your account.

HeaderRequiredDescription
x-codespar-projectNoProject 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.
# 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 and the Projects API 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

FieldTypeRequiredDescription
user_idstringYesEnd-user identifier on behalf of whom the agent acts
serversstring[]Yes*List of server identifiers (e.g., ["stripe", "mercado-pago"])
presetstringNoNamed preset: brazilian, mexican, argentinian, colombian, all
manageConnectionsobjectNoConnection-management options (see Sessions)
metadataobjectNoKey-value metadata attached to every tool call

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

curl example

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

{
  "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

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

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.


GET /v1/sessions

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

Auth required: Yes (scope: sessions:read)

Query parameters

ParameterTypeDefaultDescription
statusstring--Filter by status: active, closed, error
limitnumber20Results per page (max 100)
offsetnumber0Pagination offset

curl example

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

Response -- 200 OK

{
  "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

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

Response -- 200 OK

{
  "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

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

Response -- 200 OK

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

SDK equivalent

await session.close();

Closing a session is idempotent. Calling DELETE on an already-closed session returns 200 OK with the existing closed state.


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

FieldTypeRequiredDescription
namestringYesTool name (e.g., codespar_pay, stripe_create_refund)
argumentsobjectYesTool arguments matching the tool's input_schema

curl example

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

{
  "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

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
});

Each call to this endpoint counts as one tool call for billing purposes. If the tool fails (e.g., provider error), it still counts as a tool call.


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

FieldTypeRequiredDescription
messagestringYesNatural-language instruction for the agent

curl example

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

{
  "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 StreamEvents (assistant text, tool use, tool result, done) -- see the SDK reference for event shapes.

SDK equivalent

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

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

Response -- 200 OK

{
  "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 statusDescription
pendingUser has not yet authenticated. The authUrl field contains the OAuth URL.
connectedUser has authenticated. Tool calls to this server use the user's credentials.
expiredOAuth token has expired. A new authUrl is provided for re-authentication.
revokedUser revoked access. A new authUrl is provided.

SDK equivalent

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

FieldTypeRequiredDescription
server_idstringYesServer the tool belongs to
tool_namestringYesTool name that was called
statusstringNosuccess or error
duration_msnumberNoHow long the call took
error_codestringNoProvider error code, when it failed
inputobjectNoArguments that were passed to the tool
outputobjectNoResult returned by the tool

curl example

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

{
  "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

ParameterTypeDefaultDescription
limitnumber100Rows 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

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

Response -- 200 OK

{
  "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

FieldTypeRequiredDescription
statusstringNoNew status: completed, failed
resultobjectNoUpdated result object
errorobjectNoError details if the tool call failed

curl example

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

{
  "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

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

Response -- 200 OK

{
  "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

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

Response -- 200 OK

{
  "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.

ProviderField
AsaasexternalReference body field, echoed on every webhook
Mercado PagoX-Idempotency-Key header plus external_reference body field
StripeIdempotency-Key header, surfaced on the resulting object's metadata
iuguidempotency_key body field, echoed on webhooks
Stoneidempotency_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

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

EventWhenPayload
snapshotOnce, immediately on connectCurrent state of the tool call, often pending
updateOn each state change after connect; zero or moreThe new status envelope
doneOnce, 5 seconds after the state goes terminal, then the connection closesThe 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:

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:

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); on settlement, returns the final amount and settlement timestamp.

Auth required: Yes

curl example

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

Response -- 200 OK (settled)

{
  "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)

{
  "status": "pending"
}

SDK equivalent

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 for the full event contract, callbacks, and cancellation. The polling sibling stays available, clients pick whichever fits their runtime.

Auth required: Yes

curl example

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

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

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

Response -- 200 OK

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

SDK equivalent

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), with the verification terminal vocabulary (approved, rejected, review, expired). Polling sibling stays available.

Auth required: Yes

curl example

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

SDK equivalent

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; the shape below is what that page renders.

Query parameters

ParameterTypeRequiredDescription
meta_toolstringNoOnly rows for this meta-tool (e.g., codespar_charge)
railstringNoOnly rows for this rail (e.g., pix)

Request

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.

{
  "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

ParameterTypeRequiredDescription
provider_idstringYesProvider identifier (e.g., asaas)
canonical_toolstringYesThe provider's tool, as in /v1/meta-tools/stats (e.g., asaas:create_payment)

Request

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

Response -- 200 OK

Three of the 24 buckets shown.

{
  "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

FieldTypeRequiredDescription
server_idstringYesProvider identifier (e.g., foxbit)
keystringYesSigning key
secretstringYesSigning secret

curl example

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

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

Response -- 200 OK (invalid)

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

Error responses

All endpoints follow a consistent error format:

{
  "error": "error_code",
  "message": "Human-readable error description.",
  "status": 400
}
StatusError codeDescriptionResolution
400invalid_requestMissing required fields or invalid valuesCheck the request body against the schema
401unauthorizedInvalid or missing API keyVerify the Authorization header
403forbiddenAPI key lacks the required scopeCheck key scopes in the dashboard
404not_foundSession or resource not foundVerify the session ID; the session may have been closed
429rate_limitedToo many requestsWait and retry; check Retry-After header
429quota_exceededMonthly tool call quota exceededUpgrade your plan or wait for the next billing cycle
500internal_errorServer errorRetry with exponential backoff; contact support if persistent
503server_unavailableMCP server is temporarily unavailableThe 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

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

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

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

NameTypeRequiredDescription
agent_idstringnoOnly sessions created with this agent (its handle). A session created without an agent is never listed under one.
beforestringno—
limitintegerno—
status"active" | "closed" | "error"no—
user_idstringno—

Responses

StatusBodyDescription
200objectOK
400objectBad Request — the body or query did not match the schema.

Response 200

FieldTypeRequiredDescription
next_beforestring,nullyes—
sessionsarray of objectyes—
Example request
curl -X GET https://api.codespar.dev/v1/sessions \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/sessions HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
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()
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();
const result = await cs.api.get("/v1/sessions");
Example response 200
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"
}

POST /v1/sessions

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

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

FieldTypeRequiredDescription
agent_idstringnoThe 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.
chaosobjectno—
mocksobjectno—
serversarray of stringyes—
user_idstringno—

Responses

StatusBodyDescription
201SessionOK
400object | objectBad Request — schema failure, or an unknown server id.
403objectOK
413objectOK

Response 201

FieldTypeRequiredDescription
closed_atstring,null (date-time)no—
created_atstring (date-time)yes—
idstringyesses_-prefixed
org_idstringyes—
project_idstringyes—
serversarray of stringyes—
status"active" | "closed" | "error"yes—
user_idstringyes—
Example request
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": {}
     }'
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": {}
}
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()
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();
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):

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.

Example response 201
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"
}

GET /v1/sessions/{id}

GEThttps://api.codespar.dev/v1/sessions/{id}

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

NameTypeRequiredDescription
idstringyes—

Responses

StatusBodyDescription
200objectOK
404objectNot Found

Response 200

FieldTypeRequiredDescription
closed_atstring,null (date-time)no—
created_atstring (date-time)yes—
idstringyesses_-prefixed
org_idstringyes—
project_idstringyes—
serversarray of stringyes—
status"active" | "closed" | "error"yes—
tool_calls_countintegeryes—
user_idstringyes—
Example request
curl -X GET https://api.codespar.dev/v1/sessions/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/sessions/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
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()
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();
const result = await cs.api.get("/v1/sessions/{id}", {
  path: {
    id: "ses_0000000000000000"
  }
});
Example response 200
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
}

DELETE /v1/sessions/{id}

DELETEhttps://api.codespar.dev/v1/sessions/{id}

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

NameTypeRequiredDescription
idstringyes—

Responses

StatusBodyDescription
200objectOK
404objectNot Found

Response 200

FieldTypeRequiredDescription
closed_atstring,null (date-time)yes—
idstringyes—
status"closed"yes—
Example request
curl -X DELETE https://api.codespar.dev/v1/sessions/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
DELETE /v1/sessions/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
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()
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();
// `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"
  }
});
Example response 200
application/json
{
  "id": "obj_0000000000000000",
  "status": "closed",
  "closed_at": "2026-01-15T12:00:00.000Z"
}

GET /v1/sessions/{id}/connections

GEThttps://api.codespar.dev/v1/sessions/{id}/connections

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

NameTypeRequiredDescription
idstringyes—

Responses

StatusBodyDescription
200objectOK
404objectNot Found

Response 200

FieldTypeRequiredDescription
serversarray of objectyes—
toolsarray of objectyes—
Example request
curl -X GET https://api.codespar.dev/v1/sessions/{id}/connections \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/sessions/{id}/connections HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
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()
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();
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"
  }
});
Example response 200
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"
    }
  ]
}

POST /v1/sessions/{id}/execute

POSThttps://api.codespar.dev/v1/sessions/{id}/execute
Can move money

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

NameTypeRequiredDescription
idstringyes—

Request body

FieldTypeRequiredDescription
estimatedCostnumberno—
inputobjectno—
paramsobjectno—
toolstringyes—

Responses

StatusBodyDescription
200object | objectOK. The full envelope, or the short unregistered-tool envelope that carries no tool_call_id and no called_at.
400objectBad Request — the body or query did not match the schema.
403object | objectForbidden — 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.
404objectNot Found
409objectConflict — the session is not active, so it dispatches nothing.
422objectUnprocessable — 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.
503object | objectService 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 request
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
     }'
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
}
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()
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();
// `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);
}
Example response 200
application/json
{
  "success": true,
  "error": "string",
  "duration": 0,
  "server": "string",
  "tool": "string",
  "tool_call_id": "tc_0000000000000000",
  "called_at": "string"
}

GET /v1/sessions/{id}/mocks

GEThttps://api.codespar.dev/v1/sessions/{id}/mocks

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

NameTypeRequiredDescription
idstringyes—

Responses

StatusBodyDescription
200objectOK
404objectNot Found

Response 200

FieldTypeRequiredDescription
countersobjectyes—
mocksobject,nullyes—
Example request
curl -X GET https://api.codespar.dev/v1/sessions/{id}/mocks \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/sessions/{id}/mocks HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
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()
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();
const result = await cs.api.get("/v1/sessions/{id}/mocks", {
  path: {
    id: "ses_0000000000000000"
  }
});
Example response 200
application/json
{
  "mocks": {},
  "counters": {}
}

POST /v1/sessions/{id}/proxy_execute

POSThttps://api.codespar.dev/v1/sessions/{id}/proxy_execute
Can move money

`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

NameTypeRequiredDescription
idstringyes—

Request body

FieldTypeRequiredDescription
body—no—
endpointstringyes—
estimatedCostnumberno—
headersobjectno—
method"GET" | "POST" | "PUT" | "PATCH" | "DELETE"yes—
paramsobjectno—
serverstringyes—

Responses

StatusBodyDescription
200objectOK
400object | objectBad 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.
403object | objectForbidden — 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.
404objectNot Found
409objectConflict — the session is not active, so it dispatches nothing.
413objectPayload 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".
422objectUnprocessable — 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).
424object | objectFailed 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).
429objectToo Many Requests — the per-(org, server) bucket is empty. See the Retry-After header.
502objectBad Gateway — the upstream call itself failed (transport, not status). The attempt is still logged and chained, and proxy_call_id names the row.
503objectService 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

FieldTypeRequiredDescription
data—no—
durationnumberyes—
headersobjectyes—
proxy_call_idstringyespx_-prefixed id of the logged call.
statusintegeryesThe UPSTREAM status code, not this call's.
Example request
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}/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
}
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()
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();
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);
}
Example response 200
application/json
{
  "status": 0,
  "headers": {},
  "duration": 0,
  "proxy_call_id": "proxycall_0000000000000000"
}

POST /v1/sessions/{id}/send

POSThttps://api.codespar.dev/v1/sessions/{id}/send
Can move money

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

NameTypeRequiredDescription
idstringyes—

Request body

FieldTypeRequiredDescription
messagestringyes—

Responses

StatusBodyDescription
200objectOK
400objectBad Request — the body or query did not match the schema.
403objectForbidden — 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.
404objectNot Found
409objectConflict — the session is not active, so it dispatches nothing.
500objectInternal Server Error — the loop failed mid-flight. Carries the work that already completed, in the same fields as the 200.
503objectService Unavailable — this deployment has no model credential configured.

Response 200

FieldTypeRequiredDescription
abortedtrueno—
iterationsintegeryes—
messagestringyesThe model's last text turn. Empty when it ended on a tool call.
tool_callsarray of objectyes—
Example request
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"
     }'
POST /v1/sessions/{id}/send HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "message": "string"
}
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()
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();
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);
}
Example response 200
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
}

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

GEThttps://api.codespar.dev/v1/sessions/{id}/tool-calls

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

NameTypeRequiredDescription
idstringyes—

Query parameters

NameTypeRequiredDescription
limitintegerno—

Responses

StatusBodyDescription
200objectOK
400objectBad Request — the body or query did not match the schema.
404objectNot Found

Response 200

FieldTypeRequiredDescription
tool_callsarray of ToolCallyes—
Example request
curl -X GET https://api.codespar.dev/v1/sessions/{id}/tool-calls \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/sessions/{id}/tool-calls HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
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()
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();
const result = await cs.api.get("/v1/sessions/{id}/tool-calls", {
  path: {
    id: "ses_0000000000000000"
  }
});
Example response 200
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"
    }
  ]
}

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

POSThttps://api.codespar.dev/v1/sessions/{id}/tool-calls

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

NameTypeRequiredDescription
idstringyes—

Request body

FieldTypeRequiredDescription
duration_msintegerno—
error_codestringno—
input—no—
output—no—
server_idstringyes—
status"running" | "success" | "error"no—
tool_namestringyes—

Responses

StatusBodyDescription
201ToolCallOK
400objectBad Request — the body or query did not match the schema.
404objectNot Found
409objectOK

Response 201

FieldTypeRequiredDescription
called_atstring (date-time)yes—
duration_msinteger,nullyes—
error_codestring,nullyes—
failover_trail—no—
idstringyestc_-prefixed: the bigserial with a tc_ prefix prepended
input—no—
output—no—
routing—no—
server_idstringyes—
session_idstringyes—
status"running" | "success" | "error"yes—
tool_namestringyes—
Example request
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"
     }'
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"
}
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()
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();
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"
  }
});
Example response 201
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"
}

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

PATCHhttps://api.codespar.dev/v1/sessions/{id}/tool-calls/{tc_id}

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

NameTypeRequiredDescription
idstringyes—
tc_idstringyestc_-prefixed

Request body

FieldTypeRequiredDescription
duration_msintegerno—
error_codestring,nullno—
output—no—
status"success" | "error"no—

Responses

StatusBodyDescription
200ToolCallOK
400objectBad Request — the body or query did not match the schema.
404objectNot Found

Response 200

FieldTypeRequiredDescription
called_atstring (date-time)yes—
duration_msinteger,nullyes—
error_codestring,nullyes—
failover_trail—no—
idstringyestc_-prefixed: the bigserial with a tc_ prefix prepended
input—no—
output—no—
routing—no—
server_idstringyes—
session_idstringyes—
status"running" | "success" | "error"yes—
tool_namestringyes—
Example request
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"
     }'
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"
}
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()
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();
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"
  }
});
Example response 200
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"
}

On this page

Project scopingPOST /v1/sessionsRequest bodycurl exampleResponse -- 201 CreatedSDK equivalentGET /v1/sessionsQuery parameterscurl exampleResponse -- 200 OKGET /v1/sessions/:idcurl exampleResponse -- 200 OKDELETE /v1/sessions/:idcurl exampleResponse -- 200 OKSDK equivalentPOST /v1/sessions/:id/executeRequest bodycurl exampleResponse -- 200 OKSDK equivalentPOST /v1/sessions/:id/sendRequest bodycurl exampleResponse -- 200 OKStreaming variantSDK equivalentGET /v1/sessions/:id/connectionscurl exampleResponse -- 200 OKSDK equivalentPOST /v1/sessions/:id/tool-callsRequest bodycurl exampleResponse -- 201 CreatedGET /v1/sessions/:id/tool-callsQuery parameterscurl exampleResponse -- 200 OKPATCH /v1/sessions/:id/tool-calls/:tc_idRequest bodycurl exampleResponse -- 200 OKGET /v1/healthcurl exampleResponse -- 200 OKGET /healthcurl exampleResponse -- 200 OKAsync settlementCorrelation chainWhere the idempotency key lands, per providerPolling or streamingStreaming statusEvent shapeHeartbeatsCallbacks and cancellationGET /v1/tool-calls/:id/payment-statuscurl exampleResponse -- 200 OK (settled)Response -- 200 OK (pending)SDK equivalentGET /v1/tool-calls/:id/payment-status/streamcurl exampleStream outputSDK equivalentGET /v1/tool-calls/:id/verification-statuscurl exampleResponse -- 200 OKSDK equivalentGET /v1/tool-calls/:id/verification-status/streamcurl exampleSDK equivalentGET /v1/meta-tools/statsQuery parametersRequestResponse -- 200 OKGET /v1/meta-tools/stats/hourlyQuery parametersRequestResponse -- 200 OKPOST /v1/connections/hmac-validateRequest bodycurl exampleResponse -- 200 OKResponse -- 200 OK (invalid)Error responsesRate limit headersNext stepsEvery operation, from the specGET /v1/sessionsPOST /v1/sessionsGET /v1/sessions/{id}DELETE /v1/sessions/{id}GET /v1/sessions/{id}/connectionsPOST /v1/sessions/{id}/executeGET /v1/sessions/{id}/mocksPOST /v1/sessions/{id}/proxy_executePOST /v1/sessions/{id}/sendGET /v1/sessions/{id}/tool-callsPOST /v1/sessions/{id}/tool-callsPATCH /v1/sessions/{id}/tool-calls/{tc_id}
Sessions | CodeSpar