Skip to navigation

Streaming & Async Patterns

Real-time responses and background processing

Why streaming?

Some API calls return immediately (creating a project takes milliseconds). Others — like asking the AI a complex question — can take several seconds. Streaming lets you show results as they’re generated instead of waiting for the complete response.

Chat streaming

The Chat endpoint (POST /api/v2/chat) supports Server-Sent Events (SSE) for real-time streaming. Set stream: true in your request:

curl -X POST https://api.pyramid-ai.com/api/v2/chat \
-H "Authorization: Bearer pai_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"project_id": "...",
"messages": [{"role": "user", "content": "What are the safety requirements?"}],
"stream": true
}'

The response arrives as a stream of events:

data: {"type": "text", "content": "Based on"}
data: {"type": "text", "content": " the project"}
data: {"type": "text", "content": " safety plan,"}
data: {"type": "text", "content": " all workers"}
...
data: {"type": "done", "sources": [...], "usage": {...}}

Each event contains a small piece of the response. Your UI can display each piece as it arrives — creating a typing effect.

When to use streaming

Use caseStreaming?
Chatbot UI showing responses in real timeYes
Server-to-server integration logging full responsesNo (use stream: false)
Mobile app showing AI typing indicatorYes
Automated pipeline processing answersNo

Handling SSE in JavaScript

const response = await fetch('/api/v2/chat', {
method: 'POST',
headers: {
'Authorization': 'Bearer pai_live_YOUR_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
project_id: '...',
messages: [{ role: 'user', content: 'What are the safety requirements?' }],
stream: true,
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
// Parse SSE events and update UI
console.log(text);
}

Asynchronous operations

Some operations run in the background because they take too long for a synchronous response:

OperationPatternHow to check status
Document processingFire-and-forgetGET /documents/{id} — poll status
Batch queriesBackground jobGET /agent/batches/{id} — poll status
Compliance runsBackground jobGET /check/runs/{id} — poll status

Polling pattern

For background operations, poll the status endpoint until the job completes:

import time
import requests
def wait_for_completion(url, headers, max_wait=300):
"""Poll until status is terminal (completed/failed)."""
start = time.time()
while time.time() - start < max_wait:
response = requests.get(url, headers=headers)
data = response.json()["data"]
if data["status"] in ("completed", "failed", "partial"):
return data
time.sleep(5) # Check every 5 seconds
raise TimeoutError("Operation did not complete in time")
OperationPoll intervalTypical duration
Document processingEvery 5 seconds10–60 seconds
Batch queriesEvery 10 seconds1–30 minutes (depends on batch size)
Compliance runsEvery 10 seconds1–10 minutes (depends on requirement count)

Don’t poll more frequently than every 2 seconds. Excessive polling wastes your rate limit quota without meaningfully improving responsiveness.

Synchronous vs asynchronous at a glance

EndpointSync/AsyncResponse time
POST /projectsSyncInstant
POST /documents (reserve)SyncInstant
POST /documents/processAsync (triggers background job)Seconds to minutes
POST /agent/querySync3–15 seconds
POST /chatSync (streamable)3–30 seconds
POST /agent/batchesAsyncMinutes to hours
POST /check/verifySync5–30 seconds
POST /check/runs (create)SyncInstant
POST /check/runs/{id}/triggerAsyncMinutes