Skip to main content

Tool Calls

7 operations under /v1/tool-calls (GET): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.

14 min read
View MarkdownEdit on GitHub

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

Every operation below requires a Bearer token. See Authentication.

GET /v1/tool-calls

GEThttps://api.codespar.dev/v1/tool-calls

List tool calls across the project's sessions

Two response shapes on one operation: count_only=true returns { total } and nothing else, so a counter does not have to page through every row Otherwise the page plus a composite next_before cursor of the form <iso>:<id>; a bare ISO timestamp is still accepted for older callers but pages unstably when timestamps collide.

Query parameters

NameTypeRequiredDescription
beforestringno—
count_only"true" | "false"no—
limitintegerno—
sincestring (date-time)no—

Responses

StatusBodyDescription
200object | objectOK
400objectBad Request — the body or query did not match the schema.
Example request
curl -X GET https://api.codespar.dev/v1/tool-calls \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/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/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/tool-calls", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/tool-calls");
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"
    }
  ],
  "next_before": "string"
}

GET /v1/tool-calls/stats

GEThttps://api.codespar.dev/v1/tool-calls/stats

Tool-call figures per server

For the calling project: calls, errors and p95 latency per server over the last 24 hours (default) or 7 days, ending at the time of the read, and each server's last call with no window. Every server the project has ever called is listed, so a server with no call in the window comes back with calls: 0, p95_ms: null and its last_called_at. Same key scope as the list (sessions:read).

Query parameters

NameTypeRequiredDescription
window"24h" | "7d"no—

Responses

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

Response 200

FieldTypeRequiredDescription
serversarray of objectyesEvery server this project has ever called, most recently called first.
window"24h" | "7d"yes—
window_endstring (date-time)yesThe time of the read.
window_startstring (date-time)yes—
Example request
curl -X GET https://api.codespar.dev/v1/tool-calls/stats \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/tool-calls/stats HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/tool-calls/stats",
    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/tool-calls/stats", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/tool-calls/stats");
Example response 200
application/json
{
  "window": "24h",
  "window_start": "2026-01-15T12:00:00.000Z",
  "window_end": "2026-01-15T12:00:00.000Z",
  "servers": [
    {
      "server_id": "srv_0000000000000000",
      "calls": 0,
      "errors": 0,
      "p95_ms": 0,
      "last_called_at": "2026-01-15T12:00:00.000Z"
    }
  ]
}

GET /v1/tool-calls/{id}

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

Read one tool call

The same row the list returns, by id. {id} is the WIRE id (tc_<n>) the list emits, not the numeric column — a bare number is accepted too. Anything else is 404 rather than a database error.

Responses

StatusBodyDescription
200ToolCallOK
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 GET https://api.codespar.dev/v1/tool-calls/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/tool-calls/{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/tool-calls/{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/tool-calls/{id}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/tool-calls/{id}");
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"
}

GET /v1/tool-calls/{id}/payment-status

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

Where a tool call's payment got to

The settlement state of the payment a tool call started, computed from the normalized provider events correlated to it. One shape for every payment rail: you do not branch on the provider.

HOW THE CORRELATION WORKS, because it determines what you can expect back. The meta-tool router stamps one idempotency key per call and hands it to the provider — as a body field on some rails, as a header on others. When the provider later fires its webhook, the normalized event carries that key as external_reference, and this endpoint finds the events by matching on it. A tool call that never went through the router carries no key, so nothing can ever be correlated to it; that case is unknown rather than a pending that would have the agent poll forever.

THE STATUS IS THE HIGHEST-PRIORITY EVENT, NOT THE NEWEST. The order is refunded > failed > succeeded > updated > pending, and the winner is taken across the whole window regardless of when each event arrived. A refund that landed before a late-arriving success still reads refunded. That is deliberate — the worst outcome in the window is the one an agent must act on — and it means this field is NOT a replay of the provider's latest webhook.

updated is the rail telling you something changed without saying what. Resolve it against the provider's own API; this endpoint cannot.

WHAT pending AND unknown EACH MEAN, because they are not degrees of the same thing. pending means the call is correlatable and no settlement event has arrived — keep polling. unknown means the call carries no idempotency key, so no event will EVER correlate to it; polling is pointless. unknown is the only status that forces idempotency_key to null and events to empty, and it is never produced any other way.

The events array is the twenty most recent matching events, newest first. Twenty is a hard cap, not a page: there is no cursor, and a call with more than twenty events simply cannot show you the older ones from here.

Scoped to the credential's org AND project, with no way to widen it. A tool call that exists under another org, or under another project of the same org, is answered 404 exactly like one that never existed: existence is not probeable across the boundary.

Scope: sessions:read. This is a read of a session's own tool call, not of a wallet or a payment resource, and it needs no payment scope.

Path parameters

NameTypeRequiredDescription
idstringyesThe tool call, as tc_<digits> or as the bare digits. Both are accepted and the response always echoes the prefixed spelling. Anything else — a UUID, a negative number, an empty segment — is refused 400 before any lookup, so a malformed id and an id belonging to another tenant are answered differently.

Responses

StatusBodyDescription
200objectOK
400objectBad Request. The id is neither tc_<digits> nor bare digits. BARE body — there is no error.code to read and no request_id.
404objectNot Found. No tool call with this id is visible to this credential. BARE body.

Response 200

FieldTypeRequiredDescription
eventsarray of objectyes—
idempotency_keystring,nullyesThe key the router stamped on the call and the provider echoed back. Null exactly when payment_status is unknown.
original_statusstringyesThe tool call's OWN status — whether the call itself ran — which is a different question from whether the money settled. A call can be succeeded here and failed in payment_status.
payment_status"pending" | "succeeded" | "failed" | "refunded" | "updated" | "unknown"yes—
tool_call_idstringyesAlways tc_-prefixed, whichever spelling you sent.
Example request
curl -X GET https://api.codespar.dev/v1/tool-calls/{id}/payment-status \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/tool-calls/{id}/payment-status HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/tool-calls/{id}/payment-status",
    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/tool-calls/{id}/payment-status", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/tool-calls/{id}/payment-status", {
  path: {
    id: "tc_0000000000000000"
  }
});
Example response 200
application/json
{
  "tool_call_id": "tc_0000000000000000",
  "payment_status": "pending",
  "idempotency_key": "string",
  "original_status": "string",
  "events": [
    {
      "event_type": "string",
      "received_at": "2026-01-15T12:00:00.000Z",
      "provider": "string",
      "provider_action": "string",
      "payment_id": "payment_0000000000000000"
    }
  ]
}

GET /v1/tool-calls/{id}/payment-status/stream

GEThttps://api.codespar.dev/v1/tool-calls/{id}/payment-status/stream

The same payment status, pushed as it changes (SSE)

Server-Sent Events over the same data, same auth and same scoping as the poll above. Every frame's payload is byte-identical to that endpoint's 200 body, so one parser serves both.

THE FRAME GRAMMAR. event: snapshot arrives once, immediately, and carries the same object the non-streaming poll returns — so you never need a GET before opening the stream. event: update carries that same object again whenever a new event lands or the computed status changes. event: done carries { reason, ... } and is the last frame. Between frames the server writes SSE COMMENT lines (: heartbeat <epoch-ms>) every 15 seconds; every SSE parser ignores them, and they exist so a proxy or CDN does not close an idle connection.

The terminal set is succeeded, failed and refunded. pending, updated and unknown are NOT terminal, so a stream sitting on any of them stays open until you close it or the ceiling arrives.

A STREAM OPENED ON AN UNCORRELATABLE CALL NEVER SAYS ANYTHING AGAIN. When the call carries no idempotency key the snapshot reads unknown, the poll returns without querying, and — because unknown is not terminal — the connection is held for the full 30 minutes emitting only heartbeats. Read the snapshot's status and close the stream yourself; do not wait.

THE FIVE reason VALUES, which are the whole set. terminal — a terminal status was reached and the server closed 5 seconds later, deliberately, so a client that reconnects on close does not race the final frame. max_duration — the connection hit its 30-minute ceiling; reopen it, nothing is wrong. write_error — a heartbeat could not be written. client_disconnect and client_error — your side went away, so you will not see them.

A DATABASE HICCUP IS INVISIBLE TO YOU. The 1-second server-side poll swallows its own failures and tries again on the next tick: there is no error frame and the stream is not closed. Silence therefore means either nothing changed or the server could not tell — the two are indistinguishable from this endpoint, and the heartbeat proves only that the connection is alive.

THE REFUSALS ARE ORDINARY JSON, not SSE. Both the 400 and the 404 are decided before the stream headers are written, so a failing call answers application/json with a bare body and no frames at all. Do not attach an EventSource before checking the status.

COST, so you can size it. The server short-polls the events table once per second FOR EACH OPEN CONNECTION. Opening one stream per tool call across a large fleet is a query rate you should count before you build on it.

Scope: sessions:read, the same as the poll.

Path parameters

NameTypeRequiredDescription
idstringyesThe tool call, as tc_<digits> or as the bare digits. Both are accepted and the response always echoes the prefixed spelling. Anything else — a UUID, a negative number, an empty segment — is refused 400 before any lookup, so a malformed id and an id belonging to another tenant are answered differently.

Responses

StatusBodyDescription
200—The stream opened. snapshot first, then update per change, then done; heartbeat comments in between. The connection is closed by the server 5 seconds after a terminal status, or at 30 minutes, whichever comes first.
400objectBad Request. The id is neither tc_<digits> nor bare digits. BARE body — there is no error.code to read and no request_id.
404objectNot Found. No tool call with this id is visible to this credential. BARE body.
Example request
curl -X GET https://api.codespar.dev/v1/tool-calls/{id}/payment-status/stream \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/tool-calls/{id}/payment-status/stream HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/tool-calls/{id}/payment-status/stream",
    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/tool-calls/{id}/payment-status/stream", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/tool-calls/{id}/payment-status/stream", {
  path: {
    id: "tc_0000000000000000"
  }
});
// result is the raw Response: the body is a text/event-stream.

GET /v1/tool-calls/{id}/verification-status

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

Where a tool call's identity check got to

The KYC analogue of the payment poll: after a verification tool call, the buyer completes a hosted flow (or a provider scores them server-side) and the provider's webhooks arrive asynchronously. This reads the latest known state so an agent can wait on it.

HOW THE CORRELATION WORKS, because it determines what you can expect back. The meta-tool router stamps one idempotency key per call and hands it to the provider — as a body field on some rails, as a header on others. When the provider later fires its webhook, the normalized event carries that key as external_reference, and this endpoint finds the events by matching on it. A tool call that never went through the router carries no key, so nothing can ever be correlated to it; that case is unknown rather than a pending that would have the agent poll forever.

THE PRIORITY ORDER IS approved > rejected > review > expired > pending, taken across the window rather than by arrival time — the same rule as the payment poll, with one consequence worth stating: an approved that arrived before a later rejected still wins. If you need the provider's own chronology, read events, which is ordered.

review MEANS A HUMAN, NOT A DELAY. The provider's automated layer declined to decide and the case is waiting on manual disposition. It is not terminal and it has no deadline of ours; an agent that treats it as a slow pending will wait indefinitely.

expired is procedural, not a judgement: the buyer never finished the hosted flow inside the provider's own TTL. Start a new verification rather than re-polling this one.

pending versus unknown splits exactly as on the payment poll: pending is correlatable and waiting, unknown carries no key and will never resolve.

The events array is the twenty most recent matching events, newest first. Twenty is a hard cap, not a page: there is no cursor, and a call with more than twenty events simply cannot show you the older ones from here.

Scoped to the credential's org AND project, with no way to widen it. A tool call that exists under another org, or under another project of the same org, is answered 404 exactly like one that never existed: existence is not probeable across the boundary.

Scope: sessions:read.

Path parameters

NameTypeRequiredDescription
idstringyesThe tool call, as tc_<digits> or as the bare digits. Both are accepted and the response always echoes the prefixed spelling. Anything else — a UUID, a negative number, an empty segment — is refused 400 before any lookup, so a malformed id and an id belonging to another tenant are answered differently.

Responses

StatusBodyDescription
200objectOK
400objectBad Request. The id is neither tc_<digits> nor bare digits. BARE body — there is no error.code to read and no request_id.
404objectNot Found. No tool call with this id is visible to this credential. BARE body.

Response 200

FieldTypeRequiredDescription
eventsarray of objectyes—
hosted_urlstring,nullyesThe buyer-facing verification link, read off the originating call's own output so you can re-show it while polling. Null on rails that score server-side and have no buyer-facing flow, and null when the output carried no such field — the two are not distinguishable here. Unlike idempotency_key, this field is present even on an unknown status.
idempotency_keystring,nullyesNull exactly when verification_status is unknown.
original_statusstringyesWhether the tool CALL ran, which is a different question from whether the buyer was verified.
tool_call_idstringyesAlways tc_-prefixed, whichever spelling you sent.
verification_status"pending" | "approved" | "rejected" | "expired" | "review" | "unknown"yes—
Example request
curl -X GET https://api.codespar.dev/v1/tool-calls/{id}/verification-status \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/tool-calls/{id}/verification-status HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/tool-calls/{id}/verification-status",
    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/tool-calls/{id}/verification-status", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/tool-calls/{id}/verification-status", {
  path: {
    id: "tc_0000000000000000"
  }
});
Example response 200
application/json
{
  "tool_call_id": "tc_0000000000000000",
  "verification_status": "pending",
  "idempotency_key": "string",
  "original_status": "string",
  "hosted_url": "https://example.com/hook",
  "events": [
    {
      "event_type": "string",
      "received_at": "2026-01-15T12:00:00.000Z",
      "provider": "string",
      "verification_id": "verification_0000000000000000"
    }
  ]
}

GET /v1/tool-calls/{id}/verification-status/stream

GEThttps://api.codespar.dev/v1/tool-calls/{id}/verification-status/stream

The same verification status, pushed as it changes (SSE)

Server-Sent Events over the verification poll's data, with the same auth, the same scoping and the same payload per frame.

THE FRAME GRAMMAR. event: snapshot arrives once, immediately, and carries the same object the non-streaming poll returns — so you never need a GET before opening the stream. event: update carries that same object again whenever a new event lands or the computed status changes. event: done carries { reason, ... } and is the last frame. Between frames the server writes SSE COMMENT lines (: heartbeat <epoch-ms>) every 15 seconds; every SSE parser ignores them, and they exist so a proxy or CDN does not close an idle connection.

THE TERMINAL SET IS approved, rejected AND expired. review IS NOT IN IT, and that is the difference from every other stream on this surface: a case sent to manual disposition holds the connection open, because the disposition can still change the answer. Since a manual review routinely outlasts the 30-minute ceiling, expect done with max_duration on those cases and reconnect. pending and unknown are likewise not terminal.

A STREAM OPENED ON AN UNCORRELATABLE CALL NEVER SAYS ANYTHING AGAIN — same as the payment stream: unknown in the snapshot, then heartbeats until the ceiling. Close it yourself.

THE FIVE reason VALUES, which are the whole set. terminal — a terminal status was reached and the server closed 5 seconds later, deliberately, so a client that reconnects on close does not race the final frame. max_duration — the connection hit its 30-minute ceiling; reopen it, nothing is wrong. write_error — a heartbeat could not be written. client_disconnect and client_error — your side went away, so you will not see them.

The done frame's second field is verification_status, not payment_status; everything else about the frame is identical.

A DATABASE HICCUP IS INVISIBLE TO YOU. The 1-second server-side poll swallows its own failures and tries again on the next tick: there is no error frame and the stream is not closed. Silence therefore means either nothing changed or the server could not tell — the two are indistinguishable from this endpoint, and the heartbeat proves only that the connection is alive.

THE REFUSALS ARE ORDINARY JSON, not SSE. Both the 400 and the 404 are decided before the stream headers are written, so a failing call answers application/json with a bare body and no frames at all. Do not attach an EventSource before checking the status.

Scope: sessions:read.

Path parameters

NameTypeRequiredDescription
idstringyesThe tool call, as tc_<digits> or as the bare digits. Both are accepted and the response always echoes the prefixed spelling. Anything else — a UUID, a negative number, an empty segment — is refused 400 before any lookup, so a malformed id and an id belonging to another tenant are answered differently.

Responses

StatusBodyDescription
200—The stream opened. snapshot, then update per change, then done, with heartbeat comments in between. Closed 5 seconds after approved, rejected or expired, or at 30 minutes.
400objectBad Request. The id is neither tc_<digits> nor bare digits. BARE body — there is no error.code to read and no request_id.
404objectNot Found. No tool call with this id is visible to this credential. BARE body.
Example request
curl -X GET https://api.codespar.dev/v1/tool-calls/{id}/verification-status/stream \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/tool-calls/{id}/verification-status/stream HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/tool-calls/{id}/verification-status/stream",
    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/tool-calls/{id}/verification-status/stream", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/tool-calls/{id}/verification-status/stream", {
  path: {
    id: "tc_0000000000000000"
  }
});
// result is the raw Response: the body is a text/event-stream.
Tool Calls | CodeSpar