Validate a device
- Requests a one-time challenge from the server
- Generates or retrieves an attestation key via Apple’s App Attest
- Creates an attestation object on-device
- Sends it to the Grantiva server for validation
- Returns a signed JWT with device intelligence
AttestationResult
DeviceIntelligence
RiskCategory has three cases and is present on every plan:
An unrecognized category string from the server decodes as
.trusted.
Token caching
The SDK caches the JWT in the Keychain. CallingvalidateAttestation() again returns the cached token until it is within 5 minutes of expiry, at which point the SDK gets a new one.
Renewal does not re-run the full attest flow. Once a key has been attested, the SDK refreshes by generating an App Attest assertion against a fresh challenge — App Attest permits only one attestation per key over its lifetime. If Apple or the server rejects that assertion (assertionKeyInvalid, reattestRequired), the SDK clears the local key state and runs one full attestation with a new key.
Refresh tokens
Check if the current token is still valid, and refresh if expired:nil if no token has been stored yet — call validateAttestation() first.
When the stored token is still valid,
refreshToken() returns it without contacting the server, and the deviceIntelligence on that result is a placeholder (riskScore is nil, deviceIntegrity is "valid"). Read device intelligence from the validateAttestation() result, not from refreshToken().Check token status
Clear stored data
Force a fresh attestation on the next call:Background activity
A successful attestation starts two background tasks:- Usage heartbeat — a fire-and-forget ping every 120 seconds while the app is running, used for Monthly Active Device counting.
- Flag streaming — a Server-Sent Events connection for live feature flag updates. On iOS the SDK stops it when the app backgrounds and restarts it on foreground.
clearStoredData().
Use the token
Send the JWT to your backend as a Bearer token:- Device risk score
- Jailbreak detection status
- Device model and OS version
- Custom claims configured in the dashboard
Challenges
Every attestation starts with a one-time challenge from the server. The SDK requests it automatically, but understanding the rules helps when debugging:- Challenges are valid for 5 minutes. A slow attestation flow (e.g. paused in the debugger) can outlive its challenge — the server returns
challenge_expiredand the SDK requests a fresh one. - Challenges are single-use. Replaying a challenge returns
challenge_already_used. Never cache or share challenges between attestation attempts. - The
clientDataHashsent to Apple must be the SHA256 of the exact challenge string. A mismatch surfaces asclient_data_hash_invalid.
Troubleshooting
Common causes when the server rejects an attestation (apple_validation_failed):
- Bundle ID / Team ID mismatch — the values in the request don’t match what App Attest signed against. Check that the SDK’s
teamIdmatches your Apple Developer Team ID and the build’s bundle identifier is registered in the dashboard. - Running in the Simulator — App Attest only works on real hardware. Use API key mode for Simulator and CI builds.
- Reused or tampered attestation object — attestation objects are single-use and bound to their challenge.
- Key state drift (
reattest_required, HTTP 409) — the device’s stored App Attest key no longer matches the server’s records (backup restore, device transfer). The SDK self-heals by clearing local key state and re-attesting; you’ll only see the error if that retry also fails. - MAD limit reached (HTTP 402) — new device onboarding is paused because your plan’s Monthly Active Device limit was exceeded and the grace period ended. See Billing.