Docs/API Reference/License Types

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 advances as the key 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.

LicenseState

Octet maintains active licenses, so a key in normal use stays in ACTIVE. RENEWAL_RECOMMENDED, GRACE_PERIOD, and EXPIRED 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 invalid            = "INVALID"
}
enum class LicenseState {
    NOT_ACTIVATED,
    ACTIVE,
    RENEWAL_RECOMMENDED,
    GRACE_PERIOD,
    EXPIRED,
    INVALID,
}
State Meaning
NOT_ACTIVATED License verified, never activated.
ACTIVE Activated and current. The normal steady state for a maintained key.
RENEWAL_RECOMMENDED Activated, nearing the license exp. Maintained keys 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 key.
INVALID Signature failed, server rejected, or malformed. SDK refused to start.
Caution

LicenseState drives in-app UI only. It never drives the cryptographic gate. That is enforced by the activation token. Forging state = ACTIVE accomplishes nothing.

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) : LicenseError(...)
}
Case When Fix
MalformedKey Local PASETO signature / structural failure on the license key. Re-copy the key.
NoActivation No cached activation, and offline (can't reach /v1/activate). 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 maps to a typed detail code the license backend returns for POST /v1/bootstrap, identically on both platforms. 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 account's device limit is reached.
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.

Reading the status at runtime

guard let status = sdk.licenseStatus else { return }
switch status.state {
case .renewalRecommended:
    showBanner("Renew within \(status.daysUntilHardStop ?? 0) days")
case .gracePeriod:
    showBanner("Grace period: renew before the SDK stops")
case .expired, .invalid:
    showBlocker()
case .active, .notActivated:
    break  // happy path
}
val status = sdk.licenseStatus ?: return
when (status.state) {
    LicenseState.RENEWAL_RECOMMENDED ->
        showBanner("Renew within ${status.daysUntilHardStop ?: 0} days")
    LicenseState.GRACE_PERIOD ->
        showBanner("Grace period: renew before the SDK stops")
    LicenseState.EXPIRED, LicenseState.INVALID ->
        showBlocker()
    LicenseState.ACTIVE, LicenseState.NOT_ACTIVATED -> {
        // happy path
    }
}

See also