> ## 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 SDK Examples

> Complete SDK implementation examples for iOS and Android

This guide provides complete SDK implementation examples for iOS (Swift) and Android (Kotlin), covering both JWT and Widget authentication modes.

<Note>
  For React Native applications, consider using the JavaScript SDK (`@useago/sdk`) as an alternative to the native implementations below.
</Note>

## Prerequisites

Before implementing the SDK, review:

* [Mobile Authentication](./mobile-auth) - Authentication methods
* [Mobile Messaging API](./mobile-messaging) - Messaging endpoints
* [Mobile Ticketing API](./mobile-ticketing) - Ticket creation

***

## Swift (iOS) - Complete SDK

### JWT Authentication Mode

```swift theme={null}
class AgoChatSDK {
    let baseURL = "https://your-domain.example.com/api/sdk/v1"
    var authToken: String
    let widgetId: String
    let origin: String

    init(authToken: String, widgetId: String, origin: String) {
        self.authToken = authToken
        self.widgetId = widgetId
        self.origin = origin
    }

    private func createAuthenticatedRequest(url: URL) -> URLRequest {
        var request = URLRequest(url: url)
        request.setValue(widgetId, forHTTPHeaderField: "X-User-Anon-Id")
        request.setValue(origin, forHTTPHeaderField: "Origin")
        request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")
        return request
    }

    // Get conversation history
    func getConversations() async throws -> [Thread] {
        let url = URL(string: "\(baseURL)/conversations")!
        var request = createAuthenticatedRequest(url: url)

        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode([Thread].self, from: data)
    }

    // Get conversation with messages
    func getConversation(conversationId: String) async throws -> MessagesResponse {
        let url = URL(string: "\(baseURL)/conversations/\(conversationId)")!
        var request = createAuthenticatedRequest(url: url)

        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(MessagesResponse.self, from: data)
    }

    // Send message
    func sendMessage(content: String, threadId: String? = nil) async throws -> Message {
        let url = URL(string: "\(baseURL)/messages")!
        var request = createAuthenticatedRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        var body: [String: Any] = ["content": content]
        if let threadId = threadId {
            body["thread"] = ["id": threadId]
        }

        request.httpBody = try JSONSerialization.data(withJSONObject: body)

        // Handle SSE response
        let (bytes, _) = try await URLSession.shared.bytes(for: request)
        return try await processSSEStream(bytes)
    }

    // Create support ticket
    func createTicket(
        subject: String,
        typology: String,
        priority: String,
        body: String,
        conversationId: String? = nil
    ) async throws -> TicketResponse {
        let url = URL(string: "\(baseURL)/tickets")!
        var request = createAuthenticatedRequest(url: url)
        request.httpMethod = "POST"

        let boundary = UUID().uuidString
        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

        var formData = Data()
        formData.appendFormField(boundary: boundary, name: "subject", value: subject)
        formData.appendFormField(boundary: boundary, name: "typology", value: typology)
        formData.appendFormField(boundary: boundary, name: "priority", value: priority)
        formData.appendFormField(boundary: boundary, name: "body", value: body)

        if let conversationId = conversationId {
            formData.appendFormField(boundary: boundary, name: "conversation_id", value: conversationId)
        }

        formData.append("--\(boundary)--\r\n".data(using: .utf8)!)
        request.httpBody = formData

        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(TicketResponse.self, from: data)
    }

    private func processSSEStream(_ bytes: URLSession.AsyncBytes) async throws -> Message {
        // SSE processing implementation
        // See Mobile Messaging API for the full streaming implementation
        var content = ""
        var messageId = ""
        for try await line in bytes.lines {
            guard line.hasPrefix("data: ") else { continue }
            let jsonString = String(line.dropFirst(6))
            guard let data = jsonString.data(using: .utf8),
                  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { continue }
            if let chunk = json["content"] as? String { content += chunk }
            if let id = json["message_id"] as? String { messageId = id }
            if json["status"] as? String == "DONE" { break }
        }
        return Message(id: messageId, content: content, role: "assistant", agent: nil, knowledgeSources: nil)
    }
}

// MARK: - Models

struct Thread: Codable {
    let id: String
    let title: String?
    let lastMessageDate: String?

    enum CodingKeys: String, CodingKey {
        case id, title
        case lastMessageDate = "last_message_date"
    }
}

struct Message: Codable {
    let id: String
    let content: String
    let role: String
    let agent: Agent?
    let knowledgeSources: [KnowledgeSource]?

    enum CodingKeys: String, CodingKey {
        case id, content, role, agent
        case knowledgeSources = "knowledge_sources"
    }
}

struct Agent: Codable {
    let id: String
    let name: String
}

struct KnowledgeSource: Codable {
    let id: String
    let knowledgeDocument: KnowledgeDocument

    enum CodingKeys: String, CodingKey {
        case id
        case knowledgeDocument = "knowledge_document"
    }
}

struct KnowledgeDocument: Codable {
    let id: String
    let title: String
    let externalLinkUrl: String?
    let internalLinkUrl: String?

    enum CodingKeys: String, CodingKey {
        case id, title
        case externalLinkUrl = "external_link_url"
        case internalLinkUrl = "internal_link_url"
    }
}

struct MessagesResponse: Codable {
    let messages: [Message]
    let thread: Thread
}

struct TicketResponse: Codable {
    let id: String
    let ticketUrl: String

    enum CodingKeys: String, CodingKey {
        case id
        case ticketUrl = "ticket_url"
    }
}

// MARK: - Helper Extension

extension Data {
    mutating func appendFormField(boundary: String, name: String, value: String) {
        append("--\(boundary)\r\n".data(using: .utf8)!)
        append("Content-Disposition: form-data; name=\"\(name)\"\r\n\r\n".data(using: .utf8)!)
        append("\(value)\r\n".data(using: .utf8)!)
    }
}
```

### Widget Authentication Mode

```swift theme={null}
class AgoChatWidgetSDK {
    let baseURL = "https://your-domain.example.com/api/sdk/v1"
    let widgetId: String
    let origin: String
    var userEmail: String?

    init(origin: String) {
        self.origin = origin

        // Generate or retrieve persistent widget ID
        if let savedId = UserDefaults.standard.string(forKey: "ago_widget_id") {
            self.widgetId = savedId
        } else {
            self.widgetId = UUID().uuidString
            UserDefaults.standard.set(self.widgetId, forKey: "ago_widget_id")
        }
    }

    private func createRequest(for endpoint: String) -> URLRequest {
        let url = URL(string: "\(baseURL)/\(endpoint)")!
        var request = URLRequest(url: url)
        request.setValue(widgetId, forHTTPHeaderField: "X-User-Anon-Id")
        request.setValue(origin, forHTTPHeaderField: "Origin")
        if let email = userEmail {
            request.setValue(email, forHTTPHeaderField: "X-Widget-Email")
        }
        return request
    }

    // Get conversation history
    func getConversations() async throws -> [Thread] {
        var request = createRequest(for: "conversations")
        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode([Thread].self, from: data)
    }

    // Send message
    func sendMessage(content: String, threadId: String? = nil) async throws {
        var request = createRequest(for: "messages")
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        var body: [String: Any] = ["content": content]
        if let threadId = threadId {
            body["thread"] = ["id": threadId]
        }

        request.httpBody = try JSONSerialization.data(withJSONObject: body)
        // Handle SSE response
    }

    // Create support ticket
    func createTicket(
        subject: String,
        typology: String,
        priority: String,
        body: String,
        conversationId: String? = nil
    ) async throws -> TicketResponse {
        var request = createRequest(for: "tickets")
        request.httpMethod = "POST"

        let boundary = UUID().uuidString
        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

        var formData = Data()
        formData.appendFormField(boundary: boundary, name: "subject", value: subject)
        formData.appendFormField(boundary: boundary, name: "typology", value: typology)
        formData.appendFormField(boundary: boundary, name: "priority", value: priority)
        formData.appendFormField(boundary: boundary, name: "body", value: body)

        if let conversationId = conversationId {
            formData.appendFormField(boundary: boundary, name: "conversation_id", value: conversationId)
        }

        formData.append("--\(boundary)--\r\n".data(using: .utf8)!)
        request.httpBody = formData

        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(TicketResponse.self, from: data)
    }
}

// Usage example
let widgetSDK = AgoChatWidgetSDK(
    origin: "https://your-app.example.com"
)

// Optionally set user email for tracking
widgetSDK.userEmail = "user@example.com"

// Use the SDK
Task {
    let conversations = try await widgetSDK.getConversations()
    print("Found \(conversations.count) conversations")
}
```

***

## Kotlin (Android) - Complete SDK

### JWT Authentication Mode

```kotlin theme={null}
class AgoChatSDK(
    private val authToken: String,
    private val widgetId: String,
    private val origin: String
) {
    private val baseURL = "https://your-domain.example.com/api/sdk/v1"
    private val client = OkHttpClient()
    private val gson = Gson()

    private fun createAuthenticatedRequestBuilder(url: String): Request.Builder {
        return Request.Builder()
            .url(url)
            .header("X-User-Anon-Id", widgetId)
            .header("Origin", origin)
            .header("Authorization", "Bearer $authToken")
    }

    // Get conversation history
    suspend fun getConversations(): List<Thread> {
        val request = createAuthenticatedRequestBuilder("$baseURL/conversations")
            .build()

        return withContext(Dispatchers.IO) {
            client.newCall(request).execute().use { response ->
                val json = response.body?.string()
                gson.fromJson(json, Array<Thread>::class.java).toList()
            }
        }
    }

    // Get conversation with messages
    suspend fun getConversation(conversationId: String): MessagesResponse {
        val request = createAuthenticatedRequestBuilder("$baseURL/conversations/$conversationId")
            .build()

        return withContext(Dispatchers.IO) {
            client.newCall(request).execute().use { response ->
                val json = response.body?.string()
                gson.fromJson(json, MessagesResponse::class.java)
            }
        }
    }

    // Send message with SSE handling
    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 = createAuthenticatedRequestBuilder("$baseURL/messages")
            .header("Accept", "text/event-stream")
            .post(requestBody.toString().toRequestBody("application/json".toMediaType()))
            .build()

        val eventSourceListener = createSSEListener(onContentUpdate, onComplete, onError)
        client.newEventSource(request, eventSourceListener)
    }

    // Create support ticket
    suspend fun createTicket(
        subject: String,
        typology: String,
        priority: String,
        body: String,
        conversationId: String? = null
    ): TicketResponse {
        val formBody = MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("subject", subject)
            .addFormDataPart("typology", typology)
            .addFormDataPart("priority", priority)
            .addFormDataPart("body", body)

        conversationId?.let { formBody.addFormDataPart("conversation_id", it) }

        val request = createAuthenticatedRequestBuilder("$baseURL/tickets")
            .post(formBody.build())
            .build()

        return withContext(Dispatchers.IO) {
            client.newCall(request).execute().use { response ->
                val json = response.body?.string()
                gson.fromJson(json, TicketResponse::class.java)
            }
        }
    }

    private fun createSSEListener(
        onContentUpdate: (String) -> Unit,
        onComplete: () -> Unit,
        onError: (String) -> Unit
    ): EventSourceListener {
        return object : EventSourceListener() {
            private val accumulatedContent = StringBuilder()

            override fun onEvent(
                eventSource: EventSource,
                id: String?,
                type: String?,
                data: String
            ) {
                try {
                    if (data == "heartbeat") return

                    val json = JSONObject(data)

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

                    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?
            ) {
                onError("Stream failed: ${t?.message}")
            }
        }
    }
}

// MARK: - Models

data class Thread(
    val id: String,
    val title: String?,
    @SerializedName("last_message_date") val lastMessageDate: String?
)

data class Message(
    val id: String,
    val content: String,
    val role: String,
    val agent: Agent?,
    @SerializedName("knowledge_sources") val knowledgeSources: List<KnowledgeSource>?
)

data class Agent(
    val id: String,
    val name: String
)

data class KnowledgeSource(
    val id: String,
    @SerializedName("knowledge_document") val knowledgeDocument: KnowledgeDocument
)

data class KnowledgeDocument(
    val id: String,
    val title: String,
    @SerializedName("external_link_url") val externalLinkUrl: String?,
    @SerializedName("internal_link_url") val internalLinkUrl: String?
)

data class MessagesResponse(
    val messages: List<Message>,
    val thread: Thread
)

data class TicketResponse(
    val id: String,
    @SerializedName("ticket_url") val ticketUrl: String
)
```

### Widget Authentication Mode

```kotlin theme={null}
class AgoChatWidgetSDK(
    private val origin: String,
    private val context: Context
) {
    private val baseURL = "https://your-domain.example.com/api/sdk/v1"
    private val client = OkHttpClient()
    private val gson = Gson()
    private val widgetId: String
    var userEmail: String? = null

    init {
        // Generate or retrieve persistent widget ID
        val sharedPrefs = context.getSharedPreferences("ago_chat", Context.MODE_PRIVATE)
        widgetId = sharedPrefs.getString("widget_id", null) ?: UUID.randomUUID().toString().also {
            sharedPrefs.edit().putString("widget_id", it).apply()
        }
    }

    private fun createRequestBuilder(endpoint: String): Request.Builder {
        val builder = Request.Builder()
            .url("$baseURL/$endpoint")
            .header("X-User-Anon-Id", widgetId)
            .header("Origin", origin)

        userEmail?.let { builder.header("X-Widget-Email", it) }
        return builder
    }

    // Get conversation history
    suspend fun getConversations(): List<Thread> {
        val request = createRequestBuilder("conversations").build()

        return withContext(Dispatchers.IO) {
            client.newCall(request).execute().use { response ->
                val json = response.body?.string()
                gson.fromJson(json, Array<Thread>::class.java).toList()
            }
        }
    }

    // Send message
    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 = createRequestBuilder("messages")
            .header("Content-Type", "application/json")
            .header("Accept", "text/event-stream")
            .post(requestBody.toString().toRequestBody("application/json".toMediaType()))
            .build()

        // Handle SSE response - see JWT mode for implementation
    }

    // Create support ticket
    suspend fun createTicket(
        subject: String,
        typology: String,
        priority: String,
        body: String,
        conversationId: String? = null
    ): TicketResponse {
        val formBody = MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("subject", subject)
            .addFormDataPart("typology", typology)
            .addFormDataPart("priority", priority)
            .addFormDataPart("body", body)

        conversationId?.let { formBody.addFormDataPart("conversation_id", it) }

        val request = createRequestBuilder("tickets")
            .post(formBody.build())
            .build()

        return withContext(Dispatchers.IO) {
            client.newCall(request).execute().use { response ->
                val json = response.body?.string()
                gson.fromJson(json, TicketResponse::class.java)
            }
        }
    }
}

// Usage example
val widgetSDK = AgoChatWidgetSDK(
    origin = "https://your-app.example.com",
    context = applicationContext
)

// Optionally set user email for tracking
widgetSDK.userEmail = "user@example.com"

// Use the SDK
lifecycleScope.launch {
    val conversations = widgetSDK.getConversations()
    println("Found ${conversations.size} conversations")
}
```

***

## Best Practices

<Tip>
  ### Authentication Mode Selection

  * Use JWT mode for registered users with accounts
  * Use Widget mode for anonymous users or embedded chat scenarios
  * Never mix authentication modes in the same session
</Tip>

<Tip>
  ### Widget ID Management

  * Generate UUID once per installation
  * Store persistently (UserDefaults/SharedPreferences)
  * Use same ID across app sessions for conversation continuity
  * Clear ID only on app uninstall or user logout
</Tip>

<Tip>
  ### Error Handling

  * Implement retry logic for network failures
  * Handle 401 errors (invalid or expired token)
  * Handle 403 errors (domain not in allowed list)
  * Show user-friendly error messages
</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>

***

## Related Documentation

* [Mobile Authentication](./mobile-auth) - Authentication setup
* [Mobile Messaging API](./mobile-messaging) - Messaging endpoints
* [Mobile Ticketing API](./mobile-ticketing) - Ticket creation
* [Form Configuration API](./form-configuration) - Form configuration
