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

# Stream a chat answer

> The SSE contract of POST /api/chat/send, with client code.

`POST /api/chat/send` is the only streaming endpoint. It accepts one student message and responds
with `text/event-stream`. OpenAPI cannot describe the event framing, so this page is the contract.

## Request

```http theme={null}
POST /api/chat/send
Authorization: Bearer <token>
Content-Type: application/json
```

| Field            | Type   | Rule                                                 |
| ---------------- | ------ | ---------------------------------------------------- |
| `message`        | string | Optional. Defaults to `""`. Maximum 8000 characters. |
| `image.data`     | string | Base64 of the image bytes. No `data:` prefix.        |
| `image.mimeType` | string | For example `image/jpeg`.                            |

Send `message`, `image`, or both. A request with an empty `message` and no `image` returns `400`
with `{"error":"empty message"}`. The `400` arrives before the stream, as normal JSON.

```json theme={null}
{
  "message": "Bu soruyu çözemedim",
  "image": { "data": "/9j/4AAQSkZJRgABAQ...", "mimeType": "image/jpeg" }
}
```

<Warning>
  The image travels inline as base64 in the JSON body. Compress and resize the photo on the device
  before you encode it. Base64 adds about 33 percent to the byte count.
</Warning>

## Response events

The response status is `200` and the content type is `text/event-stream`. Each event has a name
and a JSON `data` payload.

<ResponseField name="chunk" type="ChatChunkEvent">
  One text delta. Repeats many times.

  ```text theme={null}
  event: chunk
  data: {"text":"Tamam, "}
  ```
</ResponseField>

<ResponseField name="done" type="ChatDoneEvent">
  Terminal. Holds the complete answer text and the reactions.

  ```text theme={null}
  event: done
  data: {"text":"Tamam, türevden başlayalım.","reactions":[{"emoji":"🎉"}]}
  ```
</ResponseField>

<ResponseField name="error" type="ChatErrorEvent">
  Terminal. The turn failed.

  ```text theme={null}
  event: error
  data: {"error":"chat failed"}
  ```
</ResponseField>

The stream emits exactly one terminal event: `done` or `error`. A model failure is an `error`
event, not an HTTP status. The HTTP status stays `200` in both cases.

## Rules for the client

1. Append each `chunk` payload to the text on screen.
2. On `done`, replace the text on screen with the `text` field. This corrects any lost delta.
3. On `done`, show the emoji in `reactions`. The array is usually empty. Lala reacts rarely.
4. On `error`, show a retry control. Do not show the partial text as a finished answer.
5. If the socket closes with no terminal event, treat the turn as failed.

<Note>
  The server persists the student message before the model runs. It persists the answer after the
  model runs. A turn that fails mid-stream can leave the student message in the history with no
  answer. Call `GET /api/chat/history` to resynchronize after an error.
</Note>

## No resume

The stream has no message identifier and no cursor. You cannot reconnect to a running turn. If the
application goes to the background and the connection drops, the server still completes the turn
and writes the answer to the history. Read the history back when the screen returns.

## Timing

The server aborts the model call after 60 seconds. One turn can take several model steps, because
Lala can call tools. Allow at least 90 seconds of client timeout on this request. Do not set an
idle timeout below the gap between two chunks.

## Client code

<CodeGroup>
  ```dart Dart theme={null}
  final request = http.Request('POST', Uri.parse('$baseUrl/api/chat/send'))
    ..headers['Authorization'] = 'Bearer $token'
    ..headers['Content-Type'] = 'application/json'
    ..body = jsonEncode({'message': text});

  final response = await http.Client().send(request);

  String? eventName;
  await for (final line in response.stream
      .transform(utf8.decoder)
      .transform(const LineSplitter())) {
    if (line.startsWith('event: ')) {
      eventName = line.substring(7).trim();
    } else if (line.startsWith('data: ')) {
      final payload = jsonDecode(line.substring(6));
      switch (eventName) {
        case 'chunk':
          onChunk(payload['text'] as String);
        case 'done':
          onDone(payload['text'] as String, payload['reactions'] as List);
        case 'error':
          onError(payload['error'] as String);
      }
    }
  }
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(`${baseUrl}/api/chat/send`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ message: text }),
  });

  const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
  let buffer = '';
  let eventName = null;

  for (;;) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += value;
    const lines = buffer.split('\n');
    buffer = lines.pop();
    for (const line of lines) {
      if (line.startsWith('event: ')) eventName = line.slice(7).trim();
      else if (line.startsWith('data: ')) handle(eventName, JSON.parse(line.slice(6)));
    }
  }
  ```

  ```bash curl theme={null}
  curl -N -X POST "$BASE_URL/api/chat/send" \
    -H "Authorization: Bearer $LALA_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"message":"Merhaba"}'
  ```
</CodeGroup>

<Warning>
  Do not use the browser `EventSource` API. `EventSource` sends only `GET` requests and cannot set
  the `Authorization` header. Use `fetch` with a stream reader, as shown above.
</Warning>

## Concurrency

Send one turn at a time for one student. The server does not reject a second concurrent request,
but both turns read and write the same conversation history. Disable the send control until a
terminal event arrives.
