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

# iOS and Android SDKs

> Add your agent to a native iOS or Android app with streaming replies, signed-in users, conversation history and app actions

The iOS SDK and the Android SDK put your agent inside your own app. The SDK
handles the conversation: it streams the agent's reply, keeps the history, signs
users in and runs the actions your app provides. Your app draws the chat screen,
with SwiftUI, UIKit or Jetpack Compose.

Both SDKs, and an example app for each, are public on GitHub:

| | SDK | Example app |
| - | - | - |
| iOS | [centillion-ios-sdk](https://github.com/Centillion-Labs/centillion-ios-sdk) | [ios-sdk-example-app](https://github.com/Centillion-Labs/ios-sdk-example-app) |
| Android | [centillion-android-sdk](https://github.com/Centillion-Labs/centillion-android-sdk) | [android-sdk-example-app](https://github.com/Centillion-Labs/android-sdk-example-app) |

## Before you start

**1. Open the SDK channel**

Open **Channels** in your agent's sidebar. On the **iOS SDK** or
**Android SDK** card, choose **Setup** (**Manage** once the channel is
enabled). The channel opens on its **Setup**
tab, next to **Overview** and **Actions**.

**2. Copy the agent ID**

Under **Agent ID**, choose **Copy**. You pass this ID when you create the
client. The **Quick start** snippet on the same tab already contains it,
together with the base URL of your Centillion host, which ends in
`/api/sdk`.

**3. Turn on Visibility**

Open **Overview**, switch on **Visibility** and save. Until you do, the
agent answers no messages from the app, and requests fail with
`AGENT_NOT_FOUND`. The card on **Channels** shows **Enabled** once the
platform is visible.

iOS and Android are separate channels with their own Visibility switch and
their own action settings. Turn on each one your app ships on.

## iOS

The iOS SDK needs iOS 17 or later (macOS 14 or later), Swift 6 and Xcode 16 or
later.

### Install the iOS package

In Xcode, choose **File → Add Package Dependencies** and paste the package URL:

```text theme={null}
https://github.com/Centillion-Labs/centillion-ios-sdk.git
```

Select version `0.1.0` or later and add **CentillionSDK** to your app target.
For a Swift package, add the dependency to `Package.swift` instead:

```swift Package.swift theme={null}
dependencies: [
    .package(
        url: "https://github.com/Centillion-Labs/centillion-ios-sdk.git",
        from: "0.1.0"
    )
]
// In your target's dependencies:
// .product(name: "CentillionSDK", package: "centillion-ios-sdk")
```

### Send a message from Swift

```swift theme={null}
import CentillionSDK

let client = CentillionClient(
    agentId: "YOUR_AGENT_ID",
    baseURL: "https://your-centillion-host/api/sdk"
)

let response = try await client.send("Hello!") { callbacks in
    callbacks.onTextDelta = { text in print(text, terminator: "") }
}
print(response.message.text)
```

The client and its callbacks run on the main actor, so call it from an `async`
function or a `Task`. `onTextDelta` receives the reply as it streams;
`onToolCall` and `onToolResult` report the actions the agent runs.

To continue a conversation, pass its ID: `send("Tell me more", conversationId:
response.conversationId)`. A `send` without a `conversationId` starts a new
conversation. Cancel the calling `Task` to cancel the request.

### On iOS

* The signed-in user's token is kept in the Keychain, readable only on this
  device. After the app restarts, the user is still signed in, but
  `currentUserId` stays empty until you call `identify` again.
* The SDK accepts plain HTTP only for `localhost`, `127.0.0.1` and `::1`. In
  the simulator, use `http://localhost:3000/api/sdk` against a local server.
  Remote servers need HTTPS.

## Android

The Android SDK needs Android 7.0 or later (API 24), JDK 17 or later, and an
Android Studio version that supports Android Gradle plugin 8.13.2. The SDK
builds with Kotlin 2.2.20 and Gradle 9.2.1, and adds the `INTERNET` permission
to your app.

### Install the Android module

The SDK is installed from source. Clone it beside your project:

```bash theme={null}
git clone --branch 0.1.0 https://github.com/Centillion-Labs/centillion-android-sdk.git
```

Then include its `centillion-sdk` module and depend on it. Use the Google and
Maven Central repositories.

```kotlin theme={null}
// settings.gradle.kts
include(":centillion-sdk")
project(":centillion-sdk").projectDir =
    file("../centillion-android-sdk/centillion-sdk")

// app/build.gradle.kts
dependencies {
    implementation(project(":centillion-sdk"))
}
```

### Send a message from Kotlin

```kotlin theme={null}
import com.centillion.sdk.Centillion

val client = Centillion.create(
    context = context,
    agentId = "YOUR_AGENT_ID",
    baseUrl = "https://your-centillion-host/api/sdk"
)

val response = client.sendMessage("Hello!") {
    onTextDelta { text -> print(text) }
}
println(response.message.text)
```

Call the client's `suspend` functions from a coroutine, such as
`viewModelScope`. Its callbacks (`onStart`, `onTextDelta`, `onToolCall`,
`onToolResult`, `onFinish` and `onError`) run on `Dispatchers.Main`.

The Android client continues its current conversation by default. Pass another
`conversationId` to switch, or call `client.newConversation()` to start a new
one. Cancel the coroutine to cancel the request.

### Raw event streams

`sendMessage` runs your app's actions for you. If you need every event
yourself, `sendMessageStream` and `retryStream` return a `Flow<StreamEvent>`
with `Start`, `TextDelta`, `ToolRequested`, `ToolCompleted` and `Finish`. These
flows do not run actions: for a `ToolRequested` event, call
`submitToolResult(conversationId, requestId, callId, output)` yourself, unless
the call's `execution` is `"server"`, which means Centillion already ran it.

### On Android

* The signed-in user's token is kept in memory only, never in preferences. Call
  `identify` with a fresh token from your backend each time the app starts.
* The SDK accepts plain HTTP only for `localhost`, `127.0.0.1`, `10.0.2.2` and
  `::1`. In the emulator, use `http://10.0.2.2:3000/api/sdk` against a local
  server. Remote servers need HTTPS.

## Replies and retries

Every reply ends with a response that holds `message`, `conversationId`,
`userMessageId`, `finishReason` and `usage`, which counts the `credits`,
`inputTokens` and `outputTokens` the reply used.

`sendNonStreaming` waits for the whole reply and returns the same response,
running your app's actions along the way.

`retry` takes a conversation ID and the ID of one of the agent's answers, and
asks the agent to answer again. The saved conversation changes only when the
new answer succeeds: the answer and every message after it are replaced. If the
retry fails, the conversation stays as it was.

When the stream ends before the reply finishes, the request fails with
`INCOMPLETE_STREAM`. A lost connection is a `URLSession` error on iOS and a
`NetworkException` on Android. The SDK never sends the message again by itself; show the
error and let the customer try again.

## Conversation state

`ConversationState` keeps one conversation ready for your chat screen. On iOS
its properties are observable; on Android it publishes a `StateFlow` snapshot.
It holds `messages`, `conversationId`, `isSending`, `isLoadingHistory`,
`hasMoreHistory`, `toolCalls`, `toolResults` and `error`. Use `sendMessage`,
`retry`, `loadHistory`, `loadMoreHistory`, `clearError` and `clear`.

`ConversationListState` does the same for the list of conversations, with
`conversations`, `isLoading`, `hasMore` and `error`, and `load`, `loadMore`,
`clearError` and `clear`.

When the signed-in user changes, cancel any work still running and clear both.

## History

```swift Swift theme={null}
let page = try await client.listMessages(conversationId: id, limit: 20)
if page.hasMore, let cursor = page.cursor {
    let older = try await client.listMessages(
        conversationId: id, cursor: cursor, limit: 20
    )
    // Prepend older.data to your messages.
}
```

```kotlin Kotlin theme={null}
val page = client.listMessages(conversationId, limit = 20)
if (page.hasMore) {
    val older = client.listMessages(conversationId, page.cursor, 20)
    // Prepend older.data to your messages.
}
```

`listConversations` and `listMessages` return a page with `data`, `total`,
`hasMore` and `cursor`. A page holds 20 items unless you ask for between 1 and
100\. Conversations come most recently active first, each with a `title` taken
from the customer's first message. The first page of messages holds the newest ones, in
the order they were written; each later page holds older messages to put in
front.

History only holds this agent's iOS and Android conversations. Website
conversations are never included.

* **Before sign-in**, history belongs to the app installation. The SDK creates
  an installation ID the first time it runs and keeps it for that agent and
  base URL.
* **A signed-in user** has the same history on every device and on both
  platforms.

A conversation's `status` is `ongoing`, `taken_over` or `ended`. An ended
conversation accepts no new messages or retries. While a teammate has the
conversation (`taken_over`), new messages go to them rather than to the agent,
and retries are refused. See [Helpdesk](https://cntillion.com/docs/helpdesk.md) for taking a conversation
over.

To rate an answer, call `feedback` with the conversation ID, the answer's ID and
`"up"` or `"down"`. Pass `nil` on iOS or `null` on Android to clear the rating.

## Signed-in users

When someone is signed in to your app, your backend can vouch for them, so
their conversations follow them across devices.

**1. Get the token secret**

Open **Settings → Identity** and copy the **Token secret**. Keep it on your
backend. It must never ship inside the app.

**2. Sign a token on your backend**

After your backend has authenticated the user, sign a JWT with HS256 and the
token secret. It must name the user in `sub` or `user_id`. If it has an
`exp`, it can be at most 24 hours ahead, plus 60 seconds for clock
differences. The token works exactly like the website widget's; see
[Identity verification](https://cntillion.com/docs/customer-data/identity-verification.md) for the
contact fields it can carry.

**3. Pass it to the SDK**

Fetch the token from your backend and call `identify`. Call `logout` when
the user signs out.

```swift Swift theme={null}
try await client.identify(token: jwtFromYourBackend)
client.logout()
```

```kotlin Kotlin theme={null}
client.identify(jwtFromYourBackend)
client.logout()
```

Signing in moves the conversations this installation had before sign-in to the
user and starts a new conversation. Signing out forgets the user and the
current conversation but keeps the installation ID, and deletes no history on
the server.

A token that is invalid or expired never falls back to anonymous chat: every
request fails with `INVALID_IDENTITY` until you call `identify` with a new one.

Signed-in users need the Standard plan or above, and identity verification set
up for the agent. Without them, every token is refused. See
[plans](https://cntillion.com/docs/workspaces/billing.md).

## App actions

The agent can ask your app to do something, such as reading the app version or
opening a screen, and use the result in its answer.

**1. Create the action**

Create a custom action and choose **Run client-side code** in **Action
type**. Keep **Wait for response** on when the agent needs the result. See
[Custom tools](https://cntillion.com/docs/agents/custom-actions.md).

**2. Enable it for the app**

Turn the action on for **iOS SDK**, **Android SDK** or both. The SDK
channel's **Actions** tab lists the actions for that platform.

**3. Register a handler**

Register a handler under the action's exact name:

```swift Swift theme={null}
client.tool("get_app_version") { input in
    .object(["version": .string("1.0.0")])
}
```

```kotlin Kotlin theme={null}
client.tool("get_app_version") { input ->
    JSONObject().put("version", "1.0.0")
}
```

When the agent calls the action, the SDK runs your handler, sends its result
back and lets the agent finish its answer.

* The result must be JSON of at most 20,000 bytes.
* By default the SDK runs at most 10 app actions per reply. On iOS, change
  this with `maxToolLoopSteps` when you create the client; on Android, set
  `client.maxToolLoopSteps`.
* With **Wait for response** on, your handler has 25 seconds. After that, the
  agent tells the customer the action could not be completed.
* With no handler under that name, or when the handler throws, the agent gets
  an error instead of a result.

Actions that call an API run on Centillion and need no handler in the app.
`onToolCall` and `onToolResult` report them too, with `execution` set to
`"server"`. Each platform uses its own action settings, even in a conversation
that started on the other one.

An action written as JavaScript for the website does not run in a native app.
Implement it in Swift or Kotlin instead.

## Errors

On iOS, server errors are `SDKError`, and lost connections are `URLSession`
errors. On Android, server errors are `ApiException`, and lost connections are
`NetworkException`. Both server error types carry `status`, `code` and
`message`. A cancelled request stays a cancellation.

| Code | What to do |
| - | - |
| `AGENT_NOT_FOUND` | Check the agent ID, and that Visibility is on for this platform. |
| `INVALID_IDENTITY` | Get a new token from your backend and call `identify`. |
| `CONVERSATION_NOT_FOUND` | Pick a conversation this user owns, or start a new one. |
| `CONVERSATION_NOT_ONGOING` | Start a new conversation. |
| `CONVERSATION_CHANGED` | Reload the history, then try again. |
| `RATE_LIMITED` | Wait for the agent's rate limit window to pass. |
| `CREDIT_LIMIT_REACHED`, `AGENT_UNAVAILABLE` | Show the message as it is. The workspace's plan, credits or setup need attention in the dashboard. |
| `PROVIDER_ERROR` | Try again later. |
| `INCOMPLETE_STREAM` | Show that the connection failed and offer a retry. |
| `TOOL_RESULT_TOO_LARGE`, `TOOL_LOOP_LIMIT` | Return less data from the action, or make fewer calls. |
| `INSECURE_URL` | Use an HTTPS base URL. |

## Example apps

Each example app is a working chat app built on its SDK, with streaming replies,
history, retry and a `get_app_version` action. To try that action, create a
client action with that name, turn on **Wait for response**, enable it for the
platform and ask the agent for the app version.

* **iOS.** Clone [ios-sdk-example-app](https://github.com/Centillion-Labs/ios-sdk-example-app),
  open `CentillionSample.xcodeproj`, choose an iPhone simulator and run. Run
  `swift test` in a clone of the SDK to test the library.
* **Android.** Clone
  [android-sdk-example-app](https://github.com/Centillion-Labs/android-sdk-example-app)
  with `git clone --recurse-submodules`, open it in Android Studio and run the
  `app` target.

Enter your agent ID and SDK base URL in the app. Only the Debug builds accept
plain HTTP to a local server; use HTTPS on physical devices and in production.

> [!WARNING]
>
> Test with a separate agent. Every message creates real conversations and uses
> credits.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.