Skip to main content
Browse the documentation

Using the API

Pagination and streaming

Walk long lists with opaque cursors, and follow runs, sessions and tasks live over Server-Sent Events.

Two things grow without bound on a platform where agents work all day: lists of things, and the events of work in progress. Lists come back a page at a time behind a cursor. Work in progress is streamed as Server-Sent Events (SSE), which you can resume after a dropped connection without missing anything.

Cursor pagination

Most lists take limit and cursor and answer in this shape:

{
  "items": [ ],
  "cursor": "<opaque>",
  "has_more": true
}

To read the next page, send the cursor you were given:

curl "https://api.snaga.ai/api/v1/runs?limit=50&cursor=<cursor>" \
  -H "Authorization: Bearer $SNAGA_API_KEY"

Rules worth knowing:

  • The cursor is opaque. Do not build or edit one; pass back exactly what you received. It cannot be used to jump to the far end of a list. A cursor the API did not issue is refused with 422, code: INVALID_CURSOR.
  • has_more decides when to stop. On the last page it is false and cursor is null.
  • limit is clamped, not refused. Each list has its own default and maximum — for example agents default to 20 and sessions to 50, both capped at 100. A value out of range is pulled into range.
  • Order is part of the contract. Runs come newest first. If you want the oldest, ask for order=asc instead of paging to the end.

A few lists are small enough to return whole, with a total and no cursor — GET /api/v1/integrations is one. Each operation in the API reference says which form it uses.

The SDKs walk cursors for you; see Streaming and pagination.

Server-Sent Events

Event streams are long-lived GET requests answered as text/event-stream. Each event has an id, an event name and JSON data. The main streams:

StreamWhat it carries
GET /api/v1/runs/{runId}/eventsOne run: lifecycle, model output, tool calls.
GET /api/v1/sessions/{sessionId}/eventsEvery run in one conversation.
GET /api/v1/notifications/streamYour notifications.
GET /api/v1/missions/{missionId}/eventsOne mission.
GET /api/v1/initiatives/{initiativeId}/eventsOne initiative.
GET /api/v1/a2a/tasks/{taskId}/eventsOne A2A task's status.

Following a run

curl -N https://api.snaga.ai/api/v1/runs/<run_id>/events \
  -H "Authorization: Bearer $SNAGA_API_KEY"

Every run event arrives under its own name — event: llm.chunk, event: tool.call and so on — and its data carries stream_type (lifecycle, assistant, tool or other) so you can filter. The stream ends when the run reaches a final status:

  • an event: done frame with the run's status, output, metrics and cost_usd — close on this, not on run.completed;
  • then a bare data: [DONE] line for readers written for OpenAI-style streams.

Two other frames are worth handling. gap means the server dropped some non-essential events (chiefly llm.chunk) under backpressure; tool, approval, input and document events are never dropped. error means the buffer saturated and the server closed the stream — reconnect.

Resuming after a drop

Send the id of the last event you saw as Last-Event-ID; the server replays everything strictly after it. A browser's EventSource does this for you on reconnect.

On the session stream, event ids look like <runId>:<seq>. New event names can appear over time; ignore the ones you do not know.

Streams in the browser

EventSource cannot send an Authorization header. Rather than put a full API key in a URL, mint a short-lived token:

curl -X POST https://api.snaga.ai/api/v1/auth/sse-tokens \
  -H "Authorization: Bearer $SNAGA_API_KEY"

The answer has a token and its expires_at — it lives 60 seconds. Pass it as ?token=:

const source = new EventSource(
  `https://api.snaga.ai/api/v1/runs/${runId}/events?token=${token}`,
);
source.addEventListener("done", () => source.close());

The token carries only the read scopes you already hold for streams (sessions:read, runs:read, agents:read, notifications:read) and works only for GET on event streams. Any other request presenting it is refused. Because it is short-lived, mint a fresh one each time you open a stream.