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

# Mobile Messaging API

> Messaging APIs for mobile with conversation history and real-time streaming

This guide covers the messaging APIs for mobile applications, including conversation history, message retrieval, and sending messages with real-time streaming.

## Prerequisites

Before using these APIs, configure authentication as described in [Mobile Authentication](./mobile-auth).

***

## Conversations

Retrieves the list of conversations for the authenticated user.

**Endpoint:** `GET /api/sdk/v1/conversations`

**Response:**

```json theme={null}
[
  {
    "id": "thread-uuid",
    "title": "Thread title",
    "last_message_date": "2024-01-15T10:30:00Z"
  }
]
```

<Info>
  **Usage Notes:**

  * Returns up to 100 most recent conversations
  * Conversations are sorted by last message date (newest first)
  * Each conversation contains a unique ID used to retrieve messages
</Info>

***

## Get Conversation with Messages

Retrieves a specific conversation with all its messages.

**Endpoint:** `GET /api/sdk/v1/conversations/{conversation_id}`

**Parameters:**

* `conversation_id` (path): UUID of the conversation

**Response:**

```json theme={null}
{
  "messages": [
    {
      "id": "message-uuid",
      "content": "Message content",
      "role": "user",
      "answer_text": "Plain text version of the answer",
      "agent": {
        "id": "agent-uuid",
        "name": "Agent Name"
      },
      "knowledge_sources": [
        {
          "id": "source-uuid",
          "knowledge_document": {
            "id": "doc-uuid",
            "title": "Document Title",
            "page_id": "article_12345",
            "knowledge_source": {
              "name": "Help Center"
            },
            "use_external_link": true,
            "external_link_url": "https://...",
            "internal_link_url": "https://.."
          },
          "position": 1
        }
      ]
    }
  ],
  "thread": {
    "id": "thread-uuid",
    "title": "Thread title"
  }
}
```

**Message Fields:**

| Field               | Description                                      |
| ------------------- | ------------------------------------------------ |
| `role`              | Either "user" or "assistant"                     |
| `content`           | Message content (may include markdown)           |
| `answer_text`       | Plain text version of the answer                 |
| `knowledge_sources` | References to documentation used in the response |

***

## Send New Message

Sends a new message to start or continue a conversation.

**Endpoint:** `POST /api/sdk/v1/messages`

**Request Body:**

```json theme={null}
{
  "content": "Your message here",
  "thread": {
    "id": "existing-thread-uuid"
  }
}
```

<Note>
  Omit the `thread` field to start a new conversation. Include it to continue an existing thread.
</Note>

***

## SSE Response Format

The response is sent as Server-Sent Events (SSE) stream for real-time updates.

### Event Format

Each SSE message follows this format:

```
data: {JSON_OBJECT}\n\n
```

Heartbeat messages (which can be ignored):

```
: heartbeat
```

### SSE Message Structure

```json theme={null}
{
  "message_id": "message-uuid",
  "thread": {
    "id": "thread-uuid"
  },
  "content": "Incremental content being streamed",
  "full_content": "Complete message content (sent at the end)",
  "status": "IN_PROGRESS",
  "title": "Thread title (optional)",
  "agent": {
    "id": "agent-uuid",
    "name": "Agent Name"
  },
  "knowledge_sources": [],
  "tool_call_data": [],
  "ask_to_talk_to_human": false,
  "redirect-to-thread-page": false
}
```

### Status Values

| Status        | Description                               |
| ------------- | ----------------------------------------- |
| `IN_PROGRESS` | Message is being processed                |
| `DONE`        | Message processing completed successfully |
| `ERROR`       | An error occurred during processing       |

### Example Stream

```
data: {"agent": {"id": "8095023b-...", "name": "Support Agent"}, "thread": {"id": "2ca06ee4-..."}, "redirect-to-thread-page": true}

: heartbeat

data: {"knowledge_sources": [...], "thread": {"id": "2ca06ee4-..."}, "message_id": "47f7a57f-...", "status": "IN_PROGRESS"}

data: {"content": "How", "thread": {"id": "2ca06ee4-..."}, "status": "IN_PROGRESS"}

data: {"content": " can", "thread": {"id": "2ca06ee4-..."}, "status": "IN_PROGRESS"}

data: {"content": " I", "thread": {"id": "2ca06ee4-..."}, "status": "IN_PROGRESS"}

data: {"content": " help", "thread": {"id": "2ca06ee4-..."}, "status": "IN_PROGRESS"}

data: {"thread": {"id": "2ca06ee4-..."}, "message_id": "47f7a57f-...", "status": "DONE"}

data: {"title": "Hello", "thread": {"id": "2ca06ee4-..."}}
```

### Form Presentation

If a form should be presented to the user (e.g., ticket creation):

```json theme={null}
{
  "tool_call_data": true,
  "status": "waiting_input",
  "id": "tool-call-uuid",
  "tool_name": "ago_ticketing",
  "tool_display_name": "Create Ticket",
  "thread": {"id": "thread-uuid"},
  "ask_to_talk_to_human": true,
  "allowed_to_create_ticket": true,
  "ticket": {
    "subject": "Issue subject",
    "body": "Issue description",
    "priority": "Normal"
  },
  "display_mode": "display",
  "type": "form"
}
```

***

## JavaScript Implementation

```javascript theme={null}
async function sendMessage(content, threadId) {
    const response = await fetch('/api/sdk/v1/messages', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${authToken}`
        },
        body: JSON.stringify({
            content: content,
            thread: threadId ? {id: threadId} : undefined
        })
    });

    const reader = response.body.getReader();
    const decoder = new TextDecoder('utf-8');
    let buffer = '';
    let lastMessageId = null;
    let lastStatus = null;

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

        buffer += decoder.decode(value, {stream: true});

        // Process SSE messages (data: {...}\n\n format)
        let boundary = buffer.indexOf('\n\n');
        while (boundary !== -1) {
            const chunk = buffer.slice(0, boundary + 2);
            buffer = buffer.slice(boundary + 2);

            if (chunk.startsWith('data: ')) {
                try {
                    const json = JSON.parse(chunk.slice(6));

                    // Track message ID and status for fallback polling
                    if (json.message_id) lastMessageId = json.message_id;
                    if (json.status) lastStatus = json.status;

                    // Handle different update types
                    if (json.content) {
                        // Incremental content update
                        updateMessageContent(json.content);
                    }
                    if (json.full_content) {
                        // Complete content replacement
                        replaceMessageContent(json.full_content);
                    }
                    if (json.title) {
                        // Update thread title
                        updateThreadTitle(json.title);
                    }
                    if (json.knowledge_sources) {
                        // Add knowledge sources
                        addSources(json.knowledge_sources);
                    }
                    if (json.status === 'DONE') {
                        // Message completed successfully
                        onMessageComplete();
                    }
                    if (json.status === 'ERROR') {
                        // Handle error
                        onMessageError();
                    }
                } catch (error) {
                    console.error('Failed to parse SSE chunk:', error);
                }
            }

            boundary = buffer.indexOf('\n\n');
        }
    }

    // If stream ended prematurely and message is still in progress, start polling
    if (lastMessageId && lastStatus === 'IN_PROGRESS') {
        startPolling(lastMessageId);
    }
}
```

***

## Kotlin Implementation

```kotlin theme={null}
import okhttp3.*
import okhttp3.sse.*
import org.json.JSONObject
import kotlinx.coroutines.*

class AgoChatSSEClient(private val authToken: String) {
    private val client = OkHttpClient.Builder()
        .readTimeout(0, TimeUnit.SECONDS)
        .build()

    fun sendMessage(
        content: String,
        threadId: String? = null,
        onContentUpdate: (String) -> Unit,
        onComplete: () -> Unit,
        onError: (String) -> Unit
    ) {
        val requestBody = JSONObject().apply {
            put("content", content)
            threadId?.let {
                put("thread", JSONObject().apply { put("id", it) })
            }
        }

        val request = Request.Builder()
            .url("https://your-domain.example.com/api/sdk/v1/messages")
            .header("Authorization", "Bearer $authToken")
            .header("Accept", "text/event-stream")
            .post(requestBody.toString().toRequestBody("application/json".toMediaType()))
            .build()

        var lastMessageId: String? = null
        var lastStatus: String? = null
        var accumulatedContent = StringBuilder()

        val eventSourceListener = object : EventSourceListener() {
            override fun onEvent(
                eventSource: EventSource,
                id: String?,
                type: String?,
                data: String
            ) {
                try {
                    // Ignore heartbeat messages
                    if (data == "heartbeat") return

                    val json = JSONObject(data)

                    // Track message ID and status for fallback polling
                    json.optString("message_id").takeIf { it.isNotEmpty() }?.let {
                        lastMessageId = it
                    }
                    json.optString("status").takeIf { it.isNotEmpty() }?.let {
                        lastStatus = it
                    }

                    // Handle incremental content
                    json.optString("content").takeIf { it.isNotEmpty() }?.let { content ->
                        accumulatedContent.append(content)
                        onContentUpdate(accumulatedContent.toString())
                    }

                    // Handle full content replacement
                    json.optString("full_content").takeIf { it.isNotEmpty() }?.let { fullContent ->
                        accumulatedContent.clear()
                        accumulatedContent.append(fullContent)
                        onContentUpdate(fullContent)
                    }

                    // Handle completion
                    when (json.optString("status")) {
                        "DONE" -> {
                            onComplete()
                            eventSource.cancel()
                        }
                        "ERROR" -> {
                            onError("Message processing failed")
                            eventSource.cancel()
                        }
                    }

                } catch (e: Exception) {
                    println("Failed to parse SSE data: $e")
                }
            }

            override fun onFailure(
                eventSource: EventSource,
                t: Throwable?,
                response: Response?
            ) {
                // If stream failed and message is still in progress, start polling
                if (lastMessageId != null && lastStatus == "IN_PROGRESS") {
                    startPolling(lastMessageId!!, onContentUpdate, onComplete, onError)
                } else {
                    onError("Stream failed: ${t?.message}")
                }
            }

            override fun onClosed(eventSource: EventSource) {
                if (lastMessageId != null && lastStatus == "IN_PROGRESS") {
                    startPolling(lastMessageId!!, onContentUpdate, onComplete, onError)
                }
            }
        }

        client.newEventSource(request, eventSourceListener)
    }

    private fun startPolling(
        messageId: String,
        onContentUpdate: (String) -> Unit,
        onComplete: () -> Unit,
        onError: (String) -> Unit
    ) {
        GlobalScope.launch {
            while (true) {
                delay(2000) // Poll every 2 seconds

                try {
                    val request = Request.Builder()
                        .url("https://your-domain.example.com/api/sdk/v1/messages/$messageId/status")
                        .header("Authorization", "Bearer $authToken")
                        .get()
                        .build()

                    val response = client.newCall(request).execute()
                    val data = JSONObject(response.body?.string() ?: "")

                    data.optString("content").takeIf { it.isNotEmpty() }?.let {
                        onContentUpdate(it)
                    }

                    when (data.optString("status")) {
                        "DONE" -> {
                            onComplete()
                            break
                        }
                        "ERROR" -> {
                            onError("Message processing failed")
                            break
                        }
                    }
                } catch (e: Exception) {
                    onError("Polling error: ${e.message}")
                    break
                }
            }
        }
    }
}

// Usage example
val sseClient = AgoChatSSEClient(authToken)
sseClient.sendMessage(
    content = "Hello, I need help",
    threadId = existingThreadId,
    onContentUpdate = { content ->
        runOnUiThread { messageTextView.text = content }
    },
    onComplete = {
        runOnUiThread { showMessageComplete() }
    },
    onError = { error ->
        runOnUiThread { showError(error) }
    }
)
```

***

## Fallback Polling

If SSE streaming fails or is not supported, use polling as a fallback.

### When to Use Polling

* SSE connection fails or times out
* Behind proxy that doesn't support streaming
* Mobile SDK with limited SSE support
* Testing/debugging scenarios

### Polling Endpoint

**Endpoint:** `GET /api/sdk/v1/messages/{message_id}/status`

**Response:**

```json theme={null}
{
  "message_id": "message-uuid",
  "status": "IN_PROGRESS",
  "content": "Current response text...",
  "full_content": null,
  "progress": 0.65,
  "knowledge_sources": [],
  "timestamp": "2024-12-06T14:30:00Z"
}
```

### Polling Implementation

```javascript theme={null}
async function startPolling(messageId) {
    const pollInterval = setInterval(async () => {
        try {
            const response = await fetch(
                `/api/sdk/v1/messages/${messageId}/status`,
                { headers: {'Authorization': `Bearer ${authToken}`} }
            );

            const data = await response.json();

            // Process the same JSON structure as SSE
            handleMessageUpdate(data);

            if (data.status === 'DONE' || data.status === 'ERROR') {
                clearInterval(pollInterval);
            }
        } catch (error) {
            console.error('Polling error:', error);
            clearInterval(pollInterval);
        }
    }, 2000); // Poll every 2 seconds
}
```

<Info>
  **Polling Best Practices:**

  * Poll every 2 seconds to balance responsiveness and server load
  * Set a maximum polling duration (e.g., 2-3 minutes)
  * Update UI with partial content while polling
  * Retry on network errors, stop on 4xx errors
</Info>

***

## Best Practices

<Tip>
  ### SSE Handling

  * Always implement fallback polling for network interruptions
  * Buffer partial chunks before parsing JSON
  * Handle heartbeat messages gracefully (ignore them)
  * Track message\_id for fallback polling
</Tip>

<Tip>
  ### User Experience

  * Show typing indicators during message processing
  * Display content progressively as it streams
  * Update thread title when received
  * Show knowledge sources for transparency
</Tip>

<Tip>
  ### Error Handling

  * Implement retry logic for network failures
  * Gracefully handle stream disconnections
  * Provide clear error messages to users
  * Log errors for debugging
</Tip>

***

## Related Documentation

* [Mobile Authentication](./mobile-auth) - Authentication setup
* [Mobile Ticketing API](./mobile-ticketing) - Create support tickets
* [Mobile SDK Examples](./mobile-sdk) - Complete implementation examples
* [Send Message API](./send-message-api) - Full API reference
