# 📘 AI Chat Backend API Documentation

> **Version:** 0.0.1
> **Last Updated:** May 2026

---

## Table of Contents

1. [Overview](#1-overview)
2. [Getting Started](#2-getting-started)
3. [Authentication](#3-authentication)
4. [API Flow](#4-api-flow)
5. [API Endpoints](#5-api-endpoints)
   - [AI Provider](#51-ai-provider)
   - [Assistants](#52-assistants)
   - [Conversations](#53-conversations)
   - [Messages](#54-messages)
   - [Upload](#55-upload)
   - [CMS](#56-cms)
6. [Data Models](#6-data-models)
7. [Enums & Definitions](#7-enums--definitions)
8. [Error Handling](#8-error-handling)
9. [Best Practices](#9-best-practices)

---

## 1. Overview

### What is this API?

The AI Chat Backend API is a **RESTful service** that allows you to build chat applications powered by AI. It acts as a gateway between your application and AI models (like OpenAI GPT), handling all the complexity of:

- Managing AI **Assistants** with custom behaviors
- Creating and managing **Conversations** (chat sessions)
- Sending **Messages** and receiving AI responses in real-time
- **Uploading files** that can be used in conversations

### Who should use this API?

- **Mobile app developers** building chat features
- **Web developers** creating customer support bots
- **Product teams** integrating AI into their applications

### Key Concepts

| Term                | Description                                                                       |
| ------------------- | --------------------------------------------------------------------------------- |
| **Assistant**       | An AI personality with specific instructions, like "You are a helpful math tutor" |
| **Conversation**    | A chat session between a user and an assistant                                    |
| **Message**         | A single message in a conversation (from user or assistant)                       |
| **Streaming (SSE)** | Real-time delivery of AI responses as they are generated                          |
| **Device ID**       | Unique identifier for the user's device, used for authentication                  |

---

## 2. Getting Started

### Development Environment

#### Base URL

```
https://api-chatbot-ai-aip568.dev.aperogroup.ai
```

#### API Key DEV:

```
sk-y6o8xn6GuEpqUXlQ9wfOxCmotXPmwdZlXfrhgaGIhXKoulsFwT
```

#### Public Key DEV:

```
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvwFBgEPkjGpXZLrOSAj+
31+8jpQMuMnjvyZMSvCjlNPo9MPc6CsIcbJXpVc+LojM/mpysqpwJFk46ao61j3l
UgZIGOEJyDGKEF7KOKmKJOnfnzcrx0bewnJwco5k64Iw7JRcj3x92iKOjy3Dwib2
ncCQy5emukM63p8e7Ahl0KFoziERQYzGaRSzV+bhcSDkohZeh4GO1wtItgsLPNaA
Ix1vD4y/3S7RVwcEfZ2hiLmR2WBs7vEfFOpWhBcMH3abRjH8eajfM6NaKtuxUrFc
XTnGAm9qpeBeqzxCitiqTrmodTQjX5Y+myexa2HIRWGoSAOxE0TYE2aHk82iq7Gm
nQIDAQAB
-----END PUBLIC KEY-----
```

### Quick Start Example

Here's a typical flow to get an AI conversation working:

```bash
# 1. Get available AI models
GET /api/ai-provider/models

# 2. Create an Assistant
POST /api/assistants

# 3. Create a Conversation with that Assistant
POST /api/conversations

# 4. Send Messages and get AI responses
POST /api/conversations/{conversationId}/messages
```

---

## 3. Authentication

### Device ID Authentication

This API uses **Device ID based authentication**. Every request must include the `x-device-id` header to identify the user.

### Required Headers

| Header         | Value               | Required                   | Description                           |
| -------------- | ------------------- | -------------------------- | ------------------------------------- |
| `x-device-id`  | `your-device-id`    | ✅ Yes                     | Unique identifier for the user/device |
| `Content-Type` | `application/json`  | ✅ Yes (for POST/PATCH)    | Request body format                   |
| `Accept`       | `text/event-stream` | ⚠️ For streaming endpoints | Required for SSE responses            |

### Example Request

```javascript
// JavaScript Example
const deviceId = 'user-device-123';
const { signature, timestamp } = generateSignature(API_KEY, PUBLIC_KEY);

const response = await fetch('{{BASE_URL}}/api/assistants', {
  method: 'GET',
  headers: {
    'x-api-signature': signature, // ← Required: API signature
    'x-api-timestamp': timestamp.toString(), // ← Required: Timestamp
    'x-device-id': deviceId,
  },
});
```

```bash
# cURL Example
curl -X GET "{{BASE_URL}}/api/assistants" \
  -H "x-device-id: user-device-123"
```

### Authentication Flow

```mermaid
sequenceDiagram
    participant Client
    participant API

    Note over Client: Generate or retrieve Device ID
    Client->>Client: Store deviceId in localStorage

    Client->>API: Any API Request with x-device-id header
    API->>API: Validate deviceId

    alt Valid Device ID
        API-->>Client: 200 OK + Response Data
    else Invalid/Missing Device ID
        API-->>Client: 401 Unauthorized
        Client->>Client: Redirect to login
    end
```

### Implementation Example

```javascript
// Recommended: Create a wrapper function for all API calls
const API_BASE_URL = '{{BASE_URL}}';

function authenticatedFetch(path, options = {}) {
  const deviceId = localStorage.getItem('deviceId');

  if (!deviceId) {
    throw new Error('Device ID not found. Please login first.');
  }

  // Generate fresh signature for each request
  const { signature, timestamp } = generateSignature(API_KEY, PUBLIC_KEY);

  return fetch(`${API_BASE_URL}${path}`, {
    ...options,
    headers: {
      ...options.headers,
      'x-api-signature': signature, // ← Required: API signature
      'x-api-timestamp': timestamp.toString(), // ← Required: Timestamp
      'x-device-id': deviceId,
    },
  });
}

// Usage
const response = await authenticatedFetch('/api/assistants');
const data = await response.json();
```

---

## 4. API Flow

### Typical Conversation Flow

```mermaid
sequenceDiagram
    participant Client
    participant API
    participant AI Provider

    Note over Client,API: Authentication: x-device-id header

    Note over Client,AI Provider: Step 1: Get Available Models
    Client->>API: GET /api/ai-provider/models
    API-->>Client: ["gpt-4o-mini", "inhouse"]

    Note over Client,AI Provider: Step 2: Create Assistant (One-time)
    Client->>API: POST /api/assistants
    API-->>Client: Assistant created (assistantId)

    Note over Client,AI Provider: Step 3: Start Conversation
    Client->>API: POST /api/conversations (with assistantId + model)
    API->>AI Provider: Create thread
    AI Provider-->>API: threadId
    API-->>Client: Conversation created (conversationId)

    Note over Client,AI Provider: Step 4: Chat Loop
    loop Each Message
        Client->>API: POST /api/conversations/{id}/messages
        Note right of Client: Accept: text/event-stream
        API->>AI Provider: Send message to AI
        AI Provider-->>API: SSE: token stream
        API-->>Client: SSE: stream_start {threadId, runId}
        API-->>Client: SSE: stream_chunk {"Hello..."}
        API-->>Client: SSE: stream_chunk {"How can I..."}
        API-->>Client: SSE: stream_end {"Full message"}
    end

    Note over Client,AI Provider: Optional: Stop Generation
    Client->>API: POST /api/conversations/{id}/stop
    Note right of Client: Body: {threadId, runId}
    API->>AI Provider: Cancel run
    API-->>Client: Generation stopped
```

### File Upload Flow

```mermaid
sequenceDiagram
    participant Client
    participant API
    participant S3

    Note over Client,API: x-device-id header required

    Client->>API: POST /api/upload/generate-url
    API-->>Client: { uploadUrl, fileUrl, expiresIn }
    Client->>S3: PUT uploadUrl (file binary data)
    Note right of Client: Content-Type must match requested type
    S3-->>Client: 200 OK
    Note over Client: Use fileUrl in messages
```

---

## 5. API Endpoints

### 5.1 AI Provider

Get information about available AI models.

---

#### Get Supported Models

Retrieves a list of all AI models available for use.

```http
GET /api/ai-provider/models
```

**Response:** `200 OK`

```json
{
  "data": ["gpt-4o-mini", "inhouse"]
}
```

---

#### Submit TTS Job

Submit a text-to-speech job. Returns immediately with a `runId` to poll for the result.

```http
POST /api/ai-provider/tts
```

**Required Headers** (forwarded from mobile client — BE proxies these to the TTS workflow):

| Header            | Description       |
| ----------------- | ----------------- |
| `x-api-signature` | API signature     |
| `x-api-timestamp` | Request timestamp |
| `x-api-bundleid`  | App bundle ID     |

**Request Body:**

```json
{
  "input": "Hello, how can I help you today?",
  "model": "tts-1",
  "speed": 1.0,
  "language": "en",
  "refAudio": "https://cdn.example.com/voice-ref.wav"
}
```

| Field      | Type   | Required | Description                           |
| ---------- | ------ | -------- | ------------------------------------- |
| `input`    | string | ✅       | Text to synthesize                    |
| `model`    | string | ❌       | TTS model name                        |
| `speed`    | number | ❌       | Speech speed multiplier (0.5–2.0)     |
| `numStep`  | string | ❌       | Number of diffusion steps             |
| `language` | string | ❌       | Language code (e.g. `en`, `vi`)       |
| `refAudio` | string | ❌       | Reference audio URL for voice cloning |

**Response:** `201 Created`

```json
{
  "data": {
    "runId": "run_abc123xyz",
    "status": "queued"
  },
  "timestamp": "2026-05-19T09:00:00.000Z",
  "path": "/api/ai-provider/tts"
}
```

---

#### Get TTS Job Status

Poll the status of a TTS job submitted via `POST /api/ai-provider/tts`.

```http
GET /api/ai-provider/tts/:runId
```

**Required Headers:** Same as submit — `x-api-signature`, `x-api-timestamp`, `x-api-bundleid`.

**Path Parameters:**

| Parameter | Type   | Description                              |
| --------- | ------ | ---------------------------------------- |
| `runId`   | string | Run ID returned from the submit endpoint |

**Response:** `200 OK`

```json
{
  "data": {
    "runId": "run_abc123xyz",
    "status": "success",
    "output": {
      "path": "tts/2026/05/19/run_abc123xyz.wav",
      "resultFile": "https://cdn.example.com/tts/2026/05/19/run_abc123xyz.wav",
      "resultStatus": "completed"
    },
    "error": null
  },
  "timestamp": "2026-05-19T09:00:05.000Z",
  "path": "/api/ai-provider/tts/run_abc123xyz"
}
```

| `status` value | Meaning                                       |
| -------------- | --------------------------------------------- |
| `queued`       | Job accepted, not yet started                 |
| `running`      | Job in progress                               |
| `success`      | Completed — `output.path` contains the result |
| `failed`       | Job failed — see `error` field                |
| `timeout`      | Job exceeded processing time                  |

---

### 5.2 Assistants

Assistants define how the AI behaves. Think of an assistant as a "personality" you configure.

---

#### Create Assistant

Creates a new AI assistant with custom instructions.

```http
POST /api/assistants
```

**Headers:**

```
x-device-id: your-device-id
Content-Type: application/json
```

**Request Body:**

```json
{
  "name": "Math Tutor",
  "description": "Helps students with math problems",
  "instructions": "You are a helpful math tutor. Explain concepts step by step.",
  "temperature": 0.7,
  "topP": 1.0,
  "maxTokens": 2048
}
```

| Field          | Type   | Required | Description                                   |
| -------------- | ------ | -------- | --------------------------------------------- |
| `name`         | string | ✅       | Name of the assistant                         |
| `description`  | string | ❌       | Optional description                          |
| `instructions` | string | ✅       | System prompt that defines AI behavior        |
| `temperature`  | number | ❌       | Creativity level (0-2). Default: 0.7          |
| `topP`         | number | ❌       | Nucleus sampling (0-1). Default: 1.0          |
| `maxTokens`    | number | ❌       | Max response length (1-128000). Default: 2048 |

**Response:** `201 Created`

```json
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "providerAssistantId": "asst_abc123",
    "name": "Math Tutor",
    "description": "Helps students with math problems",
    "instructions": "You are a helpful math tutor...",
    "temperature": 0.7,
    "topP": 1.0,
    "maxTokens": 2048,
    "createdAt": "2026-01-20T10:00:00.000Z",
    "updatedAt": "2026-01-20T10:00:00.000Z"
  }
}
```

---

#### List Assistants

Retrieves a paginated list of assistants.

```http
GET /api/assistants?skip=0&take=10&search=tutor
```

**Headers:**

```
x-device-id: your-device-id
```

| Parameter | Type   | Default | Description                          |
| --------- | ------ | ------- | ------------------------------------ |
| `skip`    | number | 0       | Number of items to skip              |
| `take`    | number | 10      | Number of items to return (max: 100) |
| `search`  | string | -       | Filter by name or description        |

**Response:** `200 OK`

```json
{
  "data": {
    "items": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "name": "Math Tutor",
        "description": "Helps students with math problems",
        "createdAt": "2026-01-20T10:00:00.000Z"
      }
    ],
    "total": 1
  }
}
```

---

#### Get Assistant by ID

```http
GET /api/assistants/{id}
```

**Headers:**

```
x-device-id: your-device-id
```

**Response:** `200 OK` — Returns the full assistant object.

---

#### Update Assistant

```http
PATCH /api/assistants/{id}
```

**Headers:**

```
x-device-id: your-device-id
Content-Type: application/json
```

**Request Body:** (all fields optional)

```json
{
  "name": "Advanced Math Tutor",
  "temperature": 0.5
}
```

**Response:** `200 OK` — Returns updated assistant.

---

#### Delete Assistant

```http
DELETE /api/assistants/{id}
```

**Headers:**

```
x-device-id: your-device-id
```

**Response:** `204 No Content`

---

### 5.3 Conversations

Conversations are chat sessions linked to a specific assistant.

---

#### Create Conversation

```http
POST /api/conversations
```

**Headers:**

```
x-device-id: your-device-id
Content-Type: application/json
```

**Request Body:**

```json
{
  "assistantId": "550e8400-e29b-41d4-a716-446655440000",
  "title": "Help with Algebra",
  "model": "gpt-4o-mini",
  "instructions": "Focus on quadratic equations",
  "temperature": 0.5,
  "topP": 0.9,
  "maxTokens": 4096
}
```

| Field          | Type   | Required | Description                                  |
| -------------- | ------ | -------- | -------------------------------------------- |
| `assistantId`  | string | ✅       | ID of the assistant to use                   |
| `title`        | string | ❌       | Conversation title (auto-generated if empty) |
| `model`        | enum   | ✅       | Model to use: `gpt-4o-mini` or `inhouse`     |
| `instructions` | string | ❌       | Override assistant's instructions            |
| `temperature`  | number | ❌       | Override temperature (0-2)                   |
| `topP`         | number | ❌       | Override topP (0-1)                          |
| `maxTokens`    | number | ❌       | Override max tokens                          |

**Response:** `201 Created`

```json
{
  "data": {
    "id": "660e8400-e29b-41d4-a716-446655440001",
    "title": "Help with Algebra",
    "assistantId": "550e8400-e29b-41d4-a716-446655440000",
    "model": "gpt-4o-mini",
    "providerThreadId": "thread_abc123",
    "createdAt": "2026-01-20T10:30:00.000Z",
    "updatedAt": "2026-01-20T10:30:00.000Z"
  }
}
```

---

#### List Conversations

```http
GET /api/conversations?skip=0&take=10&search=algebra&assistantId=xxx
```

**Headers:**

```
x-device-id: your-device-id
```

| Parameter     | Type   | Default | Description           |
| ------------- | ------ | ------- | --------------------- |
| `skip`        | number | 0       | Offset for pagination |
| `take`        | number | 10      | Limit (max: 100)      |
| `search`      | string | -       | Search in title       |
| `assistantId` | string | -       | Filter by assistant   |

**Response:** `200 OK`

```json
{
  "data": {
    "items": [
      {
        "id": "660e8400-e29b-41d4-a716-446655440001",
        "title": "Help with Algebra",
        "assistantId": "550e8400-e29b-41d4-a716-446655440000",
        "updatedAt": "2026-01-20T10:30:00.000Z"
      }
    ],
    "total": 1
  }
}
```

---

#### Get Conversation Details

```http
GET /api/conversations/{id}
```

**Headers:**

```
x-device-id: your-device-id
```

**Response:** `200 OK` — Returns full conversation with messages.

---

#### Update Conversation

```http
PATCH /api/conversations/{id}
```

**Headers:**

```
x-device-id: your-device-id
Content-Type: application/json
```

**Request Body:**

```json
{
  "title": "Algebra and Calculus"
}
```

**Response:** `200 OK`

---

#### Delete Conversation

```http
DELETE /api/conversations/{id}
```

**Headers:**

```
x-device-id: your-device-id
```

**Response:** `204 No Content`

> ⚠️ This will also delete all messages in the conversation.

---

### 5.4 Messages

Messages are the core of conversations. You send a message and receive the AI response as a stream.

---

#### Send Message (Streaming)

Send a message to the AI and receive the response as **Server-Sent Events (SSE)**.

```http
POST /api/conversations/{conversationId}/messages
```

**Headers:**

```
x-device-id: your-device-id
Content-Type: application/json
Accept: text/event-stream
```

**Request Body:**

```json
{
  "content": "What is the quadratic formula?",
  "model": "gpt-4o-mini"
}
```

| Field     | Type   | Required | Description                                |
| --------- | ------ | -------- | ------------------------------------------ |
| `content` | string | ✅       | The message text                           |
| `model`   | enum   | ❌       | Model override: `gpt-4o-mini` or `inhouse` |

**Response:** `200 OK` — SSE Stream

The response is a stream of events. Each event follows this format:

```
data: {"type": "<event-type>", "data": {...}}

```

**Event Types:**

| Event Type     | Description          | Data Payload                                                            |
| -------------- | -------------------- | ----------------------------------------------------------------------- |
| `stream_start` | Stream has started   | `{ threadId: "xxx", runId: "xxx" }`                                     |
| `stream_chunk` | Token chunk received | `{ content: "token" }`                                                  |
| `stream_end`   | Message complete     | `{ content: "full message", ttsAudioUrl?: "https://cdn.../audio.wav" }` |
| `error`        | Error occurred       | `{ message: "error details" }`                                          |

> `ttsAudioUrl` in `stream_end` is the full CDN URL of the generated audio. It is `undefined` when TTS is disabled or fails (non-fatal — the text response is still valid).

**Example SSE Response:**

```
data: {"type":"stream_start","data":{"threadId":"thread_abc","runId":"run_123"}}

data: {"type":"stream_chunk","data":{"content":"The"}}

data: {"type":"stream_chunk","data":{"content":" quadratic"}}

data: {"type":"stream_chunk","data":{"content":" formula"}}

data: {"type":"stream_end","data":{"content":"The quadratic formula is x = (-b ± √(b²-4ac)) / 2a...","ttsAudioUrl":"https://cdn.example.com/tts/2026/05/19/run_abc123.wav"}}
```

**JavaScript Implementation Example:**

```javascript
const sendMessage = async (conversationId, content, model) => {
  const deviceId = localStorage.getItem('deviceId');
  const { signature, timestamp } = generateSignature(API_KEY, PUBLIC_KEY);

  const response = await fetch(
    `/api/conversations/${conversationId}/messages`,
    {
      method: 'POST',
      headers: {
        'x-api-signature': signature, // ← Required: API signature
        'x-api-timestamp': timestamp.toString(), // ← Required: Timestamp
        'x-device-id': deviceId,
        'Content-Type': 'application/json',
        Accept: 'text/event-stream',
      },
      body: JSON.stringify({ content, model }),
    },
  );

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let assistantMessage = '';
  let ttsAudioUrl = '';
  let threadId = '';
  let runId = '';

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

    const chunk = decoder.decode(value, { stream: true });
    const lines = chunk.split('\n');

    for (const line of lines) {
      if (line.startsWith('data: ')) {
        try {
          const jsonStr = line.substring(6).trim();
          if (jsonStr === '[DONE]') continue;

          const eventData = JSON.parse(jsonStr);
          const type = eventData.type;
          const data = eventData.data;

          if (type === 'stream_start') {
            // Save these for stop functionality
            threadId = data.threadId;
            runId = data.runId;
          } else if (type === 'stream_chunk') {
            assistantMessage += data.content;
            // Update UI with partial message
          } else if (type === 'stream_end') {
            assistantMessage = data.content;
            ttsAudioUrl = data.ttsAudioUrl; // Full CDN URL, may be undefined
            // Final message received
          }
        } catch (e) {
          console.warn('Failed to parse SSE data', e);
        }
      }
    }
  }

  return { assistantMessage, ttsAudioUrl, threadId, runId };
};
```

---

#### Stop Message Generation

Stop the AI from generating more content mid-stream.

```http
POST /api/conversations/{conversationId}/stop
```

**Headers:**

```
x-device-id: your-device-id
Content-Type: application/json
```

**Request Body:**

```json
{
  "threadId": "thread_abc123",
  "runId": "run_xyz789"
}
```

| Field      | Type   | Required | Description                         |
| ---------- | ------ | -------- | ----------------------------------- |
| `threadId` | string | ✅       | Thread ID from `stream_start` event |
| `runId`    | string | ✅       | Run ID from `stream_start` event    |

**Response:** `200 OK`

> 💡 **Important:** The `threadId` and `runId` are provided in the `stream_start` SSE event when you send a message. You must save these values to enable the stop functionality.

---

#### Get Message History

Retrieve previous messages in a conversation.

```http
GET /api/conversations/{conversationId}/messages?page=1&limit=20
```

**Headers:**

```
x-device-id: your-device-id
```

| Parameter | Type   | Default | Description               |
| --------- | ------ | ------- | ------------------------- |
| `page`    | number | 1       | Page number               |
| `limit`   | number | 20      | Items per page (max: 100) |

**Response:** `200 OK`

```json
{
  "data": {
    "items": [
      {
        "id": "msg_001",
        "role": "user",
        "content": "What is the quadratic formula?",
        "createdAt": "2026-05-19T10:31:00.000Z"
      },
      {
        "id": "msg_002",
        "role": "assistant",
        "content": "The quadratic formula is x = (-b ± √(b²-4ac)) / 2a...",
        "ttsAudioUrl": "https://cdn.example.com/tts/2026/05/19/run_abc123.wav",
        "createdAt": "2026-05-19T10:31:05.000Z"
      }
    ],
    "total": 2
  }
}
```

> `ttsAudioUrl` is only present on `assistant` messages. It is the full CDN URL of the synthesized audio. Omitted when TTS was not generated.

---

#### Delete Message

```http
DELETE /api/conversations/{conversationId}/messages/{messageId}
```

**Headers:**

```
x-device-id: your-device-id
```

**Response:** `204 No Content`

---

### 5.5 Upload

Upload files to use in your conversations.

---

#### Generate Upload URL

Get a presigned URL to upload a file directly to cloud storage.

```http
POST /api/upload/generate-url
```

**Headers:**

```
x-device-id: your-device-id
Content-Type: application/json
```

**Request Body:**

```json
{
  "filename": "document.pdf",
  "contentType": "application/pdf",
  "fileSize": 1024000,
  "folder": "documents"
}
```

| Field         | Type   | Required | Description                                          |
| ------------- | ------ | -------- | ---------------------------------------------------- |
| `filename`    | string | ✅       | Original filename (max: 255 chars)                   |
| `contentType` | enum   | ✅       | MIME type (see [allowed types](#allowed-file-types)) |
| `fileSize`    | number | ❌       | File size in bytes (max: 100MB)                      |
| `folder`      | string | ❌       | Subfolder path (max: 200 chars)                      |

**Response:** `200 OK`

```json
{
  "data": {
    "uploadUrl": "https://bucket.s3.amazonaws.com/path?signature...",
    "fileKey": "app-name/uploads/2026/01/20/abc-123/document.pdf",
    "fileUrl": "https://cdn.example.com/uploads/.../document.pdf",
    "expiresIn": 3600,
    "contentType": "application/pdf"
  }
}
```

**How to Upload:**

```javascript
// Step 1: Get the presigned URL
const { signature, timestamp } = generateSignature(API_KEY, PUBLIC_KEY);

const response = await fetch('/api/upload/generate-url', {
  method: 'POST',
  headers: {
    'x-api-signature': signature, // ← Required: API signature
    'x-api-timestamp': timestamp.toString(), // ← Required: Timestamp
    'x-device-id': deviceId,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    filename: file.name,
    contentType: file.type,
    fileSize: file.size,
  }),
});
const { data } = await response.json();

// Step 2: Upload the file directly to S3 using PUT
// Note: S3 upload does NOT need signature - it uses presigned URL
await fetch(data.uploadUrl, {
  method: 'PUT',
  headers: {
    'Content-Type': data.contentType,
  },
  body: file, // The actual file binary
});

// Step 3: Use fileUrl in your messages
console.log('File available at:', data.fileUrl);
```

---

### 5.6 CMS

The CMS module exposes read-only content managed in Strapi — countries, AI personas, milestones, topics, lessons, and screen layouts. All endpoints require the `x-device-id` header.

#### Localization

Pass the `x-country-code` header to receive localized text fields (titles, descriptions, etc.). Omit it to receive global content with all localized fields as `undefined`.

| Header           | Value         | Required | Description                                 |
| ---------------- | ------------- | -------- | ------------------------------------------- |
| `x-device-id`    | any device ID | Yes      | Required on all endpoints                   |
| `x-country-code` | `EN`, `VI`, … | No       | Country/language code for localized content |

---

#### Recommended Client Flow

```
Step 1 — Load home screen layout
GET /api/cms/screens/{key}
  → topicListSection.topics: topic cards with documentId, slug, type, difficulty, cardPreview
  → bannerSection.lessonDocumentId: featured lesson documentId to deep-link into

Step 2 — User taps a topic → get lessons for that topic
GET /api/cms/lessons?topicDocumentId={topic.documentId}
  ⚠️  Use topic.documentId as the query param — NOT as the path segment
  → Returns paginated lessons belonging to that topic (without content body)

Step 3 — User taps a lesson → get full lesson detail
GET /api/cms/lessons/{lesson.documentId}
  ⚠️  Use lesson.documentId from the lessons list (Step 2) or bannerSection.lessonDocumentId
  → Full detail: content (roleplay-config or sentence-list), persona, goal, scenario, tasks

Supporting endpoints (load once at app start):
GET /api/cms/countries     → Language options
GET /api/cms/personas      → AI character roster
GET /api/cms/milestones    → Achievement definitions
```

> **Common mistake:** Do NOT call `GET /api/cms/lessons/{topic.documentId}` — that endpoint requires a **lesson** documentId. To get lessons for a topic, use `GET /api/cms/lessons?topicDocumentId={topic.documentId}`.

---

#### List Countries

Returns all active countries (used as language options for the app).

```http
GET /api/cms/countries
```

**Response:** `200 OK`

```json
{
  "data": [
    {
      "id": 2,
      "documentId": "j4mrfkhde73ra3umedhjbnwh",
      "code": "VI",
      "name": "Vietnamese",
      "isActive": true
    },
    {
      "id": 1,
      "documentId": "z5q41lv7eatn80dt0bax212n",
      "code": "EN",
      "name": "English",
      "isActive": true,
      "cardPreview": {
        "mediaUrl": "https://dev-static.apero.vn/content/uploads/mo_rong_anh_202604011831_3923a402b1.jpeg",
        "role": "DEFAULT"
      }
    }
  ],
  "timestamp": "19/05/2026 17:15:27",
  "path": "/api/cms/countries"
}
```

---

#### List Personas

Returns published AI personas. Localized fields (`role`, `description`, `personality`, `systemPromptTemplate`) are resolved from the `x-country-code` header.

```http
GET /api/cms/personas?page=1&pageSize=25
```

**Headers:** `x-country-code: EN` (optional)

**Query Parameters:**

| Parameter  | Type   | Default | Description    |
| ---------- | ------ | ------- | -------------- |
| `page`     | number | 1       | Page number    |
| `pageSize` | number | 25      | Items per page |

**Response:** `200 OK`

```json
{
  "data": {
    "items": [
      {
        "id": 5,
        "documentId": "gmv3l8khsr5cni2n8epagwph",
        "name": "Alex",
        "slug": null,
        "cardPreview": {
          "mediaUrl": "https://dev-static.apero.vn/content/uploads/mo_rong_anh_202604011831_3923a402b1.jpeg",
          "role": "DEFAULT"
        },
        "voice": "en-US-GuyNeural",
        "role": "Pronunciation Coach",
        "description": "A professional pronunciation coach focused on clear, natural English speech.",
        "personality": "Precise, supportive, and detail-oriented.",
        "systemPromptTemplate": "You are Alex, a pronunciation coach. Help the user improve their English pronunciation."
      },
      {
        "id": 6,
        "documentId": "xxfr6ej3xz5rqawd2bl4o5no",
        "name": "Emma",
        "slug": null,
        "cardPreview": {
          "mediaUrl": "https://dev-static.apero.vn/content/uploads/mo_rong_anh_202604011831_3923a402b1.jpeg",
          "role": "DEFAULT"
        },
        "voice": "en-US-JennyNeural",
        "role": "English Tutor",
        "description": "A friendly and patient English tutor who helps learners build confidence.",
        "personality": "Warm, encouraging, and clear in explanations.",
        "systemPromptTemplate": "You are Emma, a friendly English tutor. Help the user practice English conversation."
      }
    ],
    "pagination": {
      "page": 1,
      "pageSize": 2,
      "pageCount": 1,
      "total": 2
    }
  },
  "timestamp": "19/05/2026 18:00:45",
  "path": "/api/cms/personas?page=1&pageSize=2"
}
```

> Without `x-country-code`, all localized text fields (`role`, `description`, `personality`, `systemPromptTemplate`) will be `undefined`.

---

#### List Milestones

Returns active milestones with reward configuration.

```http
GET /api/cms/milestones?page=1&pageSize=25
```

**Headers:** `x-country-code: EN` (optional)

**Query Parameters:**

| Parameter  | Type   | Default | Description    |
| ---------- | ------ | ------- | -------------- |
| `page`     | number | 1       | Page number    |
| `pageSize` | number | 25      | Items per page |

**Response:** `200 OK`

```json
{
  "data": {
    "items": [
      {
        "id": 5,
        "documentId": "bc4phn6qy5qdaz8i6v0yx95r",
        "type": "lessons_completed",
        "triggerValue": 10,
        "isActive": true,
        "cardPreview": {
          "mediaUrl": "https://dev-static.apero.vn/content/uploads/mo_rong_anh_202604011831_3923a402b1.jpeg",
          "role": "DEFAULT"
        },
        "title": "10 Lessons Completed!",
        "description": "Complete your first 10 lessons.",
        "reward": {
          "type": "badge",
          "amount": 1,
          "label": "First Steps Badge",
          "cardPreview": {
            "mediaUrl": "https://dev-static.apero.vn/content/uploads/mo_rong_anh_202604011831_3923a402b1.jpeg",
            "role": "DEFAULT"
          }
        }
      },
      {
        "id": 6,
        "documentId": "vygcrls60bin39w1vf4nttig",
        "type": "streak",
        "triggerValue": 7,
        "isActive": true,
        "cardPreview": {
          "mediaUrl": "https://dev-static.apero.vn/content/uploads/mo_rong_anh_202604011831_3923a402b1.jpeg",
          "role": "DEFAULT"
        },
        "title": "7-Day Streak!",
        "description": "Practice every day for 7 consecutive days.",
        "reward": {
          "type": "energy",
          "amount": 50,
          "label": "50 Energy Points",
          "cardPreview": {
            "mediaUrl": "https://dev-static.apero.vn/content/uploads/mo_rong_anh_202604011831_3923a402b1.jpeg",
            "role": "DEFAULT"
          }
        }
      }
    ],
    "pagination": {
      "page": 1,
      "pageSize": 2,
      "pageCount": 1,
      "total": 2
    }
  },
  "timestamp": "19/05/2026 18:01:42",
  "path": "/api/cms/milestones?page=1&pageSize=2"
}
```

---

#### List Lessons

Returns a paginated list of active lessons. Use `topicDocumentId` to filter lessons belonging to a specific topic. Content body is **not** included — call `GET /api/cms/lessons/:documentId` to get full lesson detail.

```http
GET /api/cms/lessons?page=1&pageSize=25&topicDocumentId=dv1kyej7onlk06zhc54ohi4i
```

**Headers:** `x-country-code: EN` (optional)

**Query Parameters:**

| Parameter         | Type   | Default | Description                                                                 |
| ----------------- | ------ | ------- | --------------------------------------------------------------------------- |
| `page`            | number | 1       | Page number                                                                 |
| `pageSize`        | number | 25      | Items per page                                                              |
| `topicDocumentId` | string | —       | Filter lessons belonging to a topic (use `documentId` from topic or screen) |

**Response:** `200 OK`

```json
{
  "data": {
    "items": [
      {
        "id": 5,
        "documentId": "pd5dkfzjljijxu3kkn39f3hf",
        "slug": "ordering-coffee",
        "difficulty": "basic",
        "estimatedSeconds": 240,
        "energyCost": 5,
        "isActive": true,
        "cardPreview": {
          "mediaUrl": "https://dev-static.apero.vn/content/uploads/mo_rong_anh_202604011831_3923a402b1.jpeg",
          "role": "DEFAULT"
        },
        "topics": [
          {
            "id": 5,
            "documentId": "dv1kyej7onlk06zhc54ohi4i",
            "slug": "cafe-ordering"
          }
        ],
        "content": null
      }
    ],
    "pagination": {
      "page": 1,
      "pageSize": 25,
      "pageCount": 1,
      "total": 1
    }
  },
  "timestamp": "19/05/2026 18:12:09",
  "path": "/api/cms/lessons?topicDocumentId=dv1kyej7onlk06zhc54ohi4i"
}
```

> `content` is always `null` in list responses. Call `GET /api/cms/lessons/:documentId` for the full lesson with content.

---

#### Get Lesson

Returns a single lesson by `documentId`, including its content (dynamic zone: `roleplay-config` or `sentence-list`) with all nested data populated.

```http
GET /api/cms/lessons/:documentId
```

**Headers:** `x-country-code: EN` (optional)

**Path Parameters:**

| Parameter    | Type   | Description                      |
| ------------ | ------ | -------------------------------- |
| `documentId` | string | Strapi document ID of the lesson |

**Response:** `200 OK`

```json
{
  "data": {
    "id": 6,
    "documentId": "v398c9npek6tuu3inf932apr",
    "slug": "introducing-yourself",
    "difficulty": "beginner",
    "estimatedSeconds": 300,
    "energyCost": 5,
    "isActive": true,
    "cardPreview": {
      "mediaUrl": "https://dev-static.apero.vn/content/uploads/mo_rong_anh_202604011831_3923a402b1.jpeg",
      "role": "DEFAULT"
    },
    "topics": [
      {
        "id": 6,
        "documentId": "j2vjzslcwoewzwt3em2agpfd",
        "slug": "daily-conversation"
      }
    ],
    "content": {
      "__component": "aip568-speaking.roleplay-config",
      "id": 6,
      "systemPromptOverride": null,
      "persona": {
        "id": 6,
        "documentId": "xxfr6ej3xz5rqawd2bl4o5no",
        "name": "Emma",
        "slug": null,
        "voice": "en-US-JennyNeural",
        "cardPreview": {
          "mediaUrl": "https://dev-static.apero.vn/content/uploads/mo_rong_anh_202604011831_3923a402b1.jpeg",
          "role": "DEFAULT"
        }
      },
      "goal": [
        {
          "id": 160,
          "value": "Practice introducing yourself naturally in English.",
          "language": { "code": "EN", "name": "English" }
        },
        {
          "id": 161,
          "value": "Luyện tập tự giới thiệu bản thân một cách tự nhiên bằng tiếng Anh.",
          "language": { "code": "VI", "name": "Vietnamese" }
        }
      ],
      "scenario": [
        {
          "id": 162,
          "value": "You just joined a new company and are meeting your team for the first time.",
          "language": { "code": "EN", "name": "English" }
        },
        {
          "id": 163,
          "value": "Bạn vừa gia nhập một công ty mới và đang gặp gỡ nhóm lần đầu.",
          "language": { "code": "VI", "name": "Vietnamese" }
        }
      ],
      "inspireExamples": [
        {
          "id": 170,
          "value": "Hi, I'm Sarah. I'm the new marketing manager. I'm originally from New York but I've been living in Hanoi for 3 years.",
          "language": { "code": "EN", "name": "English" }
        }
      ],
      "tasks": [
        {
          "id": 11,
          "requiredPhrases": ["my name is", "i'm from"],
          "description": [
            {
              "id": 164,
              "value": "Tell Emma your name and where you are from.",
              "language": { "code": "EN", "name": "English" }
            },
            {
              "id": 165,
              "value": "Nói với Emma tên và quê quán của bạn.",
              "language": { "code": "VI", "name": "Vietnamese" }
            }
          ],
          "hint": [
            {
              "id": 168,
              "value": "Try: My name is [name] and I'm from [place].",
              "language": { "code": "EN", "name": "English" }
            }
          ]
        },
        {
          "id": 12,
          "requiredPhrases": ["working as", "my role"],
          "description": [
            {
              "id": 166,
              "value": "Share what your role is in the new company.",
              "language": { "code": "EN", "name": "English" }
            },
            {
              "id": 167,
              "value": "Chia sẻ vai trò của bạn trong công ty mới.",
              "language": { "code": "VI", "name": "Vietnamese" }
            }
          ],
          "hint": [
            {
              "id": 169,
              "value": "Try: I'll be working as a [job title].",
              "language": { "code": "EN", "name": "English" }
            }
          ]
        }
      ]
    }
  },
  "timestamp": "19/05/2026 18:01:53",
  "path": "/api/cms/lessons/v398c9npek6tuu3inf932apr"
}
```

> `content` is the first item of the dynamic zone. The shape depends on the component type:
>
> - `aip568-speaking.roleplay-config` — roleplay conversation with persona, goal, scenario, tasks
> - `aip568-pronunciation.sentence-list` — list of sentences with audio, phonetic, translations
>
> **Note:** Localized text fields inside `content` (e.g. `goal`, `scenario`, `tasks[].description`) are returned as raw arrays of `{ id, value, language: { code, name } }` objects — one entry per language. Clients must filter by `language.code` to get the entry matching their locale.
>
> **Note:** Top-level lesson fields `title` (and topic `title` in `topics[]`) are localized and only returned when `x-country-code` header is provided.

**Error Responses:**

| Status          | Description                                   |
| --------------- | --------------------------------------------- |
| `404 Not Found` | Lesson with given `documentId` does not exist |

---

#### Get Screen

Returns the layout configuration for a named screen. Used to drive dynamic home/lobby screen layouts.

```http
GET /api/cms/screens/:key
```

**Headers:** `x-country-code: EN` (optional)

**Path Parameters:**

| Parameter | Type   | Description                                                                      |
| --------- | ------ | -------------------------------------------------------------------------------- |
| `key`     | string | Screen key: `home`, `roleplay`, `pronunciation`, `discover`, `profile`, `custom` |

**Response:** `200 OK`

```json
{
  "data": {
    "id": 3,
    "documentId": "yp6zx56344z4j6cw6s3p06tv",
    "key": "home",
    "isActive": true,
    "topicListSection": {
      "topicType": "speaking",
      "displayMode": "grid",
      "topics": [
        {
          "documentId": "j2vjzslcwoewzwt3em2agpfd",
          "slug": "daily-conversation",
          "type": "speaking",
          "difficulty": "beginner",
          "isActive": true,
          "cardPreview": {
            "mediaUrl": "https://dev-static.apero.vn/content/uploads/mo_rong_anh_202604011831_3923a402b1.jpeg",
            "role": "DEFAULT"
          }
        },
        {
          "documentId": "dv1kyej7onlk06zhc54ohi4i",
          "slug": "cafe-ordering",
          "type": "speaking",
          "difficulty": "basic",
          "isActive": true,
          "cardPreview": {
            "mediaUrl": "https://dev-static.apero.vn/content/uploads/mo_rong_anh_202604011831_3923a402b1.jpeg",
            "role": "DEFAULT"
          }
        }
      ]
    },
    "bannerSection": {
      "cardPreview": {
        "mediaUrl": "https://dev-static.apero.vn/content/uploads/mo_rong_anh_202604011831_3923a402b1.jpeg",
        "role": "DEFAULT"
      },
      "lessonDocumentId": "v398c9npek6tuu3inf932apr"
    }
  },
  "timestamp": "19/05/2026 18:01:48",
  "path": "/api/cms/screens/home"
}
```

**Error Responses:**

| Status          | Description                              |
| --------------- | ---------------------------------------- |
| `404 Not Found` | No active screen found for the given key |

---

## 6. Data Models

### Assistant

```typescript
interface Assistant {
  id: string; // UUID - Unique identifier
  providerAssistantId: string; // Provider's assistant ID
  name: string; // Display name
  description?: string; // Optional description
  instructions: string; // System prompt
  temperature?: number; // Creativity (0-2), default: 0.7
  topP?: number; // Nucleus sampling (0-1), default: 1.0
  maxTokens?: number; // Max response tokens, default: 2048
  createdAt: Date; // Creation timestamp
  updatedAt: Date; // Last update timestamp
}
```

### Conversation

```typescript
interface Conversation {
  id: string; // UUID - Unique identifier
  title: string; // Conversation title
  assistantId: string; // Linked assistant ID
  providerThreadId?: string; // Provider's thread ID
  model?: string; // AI model being used
  instructions?: string; // Instructions override
  temperature?: number; // Temperature override
  topP?: number; // TopP override
  maxTokens?: number; // MaxTokens override
  userId?: string; // Owner user ID (from device)
  createdAt: Date; // Creation timestamp
  updatedAt: Date; // Last update timestamp
  messages?: Message[]; // Messages in conversation
}
```

### Message

```typescript
interface Message {
  id: string; // UUID - Unique identifier
  role: 'user' | 'assistant'; // Who sent the message
  content: string; // Message text
  ttsAudioUrl?: string; // Full CDN URL of the TTS audio (assistant messages only)
  providerMessageId?: string; // Provider's message ID
  createdAt: Date; // Timestamp
}
```

### Upload Response

```typescript
interface UploadResponse {
  uploadUrl: string; // Presigned PUT URL (expires)
  fileKey: string; // S3 object key/path
  fileUrl: string; // Public access URL
  expiresIn: number; // URL expiration in seconds
  contentType: string; // File MIME type
}
```

### CMS — Country

```typescript
interface CardPreviewDto {
  mediaUrl?: string; // Strapi media URL (absent when no media attached)
  role?: string; // DEFAULT | FEATURED | COMPACT | AVATAR | BANNER
}

interface CmsCountryDto {
  id: number;
  documentId: string;
  code: string; // e.g. "EN", "VI"
  name: string;
  isActive: boolean;
  cardPreview?: CardPreviewDto;
}
```

### CMS — Persona

```typescript
interface CmsPersonaDto {
  id: number;
  documentId: string;
  name: string;
  slug: string;
  voice?: string; // TTS voice ID (e.g. "en-US-JennyNeural")
  cardPreview?: CardPreviewDto;
  role?: string; // Localized
  description?: string; // Localized
  personality?: string; // Localized
  systemPromptTemplate?: string; // Localized — base system prompt for AI conversations
}
```

### CMS — Milestone

```typescript
interface CmsRewardDto {
  type: string; // energy | badge | premium_day | custom
  amount?: number;
  label?: string; // Localized
  cardPreview?: CardPreviewDto;
}

interface CmsMilestoneDto {
  id: number;
  documentId: string;
  type: string; // streak | lessons_completed | score_achieved | custom
  triggerValue: number; // Threshold to unlock (e.g. 7 for 7-day streak)
  isActive: boolean;
  cardPreview?: CardPreviewDto;
  title?: string; // Localized
  description?: string; // Localized
  reward?: CmsRewardDto;
}
```

### CMS — Topic

```typescript
interface CmsTopicDto {
  id: number;
  documentId: string;
  slug: string;
  type: string; // speaking | pronunciation | reading | writing | grammar | custom
  difficulty: string; // beginner | basic | intermediate | advanced
  isActive: boolean;
  cardPreview?: CardPreviewDto;
  title?: string; // Localized
  description?: string; // Localized
  shortDescription?: string; // Localized
}
```

### CMS — Lesson

```typescript
interface CmsTopicRefDto {
  id: number;
  documentId: string;
  slug: string;
  title?: string; // Localized — only resolved when x-country-code is set
}

interface CmsLessonDto {
  id: number;
  documentId: string;
  slug: string;
  difficulty: string; // beginner | basic | intermediate | advanced
  estimatedSeconds: number;
  energyCost: number;
  isActive: boolean;
  cardPreview?: CardPreviewDto;
  title?: string; // Localized
  topics?: CmsTopicRefDto[];
  content?: RoleplayConfig | SentenceList | null; // Dynamic zone — first item only
}

// Content: Speaking lesson
interface RoleplayConfig {
  __component: 'aip568-speaking.roleplay-config';
  persona?: PersonaRef;
  goal?: string; // Localized
  scenario?: string; // Localized
  inspireExamples?: string[]; // Localized examples
  systemPromptOverride?: string;
  tasks?: RoleplayTask[];
}

interface RoleplayTask {
  description?: string; // Localized
  hint?: string; // Localized
  requiredPhrases?: string[];
}

// Content: Pronunciation lesson
interface SentenceList {
  __component: 'aip568-pronunciation.sentence-list';
  sentences: Sentence[];
}

interface Sentence {
  text: string;
  phonetic?: string;
  difficulty?: string; // easy | medium | hard
  cardPreview?: CardPreviewDto; // Audio file via cardPreview.mediaUrl
  translations?: string[]; // Localized
  hints?: string[]; // Localized
}
```

### CMS — Screen

```typescript
interface CmsTopicSummaryDto {
  documentId: string;
  slug: string;
  type: string;
  difficulty: string;
  isActive: boolean;
  cardPreview?: CardPreviewDto;
  title?: string; // Localized
}

interface CmsTopicListSectionDto {
  title?: string; // Localized section heading
  topicType?: string; // Pre-filter hint for the list
  displayMode?: string; // grid | list | carousel
  topics?: CmsTopicSummaryDto[];
}

interface CmsBannerSectionDto {
  title?: string; // Localized
  cardPreview?: CardPreviewDto;
  topicDocumentId?: string; // Deep-link to a topic
  lessonDocumentId?: string; // Deep-link to a lesson
}

interface CmsScreenDto {
  id: number;
  documentId: string;
  key: string; // home | roleplay | pronunciation | discover | profile | custom
  isActive: boolean;
  title?: string; // Localized
  topicListSection?: CmsTopicListSectionDto;
  bannerSection?: CmsBannerSectionDto;
}
```

### SSE Event

```typescript
interface SSEEvent {
  type: 'stream_start' | 'stream_chunk' | 'stream_end' | 'error';
  data: StreamStartData | StreamChunkData | StreamEndData | ErrorData;
}

interface StreamStartData {
  threadId: string; // Thread ID for stop functionality
  runId: string; // Run ID for stop functionality
}

interface StreamChunkData {
  content: string; // Partial message token
}

interface StreamEndData {
  content: string; // Full complete message
  ttsAudioUrl?: string; // Full CDN URL of the TTS audio (undefined if TTS failed/disabled)
}

interface ErrorData {
  message: string; // Error description
}
```

---

## 7. Enums & Definitions

### Supported Models

| Value         | Description              |
| ------------- | ------------------------ |
| `gpt-4o-mini` | OpenAI GPT-4o Mini model |
| `inhouse`     | Internal/custom AI model |

### Message Roles

| Value       | Description                 |
| ----------- | --------------------------- |
| `user`      | Message sent by the user    |
| `assistant` | Message generated by the AI |

### Allowed File Types

| MIME Type                                                                 | Extension   | Description            |
| ------------------------------------------------------------------------- | ----------- | ---------------------- |
| `image/jpeg`                                                              | .jpg, .jpeg | JPEG image             |
| `image/png`                                                               | .png        | PNG image              |
| `image/gif`                                                               | .gif        | GIF image              |
| `image/webp`                                                              | .webp       | WebP image             |
| `application/pdf`                                                         | .pdf        | PDF document           |
| `application/msword`                                                      | .doc        | Word document (legacy) |
| `application/vnd.openxmlformats-officedocument.wordprocessingml.document` | .docx       | Word document          |
| `application/json`                                                        | .json       | JSON file              |
| `text/plain`                                                              | .txt        | Plain text             |
| `text/csv`                                                                | .csv        | CSV file               |

---

## 8. Error Handling

### Error Response Format

All errors follow this structure:

```json
{
  "statusCode": 400,
  "message": "Validation failed",
  "error": "Bad Request"
}
```

### Common HTTP Status Codes

| Code  | Meaning               | When It Happens                         |
| ----- | --------------------- | --------------------------------------- |
| `200` | OK                    | Request successful                      |
| `201` | Created               | Resource created                        |
| `204` | No Content            | Deleted successfully                    |
| `400` | Bad Request           | Invalid input data                      |
| `401` | Unauthorized          | Missing or invalid `x-device-id` header |
| `403` | Forbidden             | No permission to access resource        |
| `404` | Not Found             | Resource doesn't exist                  |
| `422` | Unprocessable Entity  | Validation error                        |
| `429` | Too Many Requests     | Rate limit exceeded                     |
| `500` | Internal Server Error | Server issue                            |

### Validation Errors

```json
{
  "statusCode": 400,
  "message": ["name must be a string", "instructions should not be empty"],
  "error": "Bad Request"
}
```

### Handling 401 Unauthorized

When you receive a 401 error, it usually means:

- The `x-device-id` header is missing
- The Device ID is invalid or expired

**Recommended action:** Clear local storage and redirect user to login.

```javascript
if (response.status === 401) {
  localStorage.removeItem('deviceId');
  window.location.href = '/login';
}
```

---

## 9. Best Practices

### ✅ Do's

1. **Always include required headers**

   ```javascript
   const { signature, timestamp } = generateSignature(API_KEY, PUBLIC_KEY);

   headers: {
     'x-api-signature': signature,           // ← Required
     'x-api-timestamp': timestamp.toString(), // ← Required
     'x-device-id': localStorage.getItem('deviceId') // ← Required
   }
   ```

2. **Save threadId and runId from stream_start event**

   ```javascript
   if (eventData.type === 'stream_start') {
     this.threadId = eventData.data.threadId;
     this.runId = eventData.data.runId;
   }
   ```

3. **Handle SSE connection drops gracefully**

   ```javascript
   response.body.on('error', (err) => {
     console.error('Stream error:', err);
     // Show error message to user
     // Optionally retry the request
   });
   ```

4. **Use pagination for large lists**

   ```http
   GET /api/conversations?skip=20&take=10
   ```

5. **Validate file types before upload**

   ```javascript
   const allowedTypes = [
     'image/jpeg',
     'image/png',
     'application/pdf',
     'text/plain',
   ];
   if (!allowedTypes.includes(file.type)) {
     throw new Error('Invalid file type');
   }
   ```

6. **Store Device ID persistently**
   ```javascript
   // On login/first use
   localStorage.setItem('deviceId', generatedDeviceId);
   ```

### ❌ Don'ts

1. **Don't forget the `x-device-id` header** — All requests (except public endpoints) require it.

2. **Don't hardcode IDs** — Always use IDs returned from the API.

3. **Don't ignore the `stream_start` event** — You need `threadId` and `runId` to stop generation.

4. **Don't upload files larger than 100MB** — The API will reject them.

5. **Don't parse SSE responses as regular JSON** — They have a special format (`data: {...}\n\n`).

### Complete API Wrapper Implementation

```javascript
class AIClient {
  constructor(baseUrl, apiKey, publicKey) {
    this.baseUrl = baseUrl;
    this.apiKey = apiKey;
    this.publicKey = publicKey;
    this.deviceId = localStorage.getItem('deviceId');
  }

  async request(path, options = {}) {
    if (!this.deviceId) {
      throw new Error('Not authenticated');
    }

    // Generate fresh signature for each request
    const { signature, timestamp } = generateSignature(
      this.apiKey,
      this.publicKey,
    );

    const response = await fetch(`${this.baseUrl}${path}`, {
      ...options,
      headers: {
        ...options.headers,
        'x-api-signature': signature,
        'x-api-timestamp': timestamp.toString(),
        'x-device-id': this.deviceId,
      },
    });

    if (response.status === 401) {
      localStorage.removeItem('deviceId');
      this.deviceId = null;
      throw new Error('Session expired');
    }

    return response;
  }

  // Assistants
  async getAssistants(params = {}) {
    const query = new URLSearchParams(params).toString();
    const res = await this.request(`/api/assistants?${query}`);
    return res.json();
  }

  async createAssistant(data) {
    const res = await this.request('/api/assistants', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });
    return res.json();
  }

  // Conversations
  async createConversation(assistantId, title, model) {
    const res = await this.request('/api/conversations', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ assistantId, title, model }),
    });
    return res.json();
  }

  // Messages (Streaming)
  async sendMessage(conversationId, content, model, onChunk, onComplete) {
    const res = await this.request(
      `/api/conversations/${conversationId}/messages`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Accept: 'text/event-stream',
        },
        body: JSON.stringify({ content, model }),
      },
    );

    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let threadId, runId;

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

      const chunk = decoder.decode(value, { stream: true });
      for (const line of chunk.split('\n')) {
        if (line.startsWith('data: ')) {
          try {
            const event = JSON.parse(line.substring(6));
            if (event.type === 'stream_start') {
              threadId = event.data.threadId;
              runId = event.data.runId;
            } else if (event.type === 'stream_chunk') {
              onChunk?.(event.data.content);
            } else if (event.type === 'stream_end') {
              onComplete?.(event.data.content, event.data.ttsAudioUrl, {
                threadId,
                runId,
              });
            }
          } catch (e) {}
        }
      }
    }
  }

  async stopGeneration(conversationId, threadId, runId) {
    return this.request(`/api/conversations/${conversationId}/stop`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ threadId, runId }),
    });
  }
}

// Usage
const client = new AIClient(
  '{{BASE_URL}}',
  process.env.API_KEY,
  process.env.PUBLIC_KEY,
);
```

## 10. API Signature Authentication

### What is API Signature?

Think of API signature like a **secret handshake** between your app and our server. Every time you make a request, you need to prove it's really you by sending a special encrypted code.

> [!IMPORTANT]
> **All API requests MUST include a signature** in the `x-api-signature` header. Without it, the server will reject your request.

---

### What You Need Before Starting

Ask your backend team or administrator for these 2 things:

1. **API Key** — A unique text string like `"abc-123-xyz-789"`
2. **Public Key** — A long text that starts with `-----BEGIN PUBLIC KEY-----`

Save these somewhere safe (like environment variables). **Never put them directly in your code!**

---

### How It Works (Simple Explanation)

```mermaid
sequenceDiagram
    participant You as Your App
    participant Server as API Server

    You->>You: 1. Get current time
    You->>You: 2. Combine: time + API key
    You->>You: 3. Encrypt with public key
    You->>You: 4. Convert to base64 text

    You->>Server: 5. Send request with signature

    alt ✅ Signature is valid
        Server-->>You: 200 OK - Here's your data
    else ❌ Signature is wrong
        Server-->>You: 400 Error - Invalid signature
    end
```

**In simple words:**

1. You create a special code using your API key and current time
2. You encrypt it so nobody can read it
3. You send it with your request
4. Server checks if it's correct
5. If correct → you get data. If wrong → you get error.

---

### Step-by-Step: How to Add Signature to Your App

#### Step 1: Copy This Function

Copy this function into your project. This creates the signature for you:

```javascript
const crypto = require('crypto');

// --- Helper: Generate Signature ---
function generateSignature(apiKey, publicKeyPem) {
  const timestamp = Date.now();
  const payload = timestamp + '@@@' + apiKey;

  const buffer = Buffer.from(payload, 'utf8');
  const encrypted = crypto.publicEncrypt(
    {
      key: publicKeyPem,
      padding: crypto.constants.RSA_PKCS1_PADDING,
    },
    buffer,
  );

  const signature = encrypted.toString('base64');
  return { signature, timestamp };
}
```

**What this does:**

- Gets current time in milliseconds
- Combines it with your API key using `@@@` as separator
- Encrypts it using the public key
- Returns the signature and timestamp

#### Step 2: Use It in Your Requests

Here's how to make a request with signature:

```javascript
// Your credentials (get these from your backend team)
const API_KEY = 'your-api-key-here';
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----`;

// Generate signature
const { signature, timestamp } = generateSignature(API_KEY, PUBLIC_KEY);

// Make request
const response = await fetch('https://api.example.com/api/assistants', {
  method: 'GET',
  headers: {
    'x-api-signature': signature, // ← Add this
    'x-api-timestamp': timestamp.toString(), // ← Add this
    'x-device-id': 'your-device-id',
    'Content-Type': 'application/json',
  },
});

const data = await response.json();
console.log(data);
```

---

### Complete Example: API Client Class

Here's a ready-to-use class that handles everything for you:

```javascript
const crypto = require('crypto');
const fetch = require('node-fetch');

class APIClient {
  constructor(apiKey, publicKey, baseUrl) {
    this.apiKey = apiKey;
    this.publicKey = publicKey;
    this.baseUrl = baseUrl;
  }

  // Generate signature automatically
  generateSignature() {
    const timestamp = Date.now();
    const payload = `${timestamp}@@@${this.apiKey}`;
    const buffer = Buffer.from(payload, 'utf8');

    const encrypted = crypto.publicEncrypt(
      {
        key: this.publicKey,
        padding: crypto.constants.RSA_PKCS1_PADDING,
      },
      buffer,
    );

    return {
      signature: encrypted.toString('base64'),
      timestamp,
    };
  }

  // Make any API request
  async request(endpoint, options = {}) {
    // Generate fresh signature for this request
    const { signature, timestamp } = this.generateSignature();

    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      ...options,
      headers: {
        ...options.headers,
        'x-api-signature': signature,
        'x-api-timestamp': timestamp.toString(),
        'x-device-id': options.deviceId || 'default-device',
        'Content-Type': 'application/json',
      },
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`API Error: ${error.message}`);
    }

    return response.json();
  }

  // Example: Get assistants
  async getAssistants(deviceId) {
    return this.request('/api/assistants', { deviceId });
  }

  // Example: Create conversation
  async createConversation(deviceId, data) {
    return this.request('/api/conversations', {
      method: 'POST',
      deviceId,
      body: JSON.stringify(data),
    });
  }
}

// --- How to use it ---
const client = new APIClient(
  process.env.API_KEY, // Your API key
  process.env.PUBLIC_KEY, // Your public key
  'https://api.example.com', // API base URL
);

// Now just use it!
const assistants = await client.getAssistants('device-123');
const conversation = await client.createConversation('device-123', {
  assistantId: 'xxx',
  title: 'My Chat',
});
```

---

### Common Errors and How to Fix Them

| Error Message                 | What It Means                       | How to Fix                                                         |
| ----------------------------- | ----------------------------------- | ------------------------------------------------------------------ |
| `MISSING_REQUIRED_FIELDS`     | You forgot to add signature headers | Make sure you include both `x-api-signature` and `x-api-timestamp` |
| `SIGNATURE_ERROR_DECRYPT`     | Wrong public key or API key         | Double-check you're using the correct keys from your backend team  |
| `SIGNATURE_EXPIRED`           | Request took too long (> 5 minutes) | Generate a new signature right before each request                 |
| `SIGNATURE_INVALID_TIMESTAMP` | Timestamp doesn't match             | Don't change the timestamp after creating the signature            |

#### Example Error Response

```json
{
  "statusCode": 400,
  "message": "Signature expired",
  "error": "Bad Request"
}
```

**What to do:** Generate a fresh signature and try again.

---

### Important Rules (Please Read!)

#### ✅ DO:

1. **Generate a NEW signature for EVERY request**

   ```javascript
   // Good ✅
   const { signature, timestamp } = generateSignature(apiKey, publicKey);
   await fetch(url, { headers: { 'x-api-signature': signature, ... } });
   ```

2. **Store keys in environment variables**

   ```javascript
   // Good ✅
   const API_KEY = process.env.API_KEY;
   const PUBLIC_KEY = process.env.PUBLIC_KEY;
   ```

3. **Generate signature RIGHT BEFORE making the request**
   ```javascript
   // Good ✅
   const makeRequest = async () => {
     const { signature, timestamp } = generateSignature(...);
     return fetch(...); // Immediately use it
   };
   ```

#### ❌ DON'T:

1. **Don't reuse old signatures**

   ```javascript
   // Bad ❌
   const signature = oldSignature; // Don't do this!
   ```

2. **Don't put keys directly in code**

   ```javascript
   // Bad ❌
   const API_KEY = 'abc123'; // Don't hardcode!
   ```

3. **Don't modify timestamp after creating signature**
   ```javascript
   // Bad ❌
   const { signature, timestamp } = generateSignature(...);
   timestamp = Date.now(); // Don't change it!
   ```

---

### Quick Test

Want to test if your signature works? Run this:

```javascript
const crypto = require('crypto');

// Put your real keys here
const TEST_API_KEY = 'your-api-key';
const TEST_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
... your public key ...
-----END PUBLIC KEY-----`;

function testSignature() {
  console.log('🧪 Testing signature generation...\n');

  const timestamp = Date.now();
  const payload = `${timestamp}@@@${TEST_API_KEY}`;
  console.log('📦 Payload:', payload);

  const buffer = Buffer.from(payload, 'utf8');
  const encrypted = crypto.publicEncrypt(
    {
      key: TEST_PUBLIC_KEY,
      padding: crypto.constants.RSA_PKCS1_PADDING,
    },
    buffer,
  );

  const signature = encrypted.toString('base64');
  console.log('✅ Signature created!');
  console.log('📝 First 50 chars:', signature.substring(0, 50) + '...');

  console.log('\n📤 Headers to send:');
  console.log({
    'x-api-signature': signature,
    'x-api-timestamp': timestamp.toString(),
  });
}

testSignature();
```

If this runs without errors, your signature generation is working! 🎉

---

### Checklist Before Going Live

Before you deploy to production, make sure:

- [ ] ✅ You have the API key from your backend team
- [ ] ✅ You have the public key from your backend team
- [ ] ✅ Keys are stored in environment variables (not in code)
- [ ] ✅ You're generating a NEW signature for each request
- [ ] ✅ You're including both `x-api-signature` and `x-api-timestamp` headers
- [ ] ✅ You tested with the test script above
- [ ] ✅ Error handling is in place for signature failures

---

### Need Help?

**Common questions:**

**Q: Where do I get the API key and public key?**  
A: Ask your backend team or system administrator. They will provide both.

**Q: Can I reuse the same signature for multiple requests?**  
A: No! Generate a fresh signature for each request.

**Q: My signature keeps expiring, why?**  
A: Signatures expire after 5 minutes. Generate it right before making the request.

**Q: What's the `@@@` in the payload?**  
A: It's just a separator between timestamp and API key. Don't change it.

**Q: Do I need to understand RSA encryption?**  
A: No! Just copy the `generateSignature` function and use it. It handles everything.

---
