License Types
The public types backing Octet.start(...) and OctetSdk.licenseStatus. See License & Activation for the timeline model.
LicenseStatus
Snapshot of the SDK's current license state. Read synchronously from OctetSdk.licenseStatus.
public struct LicenseStatus: Sendable, Equatable {
public let state: LicenseState
public let activatedAt: Date?
public let hardStopAt: Date?
public let daysUntilHardStop: Int?
public let tier: String
public let upgradeRecommended: Bool // soft version hint; false until backend gating is on
public let minSupportedVersion: String? // soft version hint; nil until backend gating is on
}
data class LicenseStatus(
val state: LicenseState,
val activatedAt: Instant?,
val hardStopAt: Instant?,
val daysUntilHardStop: Int?,
val tier: String,
val upgradeRecommended: Boolean, // soft version hint; false until backend gating is on
val minSupportedVersion: String?, // soft version hint; null until backend gating is on
)
| Field | Meaning |
|---|---|
state |
Coarse state that drives in-app UI. See LicenseState below. |
activatedAt |
First successful activation timestamp. nil pre-activation, or in INVALID. |
hardStopAt |
The license's exp. Octet maintains active licenses, so this value advances as the license renews. Do not design around a fixed cutoff. Identical for every device that activates this license. |
daysUntilHardStop |
ceil((hardStopAt - now) / 1 day). Useful for "X days left" banners. |
tier |
Tier carried in the license token. |
upgradeRecommended |
Soft version hint, added in 1.2. true when the backend reports this SDK version as behind the recommended floor. false until backend version gating is enabled. |
minSupportedVersion |
Soft version hint, added in 1.2. The lowest SDK version the backend supports, or nil/null until gating is enabled. Pair it with upgradeRecommended to prompt an in-app update. |
Android: in 3.0, LicenseStatus gained three trailing constructor parameters for Octet's internal use, each with a default. Source that calls its constructor or copy compiles unchanged, with positional or named arguments. Code compiled against 2.0.0 that calls the constructor or copy must be recompiled.
LicenseState
Octet maintains active licenses, so a license in normal use stays in ACTIVE. RENEWAL_RECOMMENDED, GRACE_PERIOD, EXPIRED and REVOKED cover prolonged-offline and revocation handling, not an age-based trial clock.
public enum LicenseState: String, Sendable {
case notActivated = "NOT_ACTIVATED"
case active = "ACTIVE"
case renewalRecommended = "RENEWAL_RECOMMENDED"
case gracePeriod = "GRACE_PERIOD"
case expired = "EXPIRED"
case revoked = "REVOKED"
case invalid = "INVALID"
}
enum class LicenseState {
NOT_ACTIVATED,
ACTIVE,
RENEWAL_RECOMMENDED,
GRACE_PERIOD,
EXPIRED,
REVOKED,
INVALID,
}
| State | Meaning |
|---|---|
NOT_ACTIVATED |
License verified, never activated. |
ACTIVE |
Activated and current. The normal steady state for a maintained license. |
RENEWAL_RECOMMENDED |
Activated, nearing the license exp. Maintained licenses renew automatically. Surface a banner only if you want to. |
GRACE_PERIOD |
Cached activation past its exp but within the offline tolerance. The device could not reach the backend to refresh. SDK keeps working. |
EXPIRED |
Cached activation past the offline tolerance with no backend reachable to refresh. SDK refused to start. Age alone no longer expires a maintained license. Since 3.1, also a license that expired while the session was running: predicates return INDETERMINATE with reason LICENSE_INACTIVE. |
REVOKED |
Added in 3.1. Octet withdrew the license while the session was running. Predicates return INDETERMINATE with reason LICENSE_INACTIVE and make no proof, and the next Octet.start throws a LicenseError. |
INVALID |
Signature failed, server rejected, or malformed. SDK refused to start. |
LicenseState drives in-app UI only. The activation token enforces the cryptographic gate, so a forged state = ACTIVE has no effect on it.
LicenseError
Thrown by Octet.start(...) for every license-related failure. The SDK guarantees one of these subtypes. It never throws a raw Error / Exception for license reasons.
public enum LicenseError: Error, Sendable, Equatable {
case malformedKey
case noActivation
case expired
case activationWindowClosed
case revoked
case network(message: String)
case serverRejected(httpStatus: Int, reason: String)
case upgradeRequired(minVersion: String, message: String)
case bootstrapFailed(reason: BootstrapReason)
}
sealed class LicenseError(message: String) : Exception(message) {
object MalformedKey : LicenseError(...)
object NoActivation : LicenseError(...)
object Expired : LicenseError(...)
object ActivationWindowClosed : LicenseError(...)
object Revoked : LicenseError(...)
data class Network(override val cause: Throwable) : LicenseError(...)
data class ServerRejected(val httpStatus: Int, val reason: String) : LicenseError(...)
data class UpgradeRequired(val minVersion: String, val message: String) : LicenseError(...)
data class BootstrapFailed(
val reason: BootstrapReason,
val upgradeUrl: String? = null, // set for NewDevicesBlocked
) : LicenseError(...)
}
| Case | When | Fix |
|---|---|---|
MalformedKey |
Local PASETO signature or structural failure on the license token the SDK obtained through attested bootstrap. | Confirm the SDK is a supported version. Contact support if it persists. |
NoActivation |
No cached activation, and offline (the SDK can't reach the license backend to bootstrap). | Retry when network returns. |
Expired |
Cached activation past the offline grace with the backend unreachable to refresh. | Reconnect so the SDK can re-activate. |
ActivationWindowClosed |
Server returned 403 activation_window_closed. Not returned under the current maintained-license model. Retained for compatibility. |
Contact support. |
Revoked |
Server returned 403 revoked (admin revoke). |
Contact support. |
Network |
Transient network failure during activation. message (iOS) or cause (Android) carries the underlying error for diagnostics. |
Retry. Do not parse the message for control flow. |
ServerRejected |
Any other HTTP rejection (app_blocked, ip_blocked, …). |
Inspect reason. |
UpgradeRequired |
Backend rejected this SDK version as out of support (version gating, added in 1.2). | Update the SDK to minVersion or later. |
BootstrapFailed |
Attested bootstrap failed. reason carries a BootstrapReason. Added in 2.0. |
See the BootstrapReason table below. |
BootstrapReason
Carried by bootstrapFailed. Each case except sandboxNotPermitted maps to a typed detail code the license backend returns for POST /v1/bootstrap, identically on both platforms. sandboxNotPermitted is raised by the SDK before any request is sent. See Attested bootstrap.
| Reason | Meaning |
|---|---|
attestationInvalid |
The App Attest object / key-attestation chain did not verify against the platform root. |
appNotRegistered |
The app identity is not registered to any account. |
appDisputed |
The app identity is contested and not yet resolved. |
accountDisabled |
The owning account is disabled. |
bootstrapReplay |
The bootstrap challenge was already used, or was not one the server issued. |
deviceLimitReached |
The license's device cap, set by Octet, is reached. Licenses have no cap by default. Devices that already activated are unaffected. |
newDevicesBlocked |
Added in 3.0. The account accepts no new devices. Devices that already activated are unaffected. The error carries a server-signed upgrade link, upgradeUrl. |
appPendingContested |
Added in 3.1. Another license claims the same app, and your pending registration lost that claim. Register the app again, or contact Octet support. |
pendingRateLimit |
A newly registered app is still rate-limited. |
seedInvalid |
The runtime seed (web / desktop channel) did not verify. |
sandboxTokenInvalid |
The sandbox bypass token did not verify. |
sandboxNotPermitted |
A sandbox token was presented from a store build. Raised locally by the SDK, never sent. |
Handling newDevicesBlocked
Send the user to upgradeUrl. The link is server-signed, so open it exactly as received and never build one yourself. On iOS the link is the case's associated value, .newDevicesBlocked(upgradeUrl:). On Android it is LicenseError.BootstrapFailed.upgradeUrl, set when reason is BootstrapReason.NewDevicesBlocked. The value is nil / null only if the server sent no link.
On Android, a when (reason) over BootstrapReason without an else branch no longer compiles against 3.0 or 3.1. Add the new cases or an else branch.
do {
let sdk = try await Octet.start(config: config)
} catch LicenseError.bootstrapFailed(reason: .newDevicesBlocked(let upgradeUrl)) {
if let link = upgradeUrl, let url = URL(string: link) {
await UIApplication.shared.open(url) // open exactly as received
}
}
try {
val sdk = Octet.start(context = applicationContext, config = config)
} catch (e: LicenseError.BootstrapFailed) {
when (e.reason) {
BootstrapReason.NewDevicesBlocked ->
e.upgradeUrl?.let { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it))) }
else -> showStartupError(e)
}
}
Reading the status at runtime
guard let status = sdk.licenseStatus else { return }
switch status.state {
case .renewalRecommended:
break // maintained licenses renew automatically
case .gracePeriod:
showBanner("Connect to the internet to refresh the license")
case .expired, .revoked, .invalid:
showBlocker()
case .active, .notActivated:
break // happy path
}
val status = sdk.licenseStatus ?: return
when (status.state) {
LicenseState.RENEWAL_RECOMMENDED -> { /* maintained licenses renew automatically */ }
LicenseState.GRACE_PERIOD ->
showBanner("Connect to the internet to refresh the license")
LicenseState.EXPIRED, LicenseState.REVOKED, LicenseState.INVALID ->
showBlocker()
LicenseState.ACTIVE, LicenseState.NOT_ACTIVATED -> {
// happy path
}
}
See also
- License & Activation. The timeline model and activation flow.
Octet.start(...). The call that throwsLicenseError.- Troubleshooting FAQ. What to do when activation fails.