Two endpoints stream their responses: chat streams text as it's generated, and music streams progress events while a track renders. Everything else returns a single JSON body.
Chat text streams#
POST /v1/chat responds with Content-Type: text/plain; charset=utf-8 and writes the reply into the body as it's generated — no framing, no event envelope. Concatenate the chunks to get the full text.
curl -N https://api.vaelor.chat/v1/chat \
-H "Authorization: Bearer $VAELOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Explain TCP slow start.", "attachments": [], "config": {"model": "medium", "modes": [], "uncensored": false}}'const response = await fetch('https://api.vaelor.chat/v1/chat', { method: 'POST', headers, body })
if (!response.ok) throw new Error((await response.json()).error)
let text = ''
for await (const chunk of response.body!.pipeThrough(new TextDecoderStream())) {
text += chunk
render(text)
}with requests.post(url, headers=headers, json=body, stream=True) as response:
response.raise_for_status()
for chunk in response.iter_content(chunk_size=None, decode_unicode=True):
print(chunk, end="", flush=True)Errors that happen before generation starts — bad input, insufficient credits — return a normal JSON error with a 4xx or 5xx status. Always check response.ok before reading the stream.
Cancelling#
Close the connection to stop generation. You're charged only for tokens produced before the disconnect; if nothing was generated, the reservation is refunded in full.
Music event streams#
POST /v1/music responds with Content-Type: application/x-ndjson: one JSON object per line. Rendering takes a few minutes, so the server sends a heartbeat every 10 seconds to keep proxies from closing the connection.
| Event | Fields | Meaning |
|---|---|---|
heartbeat | — | Connection is alive. Ignore it. |
stage | message | Human-readable progress update. |
complete | track, usageRemaining | The track is ready. Last event. |
error | message | Generation failed and credits were refunded. Last event. |
{"type":"stage","message":"Composing your track. This may take a few minutes…"}
{"type":"heartbeat"}
{"type":"heartbeat"}
{"type":"stage","message":"Saving your song…"}
{"type":"complete","track":{"id":"trk_8f2c…","status":"ready","title":"Harbor Lights"},"usageRemaining":412}const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader()
let buffer = ''
while (true) {
const { value, done } = await reader.read()
if (done) break
buffer += value
const lines = buffer.split('\n')
buffer = lines.pop()!
for (const line of lines.filter(Boolean)) {
const event = JSON.parse(line)
if (event.type === 'complete') return event.track
if (event.type === 'error') throw new Error(event.message)
}
}Lost the connection?
The track keeps rendering on our side. Fetch it later with Retrieve a track or find it in pending from List tracks.
OSINT runs#
Investigations can run for many minutes, so they don't stream. Start a run, then poll its events with the after cursor.