Docs/Core concepts

Streaming

Text streams for chat and NDJSON events for music.

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}}'

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.

EventFieldsMeaning
heartbeat—Connection is alive. Ignore it.
stagemessageHuman-readable progress update.
completetrack, usageRemainingThe track is ready. Last event.
errormessageGeneration failed and credits were refunded. Last event.
JSON
{"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}
TypeScript
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.

Was this page helpful?

Questions? Contact support

Last updated October 20, 2026