ExisOne Swift SDK
Native Swift package for integrating software licensing into macOS and iOS applications. Uses IOKit for hardware fingerprinting, Security.framework for offline RSA validation, and Swift concurrency (async/await) for all network operations.
Install
Add the package in Xcode:
- File → Add Package Dependencies...
- Enter the repository URL:
https://github.com/exisllc/ExisOne.Swift
- Select version 0.7.0 (or "Up to Next Major")
- Add ExisOne to your target
Or add it directly to your Package.swift:
dependencies: [
.package(url: "https://github.com/exisllc/ExisOne.Swift", from: "0.7.0")
]
// In your target:
.target(name: "MyApp", dependencies: [
.product(name: "ExisOne", package: "ExisOne.Swift")
])
Initialize
import ExisOne
// Simple initialization
let client = try ExisOneClient(
baseURL: "https://www.exisone.com",
accessToken: "exo_at_<public>_<secret>"
)
// Full options
let client = try ExisOneClient(options: ExisOneClientOptions(
baseURL: "https://www.exisone.com",
accessToken: "exo_at_<public>_<secret>",
offlinePublicKey: "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
allowedBaseURLHosts: ["www.exisone.com"],
timeout: 30
))
// SDK version
print(ExisOneClient.version) // "0.7.0"
baseURL can also be set via the EXISONE_BASEURL environment variable.
For offline license validation, set offlinePublicKey to your tenant's RSA public key (PEM format).
Obtain this from the Crypto Keys page.
Capabilities
- Hardware ID:
ExisOneHardwareId.generate()— IOKit-based fingerprint (salted SHA-256, 64-char hex) - Activate:
client.activate(...)→ActivationResult - Validate:
client.validate(...)→ValidationResultwith status, features, expiration - Deactivate:
client.deactivate(...) - Generate Key:
client.generateActivationKey(...)(requiresgeneratepermission) - Features:
client.getLicensedFeatures(...)→[String] - Support Ticket:
client.sendSupportTicket(...) - Offline Validation:
client.validateOffline(offlineCode:hardwareId:)— validates locally, no server neededclient.validateSmart(keyOrCode:hardwareId:productName:)— auto-detects online/offlineclient.deactivateSmart(keyOrCode:hardwareId:productName:)— opportunistic server sync
Quick Start
import ExisOne
let client = try ExisOneClient(
baseURL: "https://www.exisone.com",
accessToken: "exo_at_xxx_yyy"
)
// 1) Generate a hardware fingerprint (store locally)
let hwid = ExisOneHardwareId.generate()
// 2) Activate a license (user enters key & email)
let activation = await client.activate(
activationKey: "XXXX-XXXX-XXXX-XXXX",
email: "user@example.com",
hardwareId: hwid,
productName: "MyMacApp",
version: "1.0.0"
)
if !activation.success {
if activation.errorCode == "version_outdated" {
print("Please update to \(activation.minimumRequiredVersion ?? "latest")")
} else {
print(activation.errorMessage ?? "Activation failed")
}
}
// 3) Validate on app launch
let result = try await client.validate(
hardwareId: hwid,
productName: "MyMacApp",
activationKey: "XXXX-XXXX-XXXX-XXXX",
version: "1.0.0"
)
if result.isValid {
print("Licensed until: \(result.expirationDate ?? Date())")
print("Features: \(result.features)")
if result.features.contains("Pro") {
enableProFeatures()
}
} else {
print("Status: \(result.status)")
showActivationDialog()
}
// 4) Deactivate (same hardware required)
try await client.deactivate(
activationKey: "XXXX-XXXX-XXXX-XXXX",
hardwareId: hwid,
productName: "MyMacApp"
)
// 5) Generate a key (publisher tooling, requires generate permission)
let newKey = try await client.generateActivationKey(
productName: "MyMacApp",
email: "customer@example.com",
planId: 1,
validityDays: 365
)
// 6) Submit a support ticket
try await client.sendSupportTicket(
productName: "MyMacApp",
email: "user@example.com",
subject: "Help with activation",
message: "I'm having trouble activating..."
)
Hardware ID
The hardware fingerprint is generated using native macOS APIs with no subprocess calls:
- IOKit: Reads
IOPlatformUUID(the same Hardware UUID shown in System Information) - Network: Collects MAC addresses via
getifaddrs() - System: Architecture, CPU count, hostname, OS version
- Hash: SHA-256 with salt (
ExisOneHardwareSalt_v1)
let hwid = ExisOneHardwareId.generate() // "D7391D68656651079189364C9BB7D4642C97B5B05D10B44FD4CA5B8D4723A682"
The result is a stable 64-character uppercase hex string. The same Mac always produces the same ID.
Offline License Validation
For air-gapped environments, generate offline activation codes from the License Keys page using the customer's Hardware ID.
Setup
let client = try ExisOneClient(options: ExisOneClientOptions(
baseURL: "https://www.exisone.com",
accessToken: "your-token",
offlinePublicKey: """
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----
"""
))
Smart Validation (Recommended)
Auto-detects online vs offline keys. Falls back to offline if the server is unreachable.
let hwid = ExisOneHardwareId.generate()
// Works with both online keys (XXXX-XXXX-XXXX-XXXX) and offline codes
let result = await client.validateSmart(
keyOrCode: licenseKeyOrOfflineCode,
hardwareId: hwid,
productName: "MyMacApp"
)
if result.isValid {
print("Licensed until: \(result.expirationDate ?? Date())")
print("Offline mode: \(result.wasOffline)")
print("Features: \(result.features)")
} else {
print("Invalid: \(result.errorMessage ?? result.status)")
}
Direct Offline Validation
// Validate completely offline - no server calls
let result = client.validateOffline(
offlineCode: offlineCode,
hardwareId: hwid
)
if result.isValid {
print("Product: \(result.productName ?? "")")
print("Expires: \(result.expirationDate ?? Date())")
print("Features: \(result.features)")
} else if result.isExpired {
print("License expired")
} else if result.hardwareMismatch {
print("Wrong machine - license bound to different hardware")
} else {
print("Invalid: \(result.errorMessage ?? "Unknown error")")
}
SwiftUI Integration
Use the SDK in SwiftUI views with the .task modifier:
import SwiftUI
import ExisOne
struct ContentView: View {
@State private var licenseStatus = "Checking..."
@State private var features: [String] = []
var body: some View {
VStack {
Text(licenseStatus)
.font(.headline)
if features.contains("Pro") {
ProFeaturesView()
}
}
.task {
await checkLicense()
}
}
func checkLicense() async {
guard let client = try? ExisOneClient(
baseURL: "https://www.exisone.com",
accessToken: "exo_at_xxx_yyy"
) else { return }
let hwid = ExisOneHardwareId.generate()
let savedKey = UserDefaults.standard.string(forKey: "licenseKey") ?? ""
let result = await client.validateSmart(
keyOrCode: savedKey,
hardwareId: hwid,
productName: "MyMacApp"
)
if result.isValid {
licenseStatus = "Licensed"
features = result.features
} else {
licenseStatus = "Not licensed: \(result.status)"
}
}
}
macOS App Example
Typical integration pattern for a macOS application:
import ExisOne
class LicenseManager {
static let shared = LicenseManager()
private let client: ExisOneClient
private let hwid: String
private init() {
client = try! ExisOneClient(
baseURL: "https://www.exisone.com",
accessToken: "exo_at_xxx_yyy"
)
hwid = ExisOneHardwareId.generate()
}
/// Check license on app launch.
func validateOnLaunch() async -> ValidationResult {
let key = storedActivationKey()
return try! await client.validate(
hardwareId: hwid,
productName: "MyMacApp",
activationKey: key,
version: Bundle.main.appVersion
)
}
/// Activate with a new key.
func activate(key: String, email: String) async -> ActivationResult {
await client.activate(
activationKey: key,
email: email,
hardwareId: hwid,
productName: "MyMacApp",
version: Bundle.main.appVersion
)
}
private func storedActivationKey() -> String {
UserDefaults.standard.string(forKey: "activationKey") ?? ""
}
}
extension Bundle {
var appVersion: String {
infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
}
}
Error Handling
import ExisOne
do {
let result = try await client.validate(
hardwareId: hwid,
productName: "MyMacApp",
activationKey: key
)
} catch let error as ExisOneError {
switch error {
case .httpError(let code, let message):
print("Server error \(code): \(message)")
case .networkError(let message):
print("Network issue: \(message)")
case .insecureBaseURL:
print("HTTPS required")
default:
print(error.localizedDescription)
}
}
API Reference
// Version
static let version: String // "0.7.0"
// Hardware
static func generateHardwareId() -> String
// Activation
func activate(
activationKey: String,
email: String,
hardwareId: String,
productName: String,
version: String? = nil
) async -> ActivationResult
// Validation (Online)
func validate(
hardwareId: String,
productName: String? = nil,
activationKey: String? = nil,
version: String? = nil
) async throws -> ValidationResult
// Validation (Offline)
func validateOffline(
offlineCode: String,
hardwareId: String
) -> OfflineValidationResult
func validateSmart(
keyOrCode: String,
hardwareId: String,
productName: String? = nil
) async -> SmartValidationResult
// Deactivation
func deactivate(
activationKey: String,
hardwareId: String,
productName: String
) async throws -> Bool
func deactivateSmart(
keyOrCode: String,
hardwareId: String,
productName: String
) async -> DeactivationResult
// Generation (publisher)
func generateActivationKey(
productName: String,
email: String,
planId: Int? = nil,
validityDays: Int? = nil
) async throws -> String
// Features
func getLicensedFeatures(
activationKey: String
) async throws -> [String]
// Support
func sendSupportTicket(
productName: String,
email: String,
subject: String,
message: String
) async throws
Result Types
// All types conform to Sendable for safe use with Swift concurrency
struct ActivationResult {
let success: Bool
let errorCode: String?
let errorMessage: String?
let serverVersion: String?
let minimumRequiredVersion: String?
let licenseData: String?
}
struct ValidationResult {
let isValid: Bool
let status: String // "licensed", "trial", "expired", "invalid", "version_outdated"
let expirationDate: Date?
let features: [String]
let serverVersion: String?
let minimumRequiredVersion: String?
}
struct OfflineValidationResult {
let isValid: Bool
let errorMessage: String?
let productName: String?
let productId: Int
let expirationDate: Date?
let email: String?
let features: [String]
let version: String?
let isExpired: Bool
let hardwareMismatch: Bool
}
struct SmartValidationResult {
let isValid: Bool
let status: String
let expirationDate: Date?
let features: [String]
let wasOffline: Bool
let errorMessage: String?
let productName: String?
let serverVersion: String?
let minimumRequiredVersion: String?
}
struct DeactivationResult {
let success: Bool
let serverNotified: Bool
let errorMessage: String?
}
Environment Variables
| Variable | Description |
|---|---|
EXISONE_BASEURL | Default base URL if not specified in options |
Requirements
- Swift 5.9+
- macOS 12+ or iOS 15+
- Xcode 15+
- Zero external dependencies (uses only Apple frameworks: CryptoKit, Security, IOKit)
Common Errors
- 403 verify permission required: Add
verifyto the token in Access Tokens UI. - Activation key not found in this tenant: Token and key must belong to same tenant.
- 400 Bad Request on activation: Ensure product name is correct.
- version_outdated: Client version is below the minimum required. Update your app.
- ExisOneError.insecureBaseURL: Base URL must use
https://.
Changelog
- 0.7.0: Initial Swift SDK release with full feature parity to .NET, Python, and Node.js SDKs.