SSE streaming
How paymentStatusStream and verificationStatusStream push state changes — event shape, heartbeats, cancellation, TypeScript and Python usage, plus a curl example.
SSE streaming
@codespar/sdkv0.9.0Two SDK methods push async settlement state over Server-Sent Events:
session.paymentStatusStream(toolCallId, opts)— forcodespar_charge/codespar_pay.session.verificationStatusStream(toolCallId, opts)— forcodespar_kyc.
Both share the same wire shape: a single HTTP GET to /v1/tool-calls/:id/<status>/stream that opens a long-lived response with Content-Type: text/event-stream. The server pushes named events as the underlying state changes; the SDK wrapper parses them, fires callbacks, and resolves the promise on terminal.
For the correlation chain that produces these events, see Async settlement.
Event shape
Three named event types flow over the stream:
| Event name | When it fires | Payload |
|---|---|---|
snapshot | Once, immediately on connect. | The current state of the tool call — { status, ... }. Often pending. |
update | Every time the state changes after connect. | The new state envelope. May fire multiple times (e.g. pending → review → approved). |
done | Once, 5 seconds after the state goes terminal. The connection auto-closes. | The final envelope (same as the last update). |
snapshot is guaranteed first. done is guaranteed last. Between them, zero or more update frames.
A typical SSE frame on the wire:
event: update
data: {"status":"pending","tool_call_id":"tc_abc123","external_reference":"5f4...","observed_at":"2026-05-04T14:30:00Z"}Heartbeats
Every 15 seconds, the server emits a comment frame:
: heartbeat 1714838400000These keep the connection alive through proxies that idle-close silent streams. They are not events — clients that walk lines looking for event: prefixes will skip them naturally. The SDK wrappers (TypeScript and Python) filter them explicitly so the onUpdate callback is never invoked with a heartbeat.
TypeScript usage
The simplest form. Resolves with the final envelope when the state goes terminal.
import { CodeSpar } from "@codespar/sdk";
const cs = new CodeSpar({ apiKey: process.env.CODESPAR_API_KEY });
const session = await cs.create("user_123", { servers: ["asaas"] });
const charge = await session.charge({
method: "pix",
amount: 9990,
currency: "BRL",
});
const final = await session.paymentStatusStream(charge.tool_call_id);
console.log(final.status); // "succeeded"
console.log(final.final_amount_minor); // 9990Get every state change as it lands. The promise still resolves on terminal.
const final = await session.paymentStatusStream(charge.tool_call_id, {
onUpdate: (env) => {
console.log(`[${env.observed_at}] ${env.status}`);
},
});
console.log("terminal:", final.status);Pass an AbortSignal and call .abort() to drop the stream cleanly.
const ac = new AbortController();
setTimeout(() => ac.abort(), 60_000); // give up after 60s
try {
const final = await session.paymentStatusStream(charge.tool_call_id, {
signal: ac.signal,
});
console.log(final.status);
} catch (err) {
if (err.name === "AbortError") {
// user canceled or timed out — status not yet terminal
} else {
throw err;
}
}Python usage
from codespar import AsyncCodespar
async with AsyncCodespar(api_key=...) as cs:
session = await cs.create("user_123", servers=["asaas"])
charge = await session.charge(
method="pix", amount=9990, currency="BRL",
)
def on_update(env):
print(env["observed_at"], env["status"])
final = await session.payment_status_stream(
charge["tool_call_id"],
on_update=on_update,
)
print(final["status"])A synchronous wrapper exists on Session for non-async callers — session.payment_status_stream(...) blocks the calling thread until terminal.
Wrap the stream call in an asyncio.Task and cancel it.
import asyncio
task = asyncio.create_task(
session.payment_status_stream(charge["tool_call_id"]),
)
try:
final = await asyncio.wait_for(task, timeout=60)
except asyncio.TimeoutError:
task.cancel()
# status not yet terminalWhen to poll instead
The polling sibling — session.paymentStatus(toolCallId) — is the right pick for one-off "is this done yet?" reads, for batch reconciliation jobs that walk many tool_call_ids on a schedule, and for environments where opening a long-lived stream is awkward (some serverless platforms, some CI runners).
const status = await session.paymentStatus(charge.tool_call_id);
if (status.status === "pending") {
// try again later
}Polling and streaming hit the same backend tables, so they always agree. Streaming wins on latency to terminal; polling wins on operational simplicity.
Direct curl
For a quick smoke test of the stream without the SDK:
curl -N \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
https://api.codespar.dev/v1/tool-calls/tc_abc123/payment-status/streamThe -N flag disables curl's output buffering so frames print as they arrive. A typical session looks like:
event: snapshot
data: {"status":"pending","tool_call_id":"tc_abc123","observed_at":"2026-05-04T14:30:00Z"}
: heartbeat 1714838415000
event: update
data: {"status":"succeeded","tool_call_id":"tc_abc123","final_amount_minor":9990,"settled_at":"2026-05-04T14:30:18Z","observed_at":"2026-05-04T14:30:18Z"}
event: done
data: {"status":"succeeded","tool_call_id":"tc_abc123","final_amount_minor":9990,"settled_at":"2026-05-04T14:30:18Z","observed_at":"2026-05-04T14:30:23Z"}The blank line between frames is the SSE separator — every event ends with \n\n.
The verification stream uses the same shape with a different terminal-state vocabulary (approved, rejected, review, expired, pending). Swap payment-status for verification-status in the URL and use verificationStatusStream from the SDK.
Next steps
Last updated on