Swift SDK
The official Swift SDK for LicenseSeat provides a comprehensive, type-safe API for managing software licenses on Apple platforms.
Current stable release: v0.4.2. Review and commit the resolved
Package.resolvedversion before shipping.
Important: Use only a restricted publishable client key scoped to
licenses:validate; assume any credential embedded in an app can be extracted. Never ship an administrator or server-write key. Keep end-user license keys out of URLs, logs, crash reports, analytics, and API responses.
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.2")
]
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 without logging the key or installation identifier
_ = try await LicenseSeatStore.shared.activate("USER-LICENSE-KEY")
print("License activated")
// 3. Check status
switch LicenseSeatStore.shared.status {
case .active:
print("License active")
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,
telemetryEnabled: true, // Optional diagnostics; enabled by default
offlineFallbackMode: .networkOnly, // Offline fallback strategy
maxOfflineDays: 7, // Maximum signed-token age
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 | Stable custom installation ID; otherwise a random app-scoped ID is persisted |
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) |
telemetryEnabled |
Bool |
true |
Include optional device/application telemetry on supported licensing POST requests |
offlineFallbackMode |
OfflineFallbackMode |
.networkOnly |
Offline fallback strategy |
offlineTokenRefreshInterval |
TimeInterval |
259200 (72 hours) |
Legacy offline-token refresh interval |
maxOfflineDays |
Int |
0 |
Maximum signed-token age and offline-authority enablement. 0 disables offline authority; enabled values are 1...36,600, and signed expiry still applies. |
maxClockSkewMs |
TimeInterval |
300000 (5 min) |
Clock tamper tolerance |
debug |
Bool |
false |
Enable debug logging |
Offline Fallback Modes
| Mode | Description |
|---|---|
.networkOnly |
Falls back only for transport failures, timeouts, and 5xx responses. Authentication, authorization, and malformed-request errors do not erase a valid cache; authoritative license-state errors do. Recommended. |
.always |
Attempts offline validation for nonterminal failures. Revoked, expired, suspended, not-active, and device-not-activated decisions remain authoritative. |
License Lifecycle
Activation
do {
_ = try await LicenseSeatStore.shared.activate("USER-LICENSE-KEY")
print("License activated")
} catch let error as APIError {
print("Activation failed: \(error.code ?? "unknown")")
}
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:
print("License active")
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.
Offline authority is disabled by default. Set maxOfflineDays to an explicit
value in 1...36,600 only after choosing the maximum outage window your
product accepts. Zero and out-of-range values fail closed; the signed token,
underlying license, and entitlement expirations remain additional upper bounds.
LicenseSeatStore.shared.configure(
apiKey: "pk_live_xxxxxxxx",
productSlug: "your-product"
) { config in
config.offlineFallbackMode = .networkOnly // Network-first fallback mode
config.maxOfflineDays = 7 // Maximum age from signed iat
config.offlineTokenRefreshInterval = 259200 // Refresh every 72 hours
}
How Offline Validation Works
- On activation, the SDK fetches a legacy 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
- Signed Expiry: The token and underlying license expiry are always enforced
- Explicit Offline Authority:
maxOfflineDaysmust be1...36,600and imposes an app-side age cap without extending signed expiry;0disables offline grants - Protected Cache: Apple platforms use
AfterFirstUnlockThisDeviceOnlyKeychain items and safely migrate 0.4.x plaintext state
Manual Offline Methods
// Sync offline assets (downloads the legacy token + signing key, caches them)
await LicenseSeat.shared.syncOfflineAssets()
// Verify cached offline token (use when offline)
let result = await LicenseSeat.shared.verifyCachedOffline()
// ValidationResponse with valid/code/license fields
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_not_activated |
Installation fingerprint has not been activated for this license |
product_mismatch |
License not valid for this product |
Telemetry
The SDK includes the following optional telemetry object by default on supported licensing POST requests:
| 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.
Disable the optional object when the host application does not need LicenseSeat analytics:
LicenseSeatStore.shared.configure(
apiKey: "pk_live_xxxxxxxx",
productSlug: "your-product"
) { config in
config.telemetryEnabled = false
}
Core licensing is not anonymous and is not disabled by this setting. Activation, validation, heartbeat, deactivation, and offline-asset requests still send the license/product context and the app-scoped installation fingerprint. The server also observes the source IP and records successful licensing activity. Swift 0.4.2 creates a random app-scoped installation identifier and protects it in Keychain on Apple platforms; it does not derive the default from a hardware UUID. Treat these persistent identifiers and interactions as potentially linked data in the host application's privacy policy and marketplace disclosures.
Platform Support
| Platform | Minimum Version | Notes |
|---|---|---|
| macOS | 12.0+ | Full support with a Keychain-protected app-scoped installation ID |
| iOS | 13.0+ | Full support |
| tvOS | 13.0+ | Full support |
| watchOS | 8.0+ | Core features (no Network.framework) |
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 cached licensing state and reset to the initial status:
LicenseSeatStore.shared.reset()
Reset intentionally retains the app-scoped installation identifier so a normal
reset/reactivation does not consume a new seat. Supply a deliberate custom
deviceIdentifier only when the application owns its persistence and migration
contract.
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