← Blog

How to Parse Claude Streaming Responses (SSE Guide)

2026-10-26 · 4 min read · SubToAPI Team

Parsing Claude's streaming responses means reading a sequence of Server-Sent Events (SSE), identifying each event type, and accumulating the delta fragments into a complete message as they arrive. The stream isn't a single JSON blob — it's a series of named events (message_start, content_block_delta, message_stop, etc.) that you need to handle individually, in order, without assuming the full response is available upfront.

This trips people up because streaming responses look nothing like a normal API call. If you try to JSON.parse() the raw body, it fails. If you only grab the last chunk, you miss most of the content. Below is exactly how the event stream is structured and how to parse it correctly, with working code.

The event types you'll see

A Claude streaming response emits these events in order:

Each event arrives as a line prefixed with event: followed by a data: line containing JSON. A single HTTP response can contain dozens or hundreds of these, separated by blank lines.

Parsing SSE manually

If you're reading the raw stream yourself (not using an SDK), you need to buffer incoming bytes and split on double newlines, since chunks from the network don't align with event boundaries:

async function parseClaudeStream(response) {
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  let fullText = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const events = buffer.split("\n\n");
    buffer = events.pop(); // keep incomplete trailing event

    for (const raw of events) {
      const lines = raw.split("\n");
      const dataLine = lines.find((l) => l.startsWith("data:"));
      if (!dataLine) continue;

      const payload = JSON.parse(dataLine.slice(5).trim());

      if (payload.type === "content_block_delta") {
        if (payload.delta.type === "text_delta") {
          fullText += payload.delta.text;
        }
      }

      if (payload.type === "message_stop") {
        return fullText;
      }
    }
  }
  return fullText;
}

The key detail: never assume one network chunk equals one event. TCP packets and HTTP chunking don't respect SSE boundaries, so you must buffer and split on \n\n yourself, keeping any incomplete trailing fragment for the next read.

Handling tool use in streams

If Claude is calling a tool, the content block type is tool_use, and the JSON input arrives incrementally as input_json_delta fragments instead of plain text:

if (payload.type === "content_block_delta") {
  if (payload.delta.type === "input_json_delta") {
    toolInputBuffer += payload.delta.partial_json;
  }
}

if (payload.type === "content_block_stop") {
  const toolInput = JSON.parse(toolInputBuffer); // only valid once complete
}

A common bug: trying to JSON.parse() partial_json on every delta. It's a fragment, not valid JSON, until the block is fully closed. Only parse it once content_block_stop fires for that block. For a full walkthrough of tool calls, see /docs/tools.

Tracking usage and stop reason

Token usage and stop_reason don't arrive in the final text chunk — they're split across message_start (input tokens) and message_delta (output tokens, stop reason):

let usage = {};

if (payload.type === "message_start") {
  usage.input_tokens = payload.message.usage.input_tokens;
}

if (payload.type === "message_delta") {
  usage.output_tokens = payload.usage.output_tokens;
  stopReason = payload.delta.stop_reason;
}

If you're billing users per token or logging usage for analytics, you need both events — pulling only from message_delta gives you incomplete totals.

Using an SDK instead of parsing manually

Writing your own SSE parser is a reasonable learning exercise, but in production it's easy to introduce subtle bugs around buffering, reconnects, and partial JSON. Most teams end up using a client library that exposes a simple async iterator or event emitter instead of raw bytes.

SubToAPI exposes the same event-based streaming format over a stable HTTPS endpoint, so your parsing code doesn't change whether you call Claude directly or through a sub_live_... application key:

curl https://api.subtoapi.app/v1/messages \
  -H "Authorization: Bearer $SUBTOAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-4",
    "max_tokens": 1024,
    "stream": true,
    "messages": [{"role": "user", "content": "Write a haiku about parsing"}]
  }'

The response is the same SSE event sequence described above, so the parsing logic in this article applies unchanged. See /docs/streaming for event-by-event examples and /docs/messages for the full request schema.

Common parsing mistakes

If you want to skip building this parser yourself and just get a working integration, /docs/quickstart walks through a minimal streaming client in under ten minutes, and you can start with a free trial at /signup.

Questions

Do I need a special library to parse Claude's SSE stream? No — any HTTP client that exposes a readable byte stream works, as long as you buffer chunks and split on blank lines to extract complete events before parsing the JSON payload.

Why does my JSON.parse() fail partway through a tool call? Because input_json_delta sends fragments of JSON, not complete objects. Accumulate the partial_json string across deltas and only parse it after content_block_stop for that block.

Where do I get final token usage in a streamed response? Input tokens are in message_start, output tokens and stop_reason are in message_delta. You need both events to compute total usage.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →