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.
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_keyProject scoping
Every /v1 endpoint accepts an optional x-codespar-project header that pins the request to a specific project 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. |
# 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
| Field | Type | Required | Description |
|---|---|---|---|
user_id | string | Yes | End-user identifier on behalf of whom the agent acts |
servers | string[] | Yes* | List of server identifiers (e.g., ["stripe", "mercado-pago"]) |
preset | string | No | Named preset: brazilian, mexican, argentinian, colombian, all |
manageConnections | object | No | Connection-management options (see Sessions) |
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
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
| 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
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
| 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
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
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | Natural-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 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
const connections = await session.connections();POST /v1/sessions/:id/tool-calls
Records a tool call result from an external execution. This is used in advanced workflows where the tool is executed outside of CodeSpar (e.g., by the LLM framework) and the result needs to be recorded for auditing and billing.
Auth required: Yes (scope: tools:execute)
Request body
| Field | Type | Required | Description |
|---|---|---|---|
server_id | string | Yes | Server the tool belongs to |
tool_name | string | Yes | Tool name that was called |
status | string | No | success or error |
duration_ms | number | No | How long the call took |
error_code | string | No | Provider error code, when it failed |
input | object | No | Arguments that were passed to the tool |
output | object | No | Result returned by the tool |
curl example
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
| 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
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
| 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
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/healthResponse -- 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:
tool_call_id: returned by the SDK orPOST /v1/sessions/:id/execute. Opaque. Use it as the path parameter on every status endpoint.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.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
Two SDK methods push settlement state over Server-Sent Events instead of polling:
session.paymentStatusStream(toolCallId, opts)forcodespar_charge/codespar_paysession.verificationStatusStream(toolCallId, opts)forcodespar_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 1713178215000Heartbeats 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 terminalA 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
| Parameter | Type | Required | Description |
|---|---|---|---|
meta_tool | string | No | Only rows for this meta-tool (e.g., codespar_charge) |
rail | string | No | Only rows for this rail (e.g., pix) |
Request
GET /v1/meta-tools/stats?meta_tool=codespar_charge&rail=pixResponse -- 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
| Parameter | Type | Required | Description |
|---|---|---|---|
provider_id | string | Yes | Provider identifier (e.g., asaas) |
canonical_tool | string | Yes | The provider's tool, as in /v1/meta-tools/stats (e.g., asaas:create_payment) |
Request
GET /v1/meta-tools/stats/hourly?provider_id=asaas&canonical_tool=asaas:create_paymentResponse -- 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
| 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
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
}| 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 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: 1713178260Next 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
https://api.codespar.dev/v1/sessionsList sessions
Newest first, keyed on (created_at, id) so a tie does not drop a page. next_before is the cursor for the following call and is null on the last page. agent_id lists the sessions created as that agent; a handle this organization does not have answers an empty page.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
agent_id | string | no | Only sessions created with this agent (its handle). A session created without an agent is never listed under one. |
before | string | no | — |
limit | integer | no | — |
status | "active" | "closed" | "error" | no | — |
user_id | string | no | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | Bad Request — the body or query did not match the schema. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
next_before | string,null | yes | — |
sessions | array of object | yes | — |
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_KEYimport 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");{
"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
https://api.codespar.dev/v1/sessionsOpen a session
A session is the unit an agent's tool calls, policy evaluations and audit entries hang off. servers names the connected MCP servers it may reach (0–20), and every id must be one GET /v1/servers returns for this project — an id that is not gets 400 unknown_servers with the offenders listed, which is a different body from the schema invalid_body above it.
mocks and chaos are accepted only on a project whose environment is test: a live-environment project gets 403 mocks_not_permitted, a payload over 64 KiB gets 413, and a malformed one gets 400 with RFC 6901 pointers pointing at the offending field.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
agent_id | string | no | The registered agent this session acts as (its handle). Approvals raised in the session are attributed to it. Must name an active agent of this org, or the request answers 422. |
chaos | object | no | — |
mocks | object | no | — |
servers | array of string | yes | — |
user_id | string | no | — |
Responses
| Status | Body | Description |
|---|---|---|
201 | Session | 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 | — |
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.
{
"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}
https://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
| 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 | — |
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_KEYimport 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"
}
});{
"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}
https://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
| 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 | — |
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_KEYimport 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"
}
});{
"id": "obj_0000000000000000",
"status": "closed",
"closed_at": "2026-01-15T12:00:00.000Z"
}GET /v1/sessions/{id}/connections
https://api.codespar.dev/v1/sessions/{id}/connectionsList a session's servers and the tools it can call
What this session may dispatch to. servers is the ids the session was opened on, enriched from the catalog; tools is the CodeSpar meta-tool set, which is catalog-wide rather than per-server, so every entry reports server: "codespar" — the routing layer, not a backend.
Four of the six server fields are FALLBACKS, not facts, when the id is not in the catalog: name falls back to the id itself, category to "unknown", country to "BR", and auth_type to "api_key". A BR/api_key pair can therefore mean either "this is a Brazilian API-key server" or "nothing is known about this id", and the two are not distinguishable from this response.
connected is false on every entry today. It is a placeholder, not an auth-state read: do not treat false as evidence that a credential is missing.
A session in another project is 404, the same answer as one that never existed.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
404 | object | Not Found |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
servers | array of object | yes | — |
tools | array of object | yes | — |
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_KEYimport 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"
}
});{
"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
https://api.codespar.dev/v1/sessions/{id}/executeResolves `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:
idempotency_keyis 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.- The create answers
status: "PROCESSING"withpayable: falseand NO document. The instrument registers with the clearing house first. Subscribe tocommerce.charge.createdinstead of polling for a barcode that is not late, only not made yet. commerce.charge.paidis the event that means the funds arrived, by either leg. A boleto payment can be preceded bycommerce.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 onpaid.
An agreement in N instalments is N cobranças, one per parcela, each with its own due_date and its own idempotency_key. There is no single instalment charge.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
estimatedCost | number | no | — |
input | object | no | — |
params | object | no | — |
tool | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | object | OK. The full envelope, or the short unregistered-tool envelope that carries no tool_call_id and no called_at. |
400 | object | Bad Request — the body or query did not match the schema. |
403 | object | object | Forbidden — either a policy rule refused the call, or the org's monthly tool-call allowance is spent. The two bodies are disjoint: the policy one is { reason, ruleType, ruleId } and carries NO error key (plus approval_id and expires_at when the refusal opened a pending approval); the quota one is { error: "quota_exceeded", ... }. The quota refusal is NOT produced by default: tool calls are free under the rate card, and the allowance only applies on a deployment that explicitly re-enables it. |
404 | object | Not Found |
409 | object | Conflict — the session is not active, so it dispatches nothing. |
422 | object | Unprocessable — the session declares mocks and this tool has none left, or none at all. tool_name is present only on tool_not_mocked. Reached from the catalog branch; a meta-tool never answers 422. |
503 | object | object | Service Unavailable — the route's policy guard could not answer ({ error: "policy_engine_error" }), or the mock engine failed ({ code: "mocks_engine_error", message }). Two different bodies at the same status. |
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);
}{
"success": true,
"error": "string",
"duration": 0,
"server": "string",
"tool": "string",
"tool_call_id": "tc_0000000000000000",
"called_at": "string"
}GET /v1/sessions/{id}/mocks
https://api.codespar.dev/v1/sessions/{id}/mocksRead a session's declared mocks and their consume counters
mocks is the session's declared mock document verbatim, or null when the session declared none. counters is keyed by canonical tool name: n is how many times that tool has been consumed, cap is how many the declaration allows.
cap is DERIVED, not stored: an array entry caps at its length; an object entry is uncapped and reports null. A tool with a counter row but no declared entry also reports null, so cap: null means "uncapped or undeclared", never "zero remaining".
Reading this is not gated by environment. DECLARING mocks is: opening a session with a mocks field on a live-environment project is refused there, not here. When n reaches cap, the next dispatch of that tool fails with 422 mocks_exhausted on POST /v1/sessions/{id}/execute rather than falling through to a real provider.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
404 | object | Not Found |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
counters | object | yes | — |
mocks | object,null | yes | — |
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_KEYimport 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"
}
});{
"mocks": {},
"counters": {}
}POST /v1/sessions/{id}/proxy_execute
https://api.codespar.dev/v1/sessions/{id}/proxy_execute`method` + `endpoint` are forwarded to the server's upstream with credentials injected server-side
Make one raw HTTP call to a connected server
The escape hatch under the meta-tools: method + endpoint are forwarded to the server's upstream with credentials injected server-side. status is the UPSTREAM's status code, so a 200 here can carry a 404 from the provider; only a transport failure becomes 502.
server and endpoint reject : because the policy engine canonicalises a tool name as <server>:<method>:<endpoint>, and a colon in either field would let a caller forge extra delimiters. That is a 400, not a 403: bad input, not a denied decision.
MOCKED RESPONSES ANNOUNCE THEMSELVES. While a deployment answers from the mock lane, headers carries x-codespar-mock: "true" and data carries mocked: true sealed into the object itself. The second marker is the one that matters: it is also what the durable record and the audit chain store, so a mocked call cannot later be read as a real one.
413 means the policy engine refused the tool input for size. The cap is JSON.stringify(input).length > 1_000_000 — a count of UTF-16 code units of the serialized JSON, which is neither a byte count nor 1 MiB (1 048 576), and a payload of non-ASCII text hits it at fewer bytes than an ASCII one. The check runs only under a matched approval-required rule; with no such rule in play, size is not measured at all.
424 appears only where the deployment is configured to require a resolved connection. Its reason is a closed set of three, which is every non-ok outcome the credential resolver can return.
429 always carries retry_after_ms, and the same figure rounded up to whole seconds in the Retry-After header.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
body | — | no | — |
endpoint | string | yes | — |
estimatedCost | number | no | — |
headers | object | no | — |
method | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | yes | — |
params | object | no | — |
server | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | object | Bad Request — the body did not match the schema, or server is not one of the ids this session was opened on. The second body names the connected servers and points at the pre-connected sandbox meta-tool instead. |
403 | object | object | Forbidden — either a policy rule refused the call, or the org's monthly tool-call allowance is spent. The two bodies are disjoint: the policy one is { reason, ruleType, ruleId } and carries NO error key (plus approval_id and expires_at when the refusal opened a pending approval); the quota one is { error: "quota_exceeded", ... }. The quota refusal is NOT produced by default: tool calls are free under the rate card, and the allowance only applies on a deployment that explicitly re-enables it. |
404 | object | Not Found |
409 | object | Conflict — the session is not active, so it dispatches nothing. |
413 | object | Payload Too Large — the serialized tool input exceeded the policy engine's cap under an approval-required rule. Not a policy denial: 403 and 413 are kept apart so a client can tell "refused" from "too big". |
422 | object | Unprocessable — the project is in environment="test" and this server cannot be exercised there. reason is the provider's test_venue classification: none means the provider runs no test environment at all, so the call would land on production with a real effect (a Z-API WhatsApp message goes to a real number); unclassified means nobody has established what it does in test. Answered whether or not PROXY_REQUIRE_CONNECTION is set, because it is about the environment and not about a missing connection, and not retryable — the same call from a live project reaches the provider (ent#722). |
424 | object | object | Failed Dependency — no usable credential for this server. reason is the resolver's own outcome. Or shared_sandbox_live_project_refused: the project's connection for this server is the platform's shared SANDBOX credential, which serves test projects only, and this is a live project or a live-mode call; reason is that environment. Answered whether or not PROXY_REQUIRE_CONNECTION is set, nothing is sent upstream, and the fix is to connect the project's own credential (p2-proxy#5). |
429 | object | Too Many Requests — the per-(org, server) bucket is empty. See the Retry-After header. |
502 | object | Bad Gateway — the upstream call itself failed (transport, not status). The attempt is still logged and chained, and proxy_call_id names the row. |
503 | object | Service Unavailable — the policy engine could not answer. Fail-closed: the call did not run. Distinct from a 403, which is a decision that was taken. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
data | — | no | — |
duration | number | yes | — |
headers | object | yes | — |
proxy_call_id | string | yes | px_-prefixed id of the logged call. |
status | integer | yes | The UPSTREAM status code, not this call's. |
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);
}{
"status": 0,
"headers": {},
"duration": 0,
"proxy_call_id": "proxycall_0000000000000000"
}POST /v1/sessions/{id}/send
https://api.codespar.dev/v1/sessions/{id}/senda payment that settled on turn one is still visible when turn two died
Drive a model loop that may call tools
Sends one user message into a tool-use loop over the CodeSpar meta-tools, up to ten model turns. Content-negotiated: Accept: text/event-stream streams the turns as they happen and this JSON body is never sent; anything else gets the whole transcript at the end. The schema below describes the JSON form.
WHAT tool_calls IS. Every entry is a call the loop attempted and logged, in order. It is NOT a list of provider calls: a refusal never reached a provider and still appears here. server_id is what separates them — a refusal by deny-list, policy or approval is logged under agentgate, a strategy error under unknown, a consumed mock under mock, and a real dispatch under the server's own id. Read server_id before treating an entry as money moved.
aborted: true means the client hung up and the loop stopped rather than buying another turn; the transcript is short because it was cut, not because the model finished.
A 500 is NOT a rollback. The loop broke part-way and everything that already ran stands: the same tool_calls and iterations fields ride on the error body, so a payment that settled on turn one is still visible when turn two died.
This route carries no policy guard of its own, so it has no policy 403 and no policy 503; the meta-tool refusals above are how a denial surfaces here. Its only 403 is the quota, which is off by default.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
message | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | Bad Request — the body or query did not match the schema. |
403 | object | Forbidden — the org's monthly tool-call allowance is spent. NOT produced by default: tool calls are free under the rate card, and the allowance only applies on a deployment that explicitly re-enables it. |
404 | object | Not Found |
409 | object | Conflict — the session is not active, so it dispatches nothing. |
500 | object | Internal Server Error — the loop failed mid-flight. Carries the work that already completed, in the same fields as the 200. |
503 | object | Service Unavailable — this deployment has no model credential configured. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
aborted | true | no | — |
iterations | integer | yes | — |
message | string | yes | The model's last text turn. Empty when it ended on a tool call. |
tool_calls | array of object | yes | — |
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);
}{
"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
https://api.codespar.dev/v1/sessions/{id}/tool-callsList one session's tool calls
Newest first, capped by limit and NOT paginated: there is no cursor here, so a session with more calls than limit (max 500) cannot be walked past the first page from this route. GET /v1/tool-calls is the paginated one, across the whole project.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit | integer | no | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | Bad Request — the body or query did not match the schema. |
404 | object | Not Found |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
tool_calls | array of ToolCall | yes | — |
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_KEYimport 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"
}
});{
"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
https://api.codespar.dev/v1/sessions/{id}/tool-callsRecord a tool call the client executed
This RECORDS a call; it does not execute one. Two ways to use it, and the difference is status. One shot: post a terminal success/error with duration_ms and output. Two step: post running with input as the call begins, then PATCH the outcome — which is what the SDK does and why running is the default.
The audit chain is appended exactly once per call, on the transition INTO a terminal status: the one-shot POST appends it here, the two-step path appends it on the PATCH. server_id: "mock" appends nothing (the mock store emits its own events). Chain entries written from this route are marked client_sdk — self-reported, distinct from server-observed execution.
409 session_not_active when the session is closed. The returned id carries the tc_ prefix and is what the PATCH path takes.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
duration_ms | integer | no | — |
error_code | string | no | — |
input | — | no | — |
output | — | no | — |
server_id | string | yes | — |
status | "running" | "success" | "error" | no | — |
tool_name | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
201 | ToolCall | 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 | — |
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"
}
});{
"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}
https://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
| 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 | 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 | — |
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"
}
});{
"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"
}Bank consents (legacy /ofb path)
5 operations under /v1/ofb/consents (POST GET): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
Connections
11 operations under /v1/connections (GET POST DELETE PATCH PUT): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.