For React Native applications, consider using the JavaScript SDK (
@useago/sdk) as an alternative to the native implementations below.Prerequisites
Before implementing the SDK, review:- Mobile Authentication - Authentication methods
- Mobile Messaging API - Messaging endpoints
- Mobile Ticketing API - Ticket creation
Swift (iOS) - Complete SDK
JWT Authentication Mode
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
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
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
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
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
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
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
User Experience
- Show typing indicators during message processing
- Display content progressively as it streams
- Update thread title when received
- Show knowledge sources for transparency
Related Documentation
- Mobile Authentication - Authentication setup
- Mobile Messaging API - Messaging endpoints
- Mobile Ticketing API - Ticket creation
- Form Configuration API - Form configuration
