Skip to content

Tokens and errors

The SDK manages a per-install update token for you, and surfaces every failure as a typed SDKError. You rarely need to touch enrollment directly, but understanding the lifecycle helps you handle errors well.

Enrollment and the token lifecycle

The first time the SDK talks to the backend, it enrolls the install: it exchanges your app secret for a scoped, revocable update token. That token is persisted through a TokenStore (the Keychain by default on Apple platforms) and survives app launches.

Every backend-facing method auto-enrolls if there is no stored token, so you normally never call enroll() yourself:

  • checkForUpdate(...)
  • appMetadata()
  • reportInstall(...)
  • downloadURL(forRelease:)
  • downloadRelease(_:)
  • iconData(for:)

If the stored token is rejected (a uniform 401), the SDK transparently re-enrolls once and retries the call. From your code's point of view the call just succeeds. You can trigger these steps explicitly if you want to:

@discardableResult public func enroll() async throws -> SDKInstallToken
@discardableResult public func refresh() async throws -> SDKInstallToken

enroll() forces an enrollment; refresh() refreshes the current token. Both return the resulting SDKInstallToken.

The token store

public protocol TokenStore: Sendable {
    func load() throws -> SDKInstallToken?
    func save(_ token: SDKInstallToken) throws
    func clear() throws
}

By default the SDK stores the token in the Keychain, marked to persist after first unlock on that device only. It is not synced to iCloud and not included in backups. On platforms without a Keychain the default is an in-memory store. You can pass your own TokenStore to AppGantryUpdates(configuration:session:tokenStore:) if you need custom storage.

Anti-enumeration: the uniform 401

The /sdk/v1 surface returns a uniform 401 for both "unknown app" and "bad or expired token". The two cases are deliberately indistinguishable, so an attacker cannot probe which app keys exist. The SDK does not try to tell them apart either: on a 401 it simply re-enrolls and retries once. If enrollment itself is unauthorized, the call surfaces the error to you.

Errors: SDKError

Every failure is an SDKError. It is Sendable and Equatable, and it conforms to LocalizedError with a user-safe errorDescription.

public enum SDKError: Error, Sendable, Equatable {
    case notConfigured
    case notEnrolled
    case api(status: Int, body: APIErrorBody)
    case unexpectedStatus(Int)
    case decoding(String)
    case invalidURL(String)
    case transport(String)

    public var isUnauthorized: Bool // true when the api status is 401
}

Every /sdk/v1 failure body has the same shape, carried in the .api(status:body:) case:

public struct APIErrorBody: Codable {
    let error: String
    let message: String
}

Handle it like any typed Swift error:

do {
    let info = try await updates.checkForUpdate()
    // ...
} catch let error as SDKError {
    switch error {
    case .api(let status, let body):
        print("server said \(status): \(body.message)")
    case .transport(let detail):
        print("network problem: \(detail)")
    default:
        print(error.localizedDescription)
    }
}

isUnauthorized is a convenience for case .api(status: 401, _). In normal use you should rarely see a 401 reach you, because the SDK re-enrolls transparently; a 401 that survives that means enrollment itself was rejected (for example the app secret was revoked).

Rate limiting: honour 429 and back off

The edge WAF may throttle bursts of requests with an HTTP 429. The shipped client does not implement a retry loop for 429: it surfaces it as SDKError.api(status: 429, ...). When you see a 429, back off before retrying (respect any Retry-After guidance and avoid tight retry loops). Do not hammer checkForUpdate() on a timer.

Next

  • Configuration: the app secret that enrollment exchanges.
  • Privacy: what the SDK sends and where the token lives.
  • API reference: the /sdk/v1 endpoints behind these calls.