API reference

The complete public surface of the Cluestick Swift SDK at 0.5.0. Anything not listed here is internal and may change without a version bump.

Entry points

Cluestickis an enum used as a namespace — every member is static, and there is nothing to instantiate.

configure

public static func configure(
  apiKey: String,
  baseURL: URL = URL(string: "https://api.cluestick.io")!,
  identityTokenProvider: (@Sendable () async throws -> String)? = nil
)

Call once at launch. baseURL only needs overriding if you are pointed at a non-production environment. See Identity for the token provider.

identify

public static func identify() async throws

Exchanges a token from your provider for an identified session, merging any anonymous history. Throws CluestickError.notConfigured if no provider was supplied.

setProfile

public static func setProfile(
  serviceIds: [String: String]? = nil,
  traits: [String: CluestickTraitValue]? = nil
)

reset

public static func reset()

Clears the stored session and starts a fresh anonymous one. Call at sign-out.

sdkVersion

public static let sdkVersion = "0.5.0"

Messaging

public static func send(
  _ message: String, attachments: [Attachment] = []
) async throws

public static func fetchMessages(
  after cursor: String? = nil
) async throws -> CluestickMessagePage

public static func fetchAllMessages(
  after cursor: String? = nil
) async throws -> CluestickMessagePage

public static var onNewMessages: (@MainActor @Sendable () -> Void)?

fetchMessages returns the current conversation; fetchAllMessages spans every conversation this user has had. Assign onNewMessagesto refresh your own UI — a badge, an unread dot — when a reply arrives. It is called on the main actor.

Message types

public struct CluestickMessage: Identifiable, Equatable, Sendable {
  public let id: String
  public let conversationId: String
  public let direction: CluestickMessageDirection
  public let author: String?
  public let body: String
  public let createdAt: Date
  public let attachments: [CluestickAttachment]
}

public enum CluestickMessageDirection: Sendable, Equatable {
  case inbound   // written on this device, by your user
  case outbound  // a reply from your team
}

public struct CluestickMessagePage: Equatable, Sendable {
  public let messages: [CluestickMessage]
  public let cursor: String?
  public let hasMore: Bool
}

Attachments

public struct Attachment: Sendable {
  public let filename: String
  public let contentType: String

  public init(fileURL: URL, filename: String, contentType: String)
  public init(data: Data, filename: String, contentType: String)
  public init?(fileURL: URL)  // infers filename and type from the extension
}

Send them with send(_:attachments:). Limits are 25 MB per file and 10 files per message. Recognised extensions include png, jpg, jpeg, gif, webp, heic, heif, mp4, mov, webm, pdf, txt, csv, json, zip, docx, xlsx and pptx.

Attachments you receive arrive on a message as:

public struct CluestickAttachment: Identifiable, Equatable, Sendable {
  public let id: String
  public let filename: String
  public let contentType: String
  public let byteSize: Int
  public let status: Status   // .pending or .ready
  public let url: URL?

  public var isImage: Bool
  public var isVideo: Bool
}

Feature board

public static func fetchFeatureRequests() async throws -> [CluestickFeatureRequest]
public static func vote(forFeatureRequest id: String) async throws
public static func removeVote(forFeatureRequest id: String) async throws

public struct CluestickFeatureRequest: Identifiable, Equatable, Sendable {
  public enum Status: String, Decodable, Equatable, Sendable {
    case underReview = "under_review"
    case planned
    case shipped
  }

  public let id: String
  public let title: String
  public let description: [CluestickBlock]
  public let status: Status
  public let target: String?
  public let voteCount: Int
  public let votedByMe: Bool
  public let createdAt: Date
}

Voting on a closed request throws CluestickError.votingClosed.

Help centre

public static func fetchHelpCenter() async throws -> [CluestickHelpCollection]
public static func fetchArticle(id: String) async throws -> CluestickArticle
public static func reportArticleViewed(id: String)

public struct CluestickHelpCollection: Identifiable, Equatable, Sendable {
  public let id: String
  public let name: String
  public let articles: [CluestickArticleSummary]
}

public struct CluestickArticleSummary: Identifiable, Hashable, Sendable {
  public let id: String
  public let title: String
  public let updatedAt: Date
  public let readMinutes: Int
}

public struct CluestickArticle: Identifiable, Equatable, Sendable {
  public let id: String
  public let title: String
  public let updatedAt: Date
  public let readMinutes: Int
  public let blocks: [CluestickBlock]
}

reportArticleViewed is fire-and-forget: it neither suspends nor throws, so it is safe to call from onAppear. CluestickArticleSummary is Hashable so it works directly as a SwiftUI navigation value.

Content blocks

public enum CluestickBlock: Equatable, Sendable {
  case paragraph(String)
  case heading(String)
  case list([String])
  case media(url: URL?, caption: String?)
}

Push notifications

public static func setPushToken(_ deviceToken: Data)
public static func isCluestickNotification(_ userInfo: [AnyHashable: Any]) -> Bool
public static func conversationId(fromNotification userInfo: [AnyHashable: Any]) -> String?

Hand setPushToken the token from didRegisterForRemoteNotificationsWithDeviceToken. Use isCluestickNotification to decide whether an incoming payload is yours before acting on it.

Trait values

public enum CluestickTraitValue: Encodable, Equatable, Sendable {
  case string(String)
  case int(Int)
  case double(Double)
  case bool(Bool)
}

It conforms to the string, integer, float and boolean literal protocols, which is why ["plan": "annual", "seats": 3] compiles without wrapping each value.

Errors

public enum CluestickError: Error, Equatable, Sendable {
  case notConfigured
  case unauthorized
  case invalidPayload(message: String)
  case notFound
  case votingClosed
  case server(statusCode: Int)
}

CluestickUI

Every symbol below requires iOS 16 or macOS 13. Each view supplies its own NavigationStack and Done button, so it is complete sheet content on its own.

Views

public struct CluestickChatView: View { public init() }
public struct CluestickFeatureBoardView: View { public init() }
public struct CluestickHelpCenterView: View { public init() }
public struct CluestickSupportView: View { public init() }

CluestickSupportView is the hub: search, a chat card showing the last reply and an unread dot, with articles and the board as pushed destinations. Use it when you want one entry point rather than three.

Sheet modifiers

public func cluestickChatSheet(isPresented: Binding<Bool>) -> some View
public func cluestickFeatureBoardSheet(isPresented: Binding<Bool>) -> some View
public func cluestickHelpCenterSheet(isPresented: Binding<Bool>) -> some View
public func cluestickSupportSheet(isPresented: Binding<Bool>) -> some View

Theming

public func cluestickTheme(_ theme: CluestickTheme) -> some View

public struct CluestickTheme: Sendable {
  public var accent: Color?
  public var fieldBackground: Color?
  public var cornerRadius: CGFloat
  public var headerTitle: String?
  public var headerSubtitle: String?

  public init(
    accent: Color? = nil,
    fieldBackground: Color? = nil,
    cornerRadius: CGFloat = 12,
    headerTitle: String? = nil,
    headerSubtitle: String? = nil
  )
}

The theme travels through the environment, so it applies to all four Cluestick surfaces and reaches views presented as sheets. Set it once, high up.

  • Leave accent and fieldBackground nilto inherit your app’s own tint and system materials.
  • headerTitle and headerSubtitlename the team the user thinks they are writing to — usually your product, not “Cluestick”.