github.com ↗
Installhttps://github.com/Pariette-Inc/signalbird.sdk

Signalbird - Swift (iOS, macOS)

It carries two clients:

Class What for Key Where
SignalbirdApp live chat + push registration for the end user sb_public_live_… (public) the iOS app
SignalbirdClient Radio - writing logs and events sb_secret_live_… (secret) server-side Swift (Vapor, Hummingbird)

A secret key is never embedded in an iOS app: the app bundle can be unpacked and the key ends up in everyone's hands. The mobile side does chat and push registration with the public key.

Install

Swift Package Manager - Xcode → File → Add Package Dependencies:

text
https://github.com/Pariette-Inc/signalbird.sdk

or Package.swift:

swift
dependencies: [
    .package(url: "https://github.com/Pariette-Inc/signalbird.sdk", from: "1.2.0"),
],
targets: [
    .target(name: "App", dependencies: [.product(name: "Signalbird", package: "signalbird.sdk")]),
]

iOS 15+, macOS 12+. It has no dependencies: Foundation/URLSession only.


Live chat

swift
import Signalbird

let client = try SignalbirdApp(config: .init(
    publicKey: "sb_public_live_…",
    chatKey: "destek",
    pushKey: "mobil"
))

chatKey is the chat channel's module key and pushKey the push channel's (the channel names you create in the panel; not secret). The SDK sends pushKey on device and push endpoints (registerDevice, unregisterDevice, reportPushOpened) and chatKey on chat endpoints, as the X-Signalbird-Module-Key header; if the relevant key is missing the API returns 400 MODULE_KEY_MISSING (2.10.0).

You get the app key from the panel (Inbox → Settings → Apps) or by calling createApp with the Management SDK.

Bootstrap

swift
let boot = try await client.bootstrap()

guard let app = boot.dictionary?["app"] as? [String: Any],
      app["chat_enabled"] as? Bool == true else {
    return   // chat is off for this app: draw nothing
}

bootstrap also returns colours, the greeting text, working hours and pre-chat form settings; read them from there if you want your interface to be manageable from the panel.

Session and conversation

swift
// The visitor session: the secret is stored, so history survives the next launch
try await client.startSession(["name": "Ayşe", "email": "ayse@example.com"])

// The first message opens the conversation. The quota is spent here: per conversation.
let started = try await client.startConversation(body: "Where is my parcel?")

let conversationID = (started.dictionary?["conversation"] as? [String: Any])?["id"] as? String ?? ""

try await client.sendMessage(conversationID: conversationID, body: "Order number: 1234")

Fetching new messages

swift
// The `after` cursor: only messages newer than this one come back
let updates = try await client.getConversation(conversationID, after: lastMessageID)

There is no WebSocket. Poll every 3 seconds while the panel is open and on a gradually widening interval when it is closed (20 → 60 → 180 s); skip a round while the app is in the background. This recovers by itself when the connection drops and does not burn battery.

Interactions

swift
try await client.setTyping(conversationID: conversationID, isTyping: true)
try await client.markRead(conversationID: conversationID, lastMessageID: lastID)
try await client.reactToMessage(conversationID: conversationID, messageID: messageID, emoji: "👍")

// Only their own message, and only within 15 minutes of sending it
try await client.editMessage(conversationID: conversationID, messageID: messageID, body: "Correction")
try await client.deleteMessage(conversationID: conversationID, messageID: messageID)

try await client.rateConversation(conversationID, rating: 5, comment: "Thank you")
try await client.closeConversation(conversationID)

Identity

swift
// When the user signs in, bind the visitor to the contact record
try await client.identify([
    "external_id": user.id,
    "email": user.email,
    "name": user.displayName,
    "attributes": ["plan": "pro"],
])

// On sign-out, clear the local identity; the record on the server stays
client.signOut()

Push (APNs)

swift
func application(_ application: UIApplication,
                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let token = deviceToken.map { String(format: "%02x", $0) }.joined()

    Task {
        try? await client.registerDevice(
            token: token,
            externalID: currentUser?.id,
            deviceName: UIDevice.current.name,
            appVersion: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
        )
    }
}

Asking for notification permission and obtaining the token is the app's job: when you ask for that permission is a product decision, and the App Store takes it seriously. The SDK only forwards the token.

On sign-out:

swift
try await client.unregisterDevice(token: token)   // the record is deactivated, not deleted

Storing the visitor secret

The default is UserDefaults. If you want Keychain, supply your own adapter:

swift
struct KeychainStorage: SignalbirdStorage {
    func get(_ key: String) -> String? { /* … */ }
    func set(_ key: String, _ value: String) { /* … */ }
    func remove(_ key: String) { /* … */ }
}

let client = try SignalbirdApp(config: .init(
    publicKey: "sb_public_live_…",
    chatKey: "destek",
    pushKey: "mobil",
    locale: "en",
    storage: KeychainStorage()
))

The stored record carries the app key too: if you rotate the key, the old identity is discarded automatically and a new session opens. Otherwise every call made with the old secret would take a 401 and the chat would die silently.


Radio (server-side Swift)

swift
let radio = try SignalbirdClient(config: .init(
    domainKey: ProcessInfo.processInfo.environment["SIGNALBIRD_DOMAIN_KEY"] ?? "",
    source: "api-01"
))

try await radio.critical("critical", "the payment service is not responding", [
    "service": "iyzico",
])

try await radio.batch([
    ["channel": "deploy", "message": "v2.4.0 is live", "level": "info"],
])

Five levels: .debug, .info, .warn, .error, .critical.


Result shape

swift
public struct SbResult {
    public let ok: Bool
    public let status: Int
    public let data: Any?
    public let code: String?
    public let message: String?

    public var dictionary: [String: Any]? { get }
    public var array: [[String: Any]]? { get }
    public func decode<T: Decodable>(_ type: T.Type) -> T?
}

To decode into your own model:

swift
struct ConversationResponse: Decodable {
    struct Conversation: Decodable { let id: String; let status: String }
    let conversation: Conversation
}

let parsed = updates.decode(ConversationResponse.self)

Version notice

The SDK reports its version on every request: X-Signalbird-Sdk: swift/<version>. When a newer version is released the API responds with Signalbird-Sdk-Status: outdated (or unsupported once it is no longer supported) and the SDK writes a warning once per process (print, DEBUG builds only). The warning is not an error: the request result does not change.

In the panel, Domains → [domain] → Keys shows which SDK version each key is used with; keys on an old version also get a banner at the top of the panel and a notification. To update, use File → Packages → Update to Latest Package Versions in Xcode.

Error codes

Code Meaning
NO_KEY / WRONG_KEY_TYPE The initialiser throws: a key from the wrong family
VISITOR_INVALID The secret is invalid; the local identity is cleared and the next call opens a new session
CHAT_UNAVAILABLE The quota is full or chat is off
MODULE_DISABLED Your package does not include the chat or push module
NETWORK_ERROR / TIMEOUT Network (status 0)

Apart from the key check in the initialiser, no method throws; the result is always the envelope.