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:
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/v1endpoints behind these calls.