SDK Documentation

Cuti-E SDK banner

Cuti-E provides native SDKs for iOS and Android that let you collect user feedback directly in your mobile app. The SDKs handle creating conversations, sending messages, and receiving push notifications - all with minimal code.

Getting Started: You need an App ID from the Admin Dashboard.
Go to Settings → Apps to create your app and get your App ID.

Quick Start

Choose your platform to get started in under 5 minutes:

1. Add the Package

In Xcode, go to File → Add Package Dependencies and enter:

https://github.com/cuti-e/ios-sdk

2. Configure the SDK

import CutiE

// In AppDelegate or @main App
CutiE.shared.configure(appId: "app_your_app_id_here")

3. Show Feedback Form

import SwiftUI
import CutiE

struct ContentView: View {
    @State private var showFeedback = false

    var body: some View {
        Button("Send Feedback") {
            showFeedback = true
        }
        .sheet(isPresented: $showFeedback) {
            CutiEFeedbackView { conversationId in
                print("Submitted: \(conversationId)")
            }
        }
    }
}
Feedback form screenshot showing category selection and message input

The feedback form allows users to submit bug reports, feature requests, and general feedback.

1. Add the Dependency

In your settings.gradle.kts, add JitPack:

dependencyResolutionManagement {
    repositories {
        maven { url = uri("https://jitpack.io") }
    }
}

Then add the dependency in build.gradle.kts:

dependencies {
    implementation("com.github.Stig-Johnny.cutie:cutie:1.0.0")
}

2. Configure the SDK

import com.cutie.sdk.CutiE

// In your Application class
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        CutiE.getInstance(this).configure(
            appId = "app_your_app_id_here"
        )
    }
}

3. Show Feedback Form

import com.cutie.sdk.CutiEFeedbackView

@Composable
fun MyScreen() {
    var showFeedback by remember { mutableStateOf(false) }

    Button(onClick = { showFeedback = true }) {
        Text("Send Feedback")
    }

    if (showFeedback) {
        CutiEFeedbackView(
            onDismiss = { showFeedback = false },
            onSuccess = { conversationId ->
                println("Submitted: $conversationId")
            }
        )
    }
}

Configuration Options

Option Type Description
appId String Your App ID from the admin dashboard (required)
apiURL String API endpoint (default: production server)

Testing: Use the sandbox environment for development:
apiURL: "https://cutie-worker-sandbox.invotekas.workers.dev"

iOS Installation

Swift Package Manager

Add Cuti-E to your project using Xcode:

  1. File → Add Package Dependencies...
  2. Enter: https://github.com/cuti-e/ios-sdk
  3. Choose version or branch
  4. Add to your target

Manual Installation

Add to your Package.swift:

dependencies: [
    .package(url: "https://github.com/cuti-e/ios-sdk.git", from: "1.0.104")
],
targets: [
    .target(
        name: "YourTarget",
        dependencies: ["CutiE"]
    )
]

Requirements

iOS Basic Usage

Create a Conversation (Callback)

CutiE.shared.createConversation(
    category: .bug,
    message: "The app crashes when I tap save",
    title: "Crash on Save"
) { result in
    switch result {
    case .success(let conversationId):
        print("Created: \(conversationId)")
    case .failure(let error):
        print("Error: \(error.localizedDescription)")
    }
}

List Conversations (iOS 15+)

// Using async/await
let conversations = try await CutiE.shared.getConversations()
for conv in conversations {
    print("\(conv.title ?? "Untitled") - \(conv.status)")
}

Send a Message (iOS 15+)

// Using async/await
let message = try await CutiE.shared.sendMessage(
    conversationId: "conv_abc123",
    message: "Thanks for the help!"
)

iOS Feedback View

The SDK includes a ready-to-use SwiftUI feedback form:

import SwiftUI
import CutiE

struct SettingsView: View {
    @State private var showFeedback = false

    var body: some View {
        List {
            Button("Send Feedback") {
                showFeedback = true
            }
        }
        .sheet(isPresented: $showFeedback) {
            CutiEFeedbackView { conversationId in
                // Called when feedback is submitted
                print("Created conversation: \(conversationId)")
            }
        }
    }
}

iOS In-app Inbox

Let users view their feedback conversations and admin replies directly in your app (iOS 15+).

In-app inbox screenshot showing conversation list with unread badges

The inbox shows all conversations and allows users to continue chatting with support.

UX Best Practice: Add an inbox icon to your navigation bar so users can easily access their feedback from anywhere in your app. Don't bury it deep in Settings!

Recommended: Navigation Bar Icon with Badge

Add a persistent inbox icon with an unread message badge in your main view's navigation bar:

import SwiftUI
import CutiE

struct MainView: View {
    @State private var showInbox = false
    @State private var showFeedback = false
    @State private var unreadCount = 0

    var body: some View {
        NavigationView {
            YourContentView()
                .navigationTitle("Home")
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        HStack(spacing: 16) {
                            // Inbox button with badge
                            Button { showInbox = true } label: {
                                ZStack(alignment: .topTrailing) {
                                    Image(systemName: "tray.full")

                                    if unreadCount > 0 {
                                        Text(unreadCount > 99 ? "99+" : "\(unreadCount)")
                                            .font(.system(size: 10, weight: .bold))
                                            .foregroundColor(.white)
                                            .padding(.horizontal, 5)
                                            .padding(.vertical, 2)
                                            .background(Color.red)
                                            .clipShape(Capsule())
                                            .offset(x: 8, y: -8)
                                    }
                                }
                            }

                            // Submit feedback button
                            Button { showFeedback = true } label: {
                                Image(systemName: "exclamationmark.bubble")
                            }
                        }
                    }
                }
                .sheet(isPresented: $showInbox) {
                    CutiEInboxView()
                }
                .sheet(isPresented: $showFeedback) {
                    CutiEFeedbackView { _ in }
                }
                .task {
                    await loadUnreadCount()
                }
        }
    }

    private func loadUnreadCount() async {
        guard CutiE.shared.isConfigured else { return }
        do {
            unreadCount = try await CutiE.shared.getUnreadCount()
        } catch {
            print("Failed to load unread count: \(error)")
        }
    }
}

Alternative: Settings Menu

struct SettingsView: View {
    @State private var showInbox = false

    var body: some View {
        List {
            Button("My Feedback") {
                showInbox = true
            }
        }
        .sheet(isPresented: $showInbox) {
            CutiEInboxView()
        }
    }
}

UIKit Integration

// Present inbox modally from any view controller
CutiE.shared.showInbox()

// Or from a specific view controller
CutiE.shared.showInbox(from: viewController)

Async/Await API

// Get all conversations
let conversations = try await CutiE.shared.getConversations()

// Get single conversation with messages
let conversation = try await CutiE.shared.getConversation(id: "conv_abc123")

// Send a reply
let message = try await CutiE.shared.sendMessage(
    conversationId: "conv_abc123",
    message: "Thanks for the help!"
)

// Get unread message count (for badge display)
let unreadCount = try await CutiE.shared.getUnreadCount()

Android Installation

Gradle (via JitPack)

Add JitPack to your root settings.gradle.kts:

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }
}

Add the dependency to your app's build.gradle.kts:

dependencies {
    implementation("com.github.Stig-Johnny.cutie:cutie:1.0.0")
}

Requirements

Android Basic Usage

Create a Conversation

CutiE.getInstance(context).createConversation(
    category = ConversationCategory.BUG,
    message = "The app crashes when I tap save",
    title = "Crash on Save"
) { result ->
    result.onSuccess { conversation ->
        println("Created: ${conversation.id}")
    }.onFailure { error ->
        println("Error: ${error.message}")
    }
}

List Conversations

CutiE.getInstance(context).listConversations { result ->
    result.onSuccess { conversations ->
        conversations.forEach { conv ->
            println("${conv.title} - ${conv.status}")
        }
    }
}

Send a Message

CutiE.getInstance(context).sendMessage(
    message = "Thanks for the help!",
    conversationID = "conv_abc123"
) { result ->
    // Handle result
}

Android Feedback View

The SDK includes a Jetpack Compose feedback form:

import com.cutie.sdk.CutiEFeedbackView

@Composable
fun SettingsScreen() {
    var showFeedback by remember { mutableStateOf(false) }

    Button(onClick = { showFeedback = true }) {
        Text("Send Feedback")
    }

    if (showFeedback) {
        Dialog(onDismissRequest = { showFeedback = false }) {
            Surface(shape = MaterialTheme.shapes.large) {
                CutiEFeedbackView(
                    onDismiss = { showFeedback = false },
                    onSuccess = { conversationId ->
                        println("Created: $conversationId")
                    }
                )
            }
        }
    }
}

Android In-app Inbox

Let users view their feedback conversations and admin replies directly in your app.

Android in-app inbox showing conversation list

The inbox shows all conversations and allows users to continue chatting with support.

UX Best Practice: Add an inbox icon to your navigation bar so users can easily access their feedback from anywhere in your app. Don't bury it deep in Settings!

Basic Usage

import com.cutie.sdk.views.CutiEInboxView
import com.cutie.sdk.views.CutiEConversationView

@Composable
fun SettingsScreen() {
    var showInbox by remember { mutableStateOf(false) }
    var selectedConversation by remember { mutableStateOf<Conversation?>(null) }

    Button(onClick = { showInbox = true }) {
        Text("My Feedback")
    }

    if (showInbox) {
        Dialog(
            onDismissRequest = { showInbox = false },
            properties = DialogProperties(usePlatformDefaultWidth = false)
        ) {
            Surface(
                modifier = Modifier.fillMaxSize(),
                shape = MaterialTheme.shapes.large
            ) {
                CutiEInboxView(
                    onDismiss = { showInbox = false },
                    onConversationSelected = { conversation ->
                        selectedConversation = conversation
                    }
                )
            }
        }
    }

    // Show conversation detail when selected
    selectedConversation?.let { conversation ->
        Dialog(
            onDismissRequest = { selectedConversation = null },
            properties = DialogProperties(usePlatformDefaultWidth = false)
        ) {
            Surface(
                modifier = Modifier.fillMaxSize(),
                shape = MaterialTheme.shapes.large
            ) {
                CutiEConversationView(
                    conversationId = conversation.conversationId,
                    onDismiss = { selectedConversation = null }
                )
            }
        }
    }
}

Navigation Bar with Badge

Add an inbox icon with unread count badge to your app bar:

@Composable
fun MainScreen() {
    var showInbox by remember { mutableStateOf(false) }
    var unreadCount by remember { mutableStateOf(0) }

    // Load unread count on launch
    LaunchedEffect(Unit) {
        CutiE.instance?.getUnreadCount { result ->
            result.onSuccess { unreadCount = it }
        }
    }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("My App") },
                actions = {
                    // Inbox button with badge
                    IconButton(onClick = { showInbox = true }) {
                        BadgedBox(
                            badge = {
                                if (unreadCount > 0) {
                                    Badge { Text(unreadCount.toString()) }
                                }
                            }
                        ) {
                            Icon(Icons.Default.Inbox, "Feedback Inbox")
                        }
                    }
                }
            )
        }
    ) { padding ->
        // Your content here
    }

    // Show inbox dialog
    if (showInbox) {
        // ... same as above
    }
}

Callback API

// List all conversations
CutiE.instance?.listConversations { result ->
    result.onSuccess { conversations ->
        conversations.forEach { println("${it.title} - ${it.status}") }
    }
    result.onFailure { error ->
        println("Error: ${error.message}")
    }
}

// Get single conversation with messages
CutiE.instance?.getConversation(conversationId) { result ->
    result.onSuccess { conversation ->
        conversation.messages?.forEach { println(it.message) }
    }
}

// Send a reply
CutiE.instance?.sendMessage(conversationId, "Thanks!") { result ->
    result.onSuccess { message -> println("Sent: ${message.messageId}") }
}

// Get unread count for badge
CutiE.instance?.getUnreadCount { result ->
    result.onSuccess { count -> updateBadge(count) }
}

Conversation API

Categories

Category Description
bugBug reports
featureFeature requests
questionQuestions
feedbackGeneral feedback
otherOther

Statuses

Status Description
openNew, unassigned
in_progressBeing worked on
waiting_userWaiting for user response
waiting_adminWaiting for admin response
resolvedIssue resolved
closedConversation closed

Message API

Sender Types

Type Description
userMessage from app user
adminMessage from support admin
systemAutomated system message

Push Notifications

Push notification showing admin reply to user feedback

Users receive push notifications when admins reply to their feedback.

Important: Push notifications require setup in three places: Xcode, Apple Developer Portal, and the Admin Dashboard. See the complete setup guide for step-by-step instructions.

iOS (APNs)

import UserNotifications
import CutiE

// 1. Request permission (in app startup)
CutiE.shared.pushNotifications.requestPermission { granted in
    print("Push notifications \(granted ? "enabled" : "denied")")
}

// 2. In AppDelegate, forward the device token to CutiE
func application(_ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    CutiE.shared.pushNotifications.didRegisterForRemoteNotifications(withDeviceToken: deviceToken)
}

func application(_ application: UIApplication,
    didFailToRegisterForRemoteNotificationsWithError error: Error) {
    CutiE.shared.pushNotifications.didFailToRegisterForRemoteNotifications(withError: error)
}

// 3. Clear badge when app becomes active (SwiftUI)
@Environment(\.scenePhase) private var scenePhase

.onChange(of: scenePhase) { newPhase in
    if newPhase == .active {
        CutiE.shared.pushNotifications.clearBadgeCount()
    }
}

Push Notification Setup Screenshots

For detailed setup instructions with screenshots, see the iOS SDK Push Notifications Guide.

Xcode Push Capability setup screen

Enable Push Notifications capability in Xcode

Apple Developer Portal keys page

Create APNs key in Apple Developer Portal

Admin Dashboard APNs settings page

Configure APNs in Cuti-E Admin Dashboard

Android (FCM)

import com.google.firebase.messaging.FirebaseMessaging
import com.cutie.sdk.CutiE

FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
    if (task.isSuccessful) {
        CutiE.getInstance(context).registerPushToken(task.result) { result ->
            result.onSuccess {
                println("Push token registered")
            }
        }
    }
}

Need help? Open an issue on GitHub or check out the full SDK code on GitHub.

Authentication Overview

Cuti-E supports three authentication methods, each designed for different use cases:

Method Used By How It Works
Session Token Admin Dashboard, iOS Admin App Login with email/password, Apple, or Google. Full access to team resources.
API Key MCP servers, CI/CD, integrations Scoped access with optional app restrictions. Created in Settings → API Keys.
Device Auth iOS/Android SDKs Challenge-response flow using App ID + Device ID. Handled automatically by the SDK.

When to Use Each Method

Scenario Recommended
End-user feedback from mobile appsDevice Auth (automatic via SDK)
Admin managing conversations in browserSession Token (login flow)
MCP server or AI assistant integrationAPI Key with minimal scopes
CI/CD pipeline reading analyticsAPI Key with analytics:read scope
Webhook or server-to-server integrationAPI Key or OAuth credentials

API Keys

API keys provide scoped, long-lived access for integrations, MCP servers, and automation. Keys are created in the Admin Dashboard under Settings → API Keys.

Creating an API Key

  1. Go to Admin DashboardSettings
  2. Scroll to the API Keys section
  3. Click + New API Key
  4. Enter a descriptive name (e.g., “MCP Server”)
  5. Select the scopes your integration needs (see Scopes Reference)
  6. Optionally restrict to specific apps
  7. Set an expiry period (required for scoped keys)
  8. Click Create and copy the key immediately — it is only shown once

Important: The full API key is only displayed once at creation time. Store it securely. If you lose it, revoke the key and create a new one.

Key Format

ck_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
├────┤├──┤├──────────────────────────────┤
  │    │              │
  │    │   Random token (high entropy)
  │    │
  │    Environment ("live" — currently always live)
  │
  Prefix (ck = "cutie key")

Using API Keys

Send the key in the Authorization header with all API requests:

curl -H "Authorization: Bearer ck_live_a1b2c3..." \
  https://api.cuti-e.com/v1/conversations

API Key Endpoints

Method Endpoint Auth Description
POST/v1/admin/api-keysSessionCreate a new API key
GET/v1/admin/api-keysSessionList all API keys
DELETE/v1/admin/api-keys/:keyIdSessionRevoke an API key
GET/v1/auth/token/verifySession / API Key / OAuthVerify token and view metadata

Create API Key (Request)

POST /v1/admin/api-keys
Content-Type: application/json
Authorization: Bearer <session-token>

{
  "name": "MCP Server",
  "expires_in_days": 90,
  "scopes": ["conversations:read", "messages:read"],
  "app_ids": ["app_abc123"]
}

Create API Key (Response)

{
  "key_id": "akey_x1y2z3",
  "api_key": "ck_live_a1b2c3d4...",
  "key_prefix": "ck_live_a1b2c3d4",
  "name": "MCP Server",
  "scopes": ["conversations:read", "messages:read"],
  "app_ids": ["app_abc123"],
  "expires_at": 1718234567890,
  "created_at": 1710458567890
}

Verify Token

GET /v1/auth/token/verify
Authorization: Bearer ck_live_a1b2c3d4...

// Response
{
  "valid": true,
  "admin_id": "admin_xxx",
  "customer_id": "cust_xxx",
  "type": "api_key",
  "scopes": ["conversations:read", "messages:read"],
  "app_ids": ["app_abc123"],
  "key_id": "akey_x1y2z3"
}

Scopes Reference

Scopes control what an API key can access. Use the minimum scopes needed for your integration.

Conversations & Messages

Scope Grants Access To
conversations:readList and view conversations
conversations:writeUpdate conversation status and priority
messages:readRead messages within conversations
messages:writeSend admin replies to conversations

Management

Scope Grants Access To
apps:readList registered apps
apps:writeCreate and update apps
tags:readList conversation tags
tags:writeCreate and manage tags
analytics:readView conversation and usage analytics
canned_responses:readList saved response templates
canned_responses:writeCreate and edit response templates

Admin

Scope Grants Access To
team:readList team members and roles
team:writeInvite, remove, and update team members
customer:readView account/billing information
customer:writeUpdate account settings
webhooks:readList webhook configurations
webhooks:writeCreate and manage webhooks

App Restrictions

In addition to scopes, API keys can be restricted to specific apps. When app_ids is provided as a non-empty array, the key can only access conversations and data belonging to those apps. For access to all apps in the team, omit app_ids entirely or set it to null (an empty array [] is not accepted).

Error Responses

Status Meaning
401 UnauthorizedMissing, invalid, or expired token
403 ForbiddenToken valid but lacks required scope for this endpoint
// 403 Forbidden - insufficient scopes
{
  "error": "Insufficient scope. Required: messages:write"
}

Migration Guide

If your integration uses the legacy api_key field from the customers table, follow these steps to migrate to scoped API keys.

What Changed

Feature Legacy Key Scoped API Key
FormatRandom stringck_live_... (prefixed)
ScopesFull accessConfigurable per key
App restrictionsNoneOptional per-app targeting
ExpiryNever1–365 days (configurable)
RevocationRegenerate in settingsRevoke individual keys
AuditNo trackingLast-used timestamp
Multiple keysOne per teamUnlimited per team

Migration Steps

  1. Create a scoped key in Settings → API Keys with the scopes your integration needs
  2. Update your integration to use the new ck_live_... key in the Authorization: Bearer header
  3. Test that all operations still work with the new key
  4. Remove the legacy key from your integration’s configuration

Backward compatibility: Legacy API keys continue to work and provide full access. There is no immediate deadline to migrate, but scoped keys are recommended for better security.

Best Practices

Least Privilege

Only grant the scopes your integration actually needs. For example, an MCP server that reads conversations should use conversations:read and messages:read — not full access.

One Key Per Integration

Create a separate API key for each service or tool. This way, revoking one key doesn’t break other integrations.

Integration Recommended Scopes
MCP Server (read-only)conversations:read, messages:read
MCP Server (with replies)conversations:read, conversations:write, messages:read, messages:write
Analytics dashboardanalytics:read
CI/CD webhook setupwebhooks:read, webhooks:write

Key Rotation

Set expiry dates and rotate keys before they expire. Create a new key, update your integration, then revoke the old one. This avoids downtime.

Never Commit Keys to Git

Store API keys in environment variables, secret managers (e.g., Bitwarden, 1Password), or CI/CD secrets. Never hardcode them in source code or commit them to version control.

# Environment variable
export CUTIE_API_KEY="ck_live_a1b2c3..."

# Use in your integration
const apiKey = process.env.CUTIE_API_KEY;