> ## Documentation Index
> Fetch the complete documentation index at: https://docs.connectly.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# NDJSON Streaming

> How to correctly parse the NDJSON streaming response from the Agent Graph invoke endpoint — and why response.json() fails 🤖

The Agent Graph `/invoke` endpoint returns a **stream of newline-delimited JSON objects** (NDJSON) rather than a single JSON response. Each line is a complete, self-contained JSON event. This lets your application begin processing the agent's reply progressively rather than waiting for the entire response to arrive.

## Why `response.json()` fails

If you try to parse the raw response body as a single JSON document it will throw an error:

```python theme={null}
response = requests.post("https://api.connectly.ai/external/v1/ai/agent_graph/invoke", ...)
data = response.json()  # ❌ JSONDecodeError
```

The response body contains multiple JSON objects separated by newlines — not a single valid JSON document. The standard `response.json()` method cannot handle that format.

## The correct approach

Enable streaming on the request and iterate over the response line by line, parsing each non-empty line as its own JSON object:

```python theme={null}
import requests, json

response = requests.post(
    "https://api.connectly.ai/external/v1/ai/agent_graph/invoke",
    headers={"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"},
    json={
        "clientKey": "customer-123",
        "businessId": "your-business-id",
        "sessionId": "b6bed81c-5fda-486d-b96c-1ff3af36219a",
        "inputEvents": [{
            "messageEvent": {
                "role": "USER",
                "content": {"textContent": {"text": "Hi"}}
            }
        }]
    },
    stream=True          # ✅ don't buffer the full response
)

for raw_line in response.iter_lines():   # ✅ one line at a time
    if raw_line:                          # skip keep-alive empty lines
        event = json.loads(raw_line.decode("utf-8"))
        print(event)
```

Key differences from a standard request:

* Pass `stream=True` so the response body is not buffered all at once.
* Use `response.iter_lines()` to read one line at a time.
* Skip empty lines with `if raw_line:` — these are keep-alive bytes with no data.
* Decode each line from bytes to UTF-8, then parse with `json.loads()`.

## Parsing approaches compared

| Approach                                 | Works with `/invoke`? | Notes                                          |
| ---------------------------------------- | --------------------- | ---------------------------------------------- |
| `response.json()`                        | ❌                     | Throws `JSONDecodeError`.                      |
| `response.iter_lines()` + `json.loads()` | ✅                     | Correct — parse each line individually.        |
| `response.text.split("\n")`              | ⚠️                    | Works but error-prone — prefer `iter_lines()`. |
| `/invoke_sync` + `response.json()`       | ✅                     | No streaming; waits for the full response.     |

<Tip>
  If you don't need real-time streaming — for example, in a backend worker that waits for the complete response — use [Invoke (sync)](/ai/agent-graph/invoke-sync) instead. It returns a single aggregated JSON object and works with a standard `response.json()` call.
</Tip>

## What each line contains

Each parsed line is a JSON event object from the agent. The structure varies by event type — a `messageEvent` carries the agent's text reply, a `recommendationEvent` carries product suggestions, an `agentHandoverEvent` signals a human handoff. See [Invoke (stream)](https://docs.connectly.ai/api-reference/agent-graph-invoke) for the full response event schema.
