Swift SDK
The official Swift SDK for LicenseSeat provides a comprehensive, type-safe API for managing software licenses on Apple platforms.
Installation
You can install the Swift SDK using the SPM (Swift Package Manager) or through Xcode:
Swift Package Manager
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/licenseseat/licenseseat-swift.git", from: "0.4.1")
]
Then add the dependency to your target:
.target(
name: "YourApp",
dependencies: [
.product(name: "LicenseSeat", package: "licenseseat-swift")
]
)
Xcode
- File > Add Package Dependencies
- Enter:
https://github.com/licenseseat/licenseseat-swift.git - Select your version requirements and add to your target
Quick Start
import LicenseSeat
// 1. Configure at app launch
LicenseSeatStore.shared.configure(
apiKey: "pk_live_xxxxxxxx",
productSlug: "your-product" // Required
)
// 2. Activate a license
let license = try await LicenseSeatStore.shared.activate("USER-LICENSE-KEY")
print("Device ID: \(license.deviceId)")
// 3. Check status
switch LicenseSeatStore.shared.status {
case .active(let details):
print("Licensed! Device: \(details.device)")
case .inactive:
print("No license activated")
default:
break
}
Configuration
Basic Configuration
LicenseSeatStore.shared.configure(
apiKey: "pk_live_xxxxxxxx",
productSlug: "your-product"
)
Advanced Configuration
let config = LicenseSeatConfig(
apiBaseUrl: "https://licenseseat.com/api/v1", // v1 API endpoint
apiKey: "pk_live_xxxxxxxx",
productSlug: "your-product", // Required for all operations
storagePrefix: "myapp_",
autoValidateInterval: 3600, // Re-validate every hour
heartbeatInterval: 300, // Heartbeat every 5 minutes
maxRetries: 3,
retryDelay: 1,
offlineFallbackMode: .networkOnly, // Offline fallback strategy
maxOfflineDays: 7, // 7-day grace period
maxClockSkewMs: 300000, // 5-minute clock tolerance
debug: true
)
let licenseSeat = LicenseSeat(config: config)
Or using the builder-style API:
LicenseSeatStore.shared.configure(
apiKey: "pk_live_xxxxxxxx",
productSlug: "your-product"
) { config in
config.autoValidateInterval = 3600
config.heartbeatInterval = 300
config.offlineFallbackMode = .networkOnly
config.maxOfflineDays = 7
config.debug = true
}
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
apiBaseUrl |
String |
https://licenseseat.com/api/v1 |
v1 API endpoint |
apiKey |
String? |
nil |
Your publishable API key |
productSlug |
String? |
nil |
Required. Product identifier |
storagePrefix |
String |
licenseseat_ |
Prefix for cache keys |
deviceIdentifier |
String? |
Auto-generated | Custom device ID |
autoValidateInterval |
TimeInterval |
3600 (1 hour) |
Background validation interval |
heartbeatInterval |
TimeInterval |
300 (5 min) |
Standalone heartbeat interval (0 = disabled) |
networkRecheckInterval |
TimeInterval |
30 |
Offline connectivity check interval |
maxRetries |
Int |
3 |
API retry attempts |
retryDelay |
TimeInterval |
1 |
Base retry delay (exponential backoff) |
offlineFallbackMode |
OfflineFallbackMode |
.networkOnly |
Offline fallback strategy |
offlineTokenRefreshInterval |
TimeInterval |
259200 (72 hours) |
Legacy offline-token refresh interval |
maxOfflineDays |
Int |
0 |
Grace period for offline use |
maxClockSkewMs |
TimeInterval |
300000 (5 min) |
Clock tamper tolerance |
debug |
Bool |
false |
Enable debug logging |
Offline Fallback Modes
| Mode | Description |
|---|---|
.networkOnly |
Falls back to offline validation only for network errors (timeouts, connectivity issues, 5xx). Business logic errors (4xx) immediately invalidate. Recommended. |
.always |
Always attempts offline validation on any failure. |
License Lifecycle
Activation
do {
let license = try await LicenseSeatStore.shared.activate("USER-LICENSE-KEY")
print("Activated: \(license.licenseKey)")
print("Device ID: \(license.deviceId)")
print("Activation ID: \(license.activationId)")
} catch let error as APIError {
print("Activation failed: \(error.code ?? "unknown") - \(error.message)")
}
With options:
let license = try await LicenseSeatStore.shared.activate(
"USER-LICENSE-KEY",
options: ActivationOptions(
deviceId: "custom-device-id",
deviceName: "User's MacBook Pro",
metadata: ["version": "2.0.0", "environment": "production"]
)
)
Deactivation
try await LicenseSeatStore.shared.deactivate()
Validation
let result = try await LicenseSeatStore.shared.validate(licenseKey: "USER-LICENSE-KEY")
if result.valid {
print("License is valid")
print("Plan: \(result.license.planKey)")
print("Status: \(result.license.status)")
// Check entitlements from validation
for entitlement in result.license.activeEntitlements {
print("Entitlement: \(entitlement.key)")
if let expiresAt = entitlement.expiresAt {
print(" Expires: \(expiresAt)")
}
}
} else {
print("Invalid: \(result.code ?? "unknown")")
print("Message: \(result.message ?? "")")
}
Status Checking
Get Current Status
switch LicenseSeatStore.shared.status {
case .inactive(let message):
print("No license: \(message)")
showActivationScreen()
case .pending(let message):
print("Validating: \(message)")
showLoadingIndicator()
case .active(let details):
print("Licensed to: \(details.license)")
print("Device: \(details.device)")
enableFeatures()
case .offlineValid(let details):
print("Valid offline until next sync")
enableFeatures()
showOfflineBanner()
case .invalid(let message):
print("Invalid: \(message)")
showErrorScreen(message)
case .offlineInvalid(let message):
print("Expired offline: \(message)")
showRenewalScreen()
}
Status Types
| Status | Description |
|---|---|
.inactive |
No license activated |
.pending |
License pending validation |
.active |
License is valid (online) |
.offlineValid |
License is valid (offline check) |
.invalid |
License is invalid |
.offlineInvalid |
License invalid (offline, e.g., expired) |
Entitlements
Check feature access based on license entitlements:
let status = LicenseSeatStore.shared.entitlement("premium-features")
switch status.reason {
case nil where status.active:
enablePremiumFeatures()
case .expired:
showRenewalPrompt(expiresAt: status.expiresAt)
case .notFound:
showUpgradePrompt()
case .noLicense:
showActivationPrompt()
default:
disablePremiumFeatures()
}
// Access entitlement details
if let entitlement = status.entitlement {
print("Entitlement key: \(entitlement.key)")
if let metadata = entitlement.metadata {
print("Metadata: \(metadata)")
}
}
Reactive Entitlement Monitoring
LicenseSeatStore.shared.entitlementPublisher(for: "api-access")
.receive(on: DispatchQueue.main)
.sink { status in
apiAccessEnabled = status.active
if let expiresAt = status.expiresAt {
scheduleExpirationWarning(at: expiresAt)
}
}
.store(in: &cancellables)
SwiftUI Integration
Property Wrappers
The SDK provides property wrappers for reactive SwiftUI apps:
import SwiftUI
import LicenseSeat
@main
struct MyApp: App {
init() {
LicenseSeatStore.shared.configure(
apiKey: ProcessInfo.processInfo.environment["LICENSESEAT_API_KEY"] ?? "",
productSlug: "my-app"
)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@LicenseState private var status // Auto-updates on license changes
@EntitlementState("pro") private var hasPro // Feature flag
var body: some View {
switch status {
case .active, .offlineValid:
MainAppView()
.environment(\.proEnabled, hasPro)
case .inactive:
ActivationView()
case .invalid(let message):
ErrorView(message: message)
case .pending:
ProgressView("Validating...")
case .offlineInvalid:
ExpiredView()
}
}
}
struct ActivationView: View {
@State private var licenseKey = ""
@State private var isLoading = false
@State private var error: String?
var body: some View {
Form {
TextField("License Key", text: $licenseKey)
Button("Activate") {
Task {
isLoading = true
defer { isLoading = false }
do {
try await LicenseSeatStore.shared.activate(licenseKey)
} catch let apiError as APIError {
self.error = apiError.message
} catch {
self.error = error.localizedDescription
}
}
}
.disabled(licenseKey.isEmpty || isLoading)
if let error {
Text(error).foregroundColor(.red)
}
}
}
}
UIKit / AppKit Integration
Use Combine publishers for reactive updates:
import LicenseSeat
import Combine
class LicenseManager: ObservableObject {
@Published var isLicensed = false
@Published var hasProFeatures = false
private var cancellables = Set<AnyCancellable>()
init() {
LicenseSeatStore.shared.configure(
apiKey: "pk_live_xxxxxxxx",
productSlug: "your-product"
)
// React to license status changes
LicenseSeatStore.shared.$status
.receive(on: DispatchQueue.main)
.sink { [weak self] status in
switch status {
case .active, .offlineValid:
self?.isLicensed = true
default:
self?.isLicensed = false
}
}
.store(in: &cancellables)
// Monitor specific entitlements
LicenseSeatStore.shared.entitlementPublisher(for: "pro-features")
.map { $0.active }
.receive(on: DispatchQueue.main)
.assign(to: &$hasProFeatures)
}
func activate(_ key: String) async throws {
try await LicenseSeatStore.shared.activate(key)
}
func deactivate() async throws {
try await LicenseSeatStore.shared.deactivate()
}
}
Event System
Subscribe to SDK events for analytics, UI updates, or custom logic:
// Subscribe with closure (returns AnyCancellable)
let cancellable = licenseSeat.on("activation:success") { data in
print("License activated!")
Analytics.track("license_activated")
}
// Unsubscribe by cancelling
cancellable.cancel()
// Or use Combine publishers
licenseSeat.eventPublisher
.filter { $0.name.hasPrefix("validation:") }
.sink { event in
switch event.name {
case "validation:success":
updateUI()
case "validation:offline-success":
showOfflineBanner()
case "license:revoked":
lockFeatures()
default:
break
}
}
.store(in: &cancellables)
Available Events
| Event | Description |
|---|---|
activation:start/success/error |
License activation lifecycle |
validation:start/success/failed/error |
Online validation |
validation:offline-success/offline-failed |
Offline validation |
deactivation:start/success/error |
License deactivation |
license:loaded |
Cached license loaded at startup |
license:revoked |
License revoked by server |
offlineToken:verified |
Legacy offline token signature verified |
offlineToken:verificationFailed |
Legacy offline token verification failed |
heartbeat:success |
Heartbeat acknowledged by server |
heartbeat:error |
Heartbeat request failed |
autovalidation:cycle |
Auto-validation cycle triggered |
network:online/offline |
Connectivity changes |
sdk:reset |
SDK state cleared |
Offline Validation
The SDK provides seamless offline support with Ed25519 cryptographic verification:
Note: The current Swift SDK offline flow still uses signed offline tokens. Machine files are the newer preferred offline artifact at the API level, but the Swift SDK has not migrated to them yet. Treat offline tokens as the current Swift implementation detail, not the long-term product direction.
LicenseSeatStore.shared.configure(
apiKey: "pk_live_xxxxxxxx",
productSlug: "your-product"
) { config in
config.offlineFallbackMode = .networkOnly // Network-first fallback mode
config.maxOfflineDays = 7 // 7-day grace period
config.offlineTokenRefreshInterval = 259200 // Refresh every 72 hours
}
How Offline Validation Works
- On activation, the SDK fetches a signed offline token from the server
- The token contains license data, entitlements, and an Ed25519 signature
- When offline, the SDK verifies the signature locally
- Clock tamper detection prevents users from bypassing expiration
Security Features
- Ed25519 Signatures: Offline licenses are cryptographically signed
- Clock Tamper Detection: Detects system clock manipulation
- Constant-Time Comparison: Prevents timing attacks on license keys
- Grace Period: Configurable offline validity period
Manual Offline Methods
// Sync offline assets (downloads the legacy token + signing key, caches them)
try await LicenseSeat.shared.syncOfflineAssets()
// Verify cached offline token (use when offline)
let result = try await LicenseSeat.shared.verifyCachedOffline()
// ValidationResult with valid, offline, license, activation
API Response Format
The v1 API uses Stripe-style conventions with object fields identifying response types.
Activation Response
{
"object": "activation",
"id": 12345,
"fingerprint": "mac_abc123",
"device_name": "User's MacBook",
"license_key": "LICENSE-KEY",
"activated_at": "2025-01-15T10:30:00Z",
"license": {
"object": "license",
"key": "LICENSE-KEY",
"status": "active",
"mode": "hardware_locked",
"plan_key": "pro",
"seat_limit": 5,
"active_seats": 1,
"active_entitlements": [
{"key": "premium", "expires_at": null, "metadata": null}
],
"product": {"slug": "my-app", "name": "My App"}
}
}
Deactivation Response
{
"object": "deactivation",
"activation_id": 12345,
"deactivated_at": "2025-01-15T12:00:00Z"
}
Error Handling
do {
try await LicenseSeatStore.shared.activate("INVALID-KEY")
} catch let error as APIError {
print("Error code: \(error.code ?? "unknown")")
print("Message: \(error.message)")
print("Details: \(error.details ?? [:])")
}
Common Error Codes
| Code | Description |
|---|---|
license_not_found |
License key doesn't exist |
license_expired |
License has expired |
license_suspended |
License has been suspended |
seat_limit_exceeded |
No available seats |
device_mismatch |
Device ID doesn't match activation |
product_mismatch |
License not valid for this product |
Telemetry
The SDK automatically collects and sends the following telemetry fields on every API call:
| Field | Source |
|---|---|
sdk_name |
Always swift |
sdk_version |
LicenseSeatConfig.sdkVersion |
os_name |
macOS, iOS, tvOS, watchOS, visionOS |
os_version |
ProcessInfo.operatingSystemVersion |
platform |
native |
device_model |
sysctlbyname("hw.model") |
app_version |
CFBundleShortVersionString |
app_build |
CFBundleVersion |
device_type |
desktop, phone, tablet, watch, tv, headset |
architecture |
arm64 or x64 (compile-time) |
cpu_cores |
ProcessInfo.processorCount |
memory_gb |
ProcessInfo.physicalMemory (rounded to nearest GB) |
locale |
Locale.current.identifier |
language |
2-letter code extracted from locale |
timezone |
TimeZone.current.identifier |
screen_resolution |
Native pixel resolution (macOS/iOS only) |
display_scale |
NSScreen.backingScaleFactor / UIScreen.scale |
See Telemetry for the full field reference.
Platform Support
| Platform | Minimum Version | Notes |
|---|---|---|
| macOS | 12.0+ | Full support including hardware UUID |
| iOS | 13.0+ | Full support |
| tvOS | 13.0+ | Full support |
| watchOS | 8.0+ | Core features (no Network.framework) |
Testing Authentication
Test API key authentication and API health:
do {
let result = try await LicenseSeatStore.shared.testAuth()
print("Authenticated: \(result.authenticated)") // true if API key is valid
print("Healthy: \(result.healthy)") // API health status
print("API Version: \(result.apiVersion)") // e.g., "1.0.0"
} catch {
print("Auth test failed: \(error)")
}
AuthTestResult Properties
| Property | Type | Description |
|---|---|---|
authenticated |
Bool |
Whether the API key is valid |
healthy |
Bool |
Whether the API is healthy |
apiVersion |
String |
The API version (e.g., 1.0.0) |
Debug Report
Generate a diagnostic report for support:
let report = LicenseSeatStore.shared.debugReport()
print(report)
// Contains: SDK version, status, license key prefix (redacted), timestamps, etc.
Reset SDK
Clear all cached data and reset to initial state:
LicenseSeatStore.shared.reset()
Next Steps
- JavaScript SDK - For web applications
- Offline Licensing Guide - In-depth offline documentation
- Air-Gapped Licensing - For devices without any internet
- API Reference - Direct API access