PPactDocs
Mobile & SDKs

PactSDK for Swift (iOS, macOS, watchOS, tvOS)

Pure-Swift client for Pact's consent-native CRM. Async/await, white-label theming, offline writes, no third-party deps.

PactSDK is the official Swift package for Pact. It targets iOS 15+, macOS 12+, watchOS 8+, and tvOS 15+ and has zero third-party dependencies — just Foundation and URLSession — so you can drop it into a production app without inheriting an entire HTTP stack.

Install

In Xcode: File ▸ Add Package Dependencies… then enter the package URL:

code
https://github.com/deanjt/pact-sdk-ios

Or pin to the monorepo subpath as a Git submodule. In a Package.swift:

swift
.package(url: "https://github.com/deanjt/pact-sdk-ios", from: "0.1.0"),

Quickstart

swift
import PactSDK

let pact = PactClient(token: ProcessInfo.processInfo.environment["PACT_TOKEN"]!)
pact.onCost = { cost in
    print("call cost: \(cost.actualCents) cents (predicted: \(cost.predictedCents ?? 0))")
}

let accounts = try await pact.accounts.list(limit: 5)
print(accounts.data.data)
print("consent_filtered:", accounts.consentFiltered ?? false)

let briefing = try await pact.agents.fire(
    agentId: "daily_briefing",
    input: ["prompt": AnyCodable("My week ahead")]
)
print(briefing.data.status, "byok=\(briefing.data.byok ?? false)")

The full SwiftUI sample lives in packages/sdk-ios/Examples/PactQuickstart.

Auth + tenancy

swift
let pact = PactClient(
    token:   "pact_live_…",
    baseURL: URL(string: "https://api.pact.place")!,
    tenantID: "tenant_uuid"   // optional; required for service-level tokens
)
// Rotate the token from a SwiftUI lifecycle observer:
pact.setToken(newToken)

The same pact_live_* / pact_test_* keys (or OAuth access tokens) used by the JS SDK work here.

swift
let result = try await pact.agents.fire(agentId: "daily_briefing", input: [:])
switch result.data.status {
case "consent_blocked":
    // result.data.consentBlockedSubjects holds the contact ids that opted out.
    showConsentExplainer(for: result.data.consentBlockedSubjects ?? [])
case "ok":
    let cost = result.cost!
    if cost.charged == false {
        // BYOK — your tenant supplied the LLM credential, no Pact charge.
    }
default:
    break
}

Offline writes

swift
let storage = UserDefaultsQueueStorage()
let pact = PactClient(Options(token: "...", baseURL: ..., session: .shared))
let queue = OfflineQueue(storage: storage)   // persist across launches

// Switch the client offline on network loss; replays happen automatically:
pact.setOnline(false)
do {
    _ = try await pact.activities.log(.init(
        subjectType: "contact", subjectId: id, kind: "call",
        occurredAt: ISO8601DateFormatter().string(from: Date())))
} catch PactError.queuedOffline {
    // Mutation queued — surface a "Saved offline" toast.
}
pact.setOnline(true)            // drains the queue automatically

Real-time events

swift
let sub = pact.subscribeEvents(types: ["agent.run.completed"])
Task {
    for await frame in sub.frames {
        print(frame.type, frame.id)
    }
}
// later: sub.cancel()

White-label theming

swift
let theme = PactTheme.resolve(overrides: [
    "colors": ["accentEmber": tenant.brandColor]
])
// theme.colors.accentEmber, theme.spacing.p4, …
// Convert to UIColor:
let ember = PactColor.fromPactHex(theme.colors.accentEmber)

Compatibility notes

ConcernBehaviour
Swift concurrency modeBuilds clean under Swift 5 and Swift 6 strict mode.
SendableModels and theme structs conform; URLSession-derived caches are actor-isolated.
Background URL sessionsConfigure your own URLSession and pass it via Options.session.
CombineWrap an async call in Future if you need a publisher; first-party Combine bindings are on the roadmap.