Skip to Content
macOS (Swift)Installation and setup

Installation and setup of the macOS SDK

Install

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-swift

It 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

License.swift
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:

  1. It reads the stored key. No key: the state is .missingKey as soon as init returns, and ready() returns it.
  2. With a key and a valid stored token, the state becomes .active, and ready() returns it right away. The online validation runs in the background: when it refuses the license, the state changes.
  3. 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 .active or the reason the app is locked.
  4. 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 ) )
OptionDefaultWhat it does
productIDRequiredThe 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.
publicKeyRequiredYour organization’s Ed25519 public key, as the PEM shown on the Security page. Verifies every token.
versionCFBundleShortVersionStringThe version of your app, sent with every validation and checked offline against the token’s mv.
machineNameThe computer nameThe name of the machine, shown to you and in the customer portal.
platformmacOS and its versionThe machine’s platform, like macOS 26.5.1.
fingerprintThe hardware UUIDThe machine part of the fingerprint: .value(String), or .provider { … }. See machine fingerprint.
minOfflineDurationZeroHow long, in seconds, your app must keep working offline after a renewal. Makes renewals happen earlier. See renewal.
keychainServiceThe bundle idThe Keychain service the values are stored under, the name macOS shows when it asks for permission. See stored data.
useKeychainfalse for unsigned and ad-hoc signed builds, else trueStore the values in the Keychain, as well as in files. See development builds.
apiBaseURLhttps://api.licensemanager.devFor 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" #endif

A 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:

RequestWhenTimeout
POST https://api.licensemanager.dev/v1/products/{productId}/validate-keyEach validation10 seconds
HEAD https://google.com, to read the time from its Date headerOn launch, then every hour, for the clock check5 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.