Installation and setup of the macOS SDK
Install
Xcode
In Xcode, choose File > Add Package Dependencies…, paste the URL below in the search field, and add the LicenseManagerKit library to your app’s target.
https://github.com/licenseman/licensemanager-swiftIt needs macOS 13 or later, and Swift 6 (Xcode 16 or later). Versions follow semantic versioning : from: "1.0.0" gets every 1.x release.
Create the license manager
Copy your product’s id and public key
Open Security in the sidebar: copy the public key, and the id of the product, like x7Kp2QaZ.
Create one LicenseManager for your app
import LicenseManagerKit
let licenseManager: LicenseManager = {
do {
return try LicenseManager(
productID: "x7Kp2QaZ",
publicKey: """
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA…
-----END PUBLIC KEY-----
"""
)
} catch {
fatalError("LicenseManager: \(error.localizedDescription)")
}
}()Create it once, when your app starts, and keep it for the app’s lifetime: a global, a property of your app delegate, or of your App. Creating it starts the validation of the stored key in the background.
In a sandboxed app, allow outgoing connections
In your target’s Signing & Capabilities, under App Sandbox, check Outgoing Connections (Client) (com.apple.security.network.client). Without it, every validation fails with network_error. See sandbox and distribution.
Then show your app, or the activation screen, depending on the state. With SwiftUI, see LicenseObserver.
Hard-code the public key and the product id in your source code. Never load them from a file next to your app, or fetch them at runtime: whoever replaces the public key can sign their own tokens, and your app would trust them. See the public key.
What happens on creation
LicenseManager.init returns right away. Then, in the background:
- It reads the stored key. No key: the state is
.missingKeyas soon asinitreturns, andready()returns it. - With a key and a valid stored token, the state becomes
.active, andready()returns it right away. The online validation runs in the background: when it refuses the license, the state changes. - With a key but no valid stored token, it validates the key online, retrying for up to 30 seconds, and
ready()returns once it’s done, with.activeor the reason the app is locked. - It starts the clock check, now and every hour.
init throws a ConfigError only for a mistake in your code: an empty product id (.missingProductID) or public key (.missingPublicKey), a public key that isn’t an Ed25519 key in PEM format (.invalidPublicKey), or no version (.missingVersion, see below). You’ll see it the first time you run the app. Everything else, a locked Keychain, no network, an unreadable machine id, is handled without failing.
Options
Most of the time, you only need the product id and the public key. The other options have defaults that fit most apps.
try LicenseManager(
productID: "x7Kp2QaZ", // both required
publicKey: publicKey,
options: .init(
fingerprint: .provider { readMyID() }, // default: the Mac's hardware UUID
version: "2.3.1", // default: CFBundleShortVersionString
machineName: "Jane's MacBook", // default: the computer name
platform: "macOS", // default: "macOS 26.5.1"
minOfflineDuration: 3 * 24 * 60 * 60, // default: zero, in seconds
keychainService: "com.acme.photo", // default: the bundle id
useKeychain: true // default: false for ad-hoc signed builds
)
)| Option | Default | What it does |
|---|---|---|
productID | Required | The id of your product. Checked in every token’s aud, and used to keep each product’s stored data apart. Shown on the Security page. |
publicKey | Required | Your organization’s Ed25519 public key, as the PEM shown on the Security page. Verifies every token. |
version | CFBundleShortVersionString | The version of your app, sent with every validation and checked offline against the token’s mv. |
machineName | The computer name | The name of the machine, shown to you and in the customer portal. |
platform | macOS and its version | The machine’s platform, like macOS 26.5.1. |
fingerprint | The hardware UUID | The machine part of the fingerprint: .value(String), or .provider { … }. See machine fingerprint. |
minOfflineDuration | Zero | How long, in seconds, your app must keep working offline after a renewal. Makes renewals happen earlier. See renewal. |
keychainService | The bundle id | The Keychain service the values are stored under, the name macOS shows when it asks for permission. See stored data. |
useKeychain | false for unsigned and ad-hoc signed builds, else true | Store the values in the Keychain, as well as in files. See development builds. |
apiBaseURL | https://api.licensemanager.dev | For tests only: another API origin. |
version
By default, it’s your app’s Version (CFBundleShortVersionString, MARKETING_VERSION in Xcode), like 2.3.1: the same in debug and release builds. Executables without an Info.plist, like swift run ones, have none: pass version, or init throws .missingVersion.
machineName and platform
Both are sent with every validation, and replace what LicenseManager knows about the machine. The computer name is the one in System Settings > General > Sharing, like “Jane’s MacBook Pro”, which is what makes machines easy to recognize in the customer portal. Pass your own value if you’d rather not send it.
One product per manager
A LicenseManager manages one product. An app selling several products, like an app and a paid plugin with their own licenses, can create one LicenseManager per product: their stored data and fingerprints are kept apart.
Development and release builds
Point your debug builds at a test product, and your release builds at the live one. Both share the same public key, which is one per organization:
#if DEBUG
let productID = "t3Hw9LmB" // test product
#else
let productID = "x7Kp2QaZ"
#endifA test key validated against the live product answers not_found, and the opposite too: the SDK shows .invalidKey.
Since both products share the public key, the product id is what stops a test key from unlocking your release build: the SDK checks it in every token. Make sure release builds can’t end up with the test product’s id, like from an environment variable or a user default a customer could set.
Network access
The SDK makes two kinds of requests, with URLSession, without cookies or cache:
| Request | When | Timeout |
|---|---|---|
POST https://api.licensemanager.dev/v1/products/{productId}/validate-key | Each validation | 10 seconds |
HEAD https://google.com, to read the time from its Date header | On launch, then every hour, for the clock check | 5 seconds |
Both are HTTPS, so App Transport Security needs no exception. On a network that blocks them, or without the sandbox’s network entitlement, the SDK handles it like being offline.