validateAttestation(), you need to know whether it is actually working. This guide walks through what to look for on both the iOS Simulator and a physical device, what a successful response looks like, and how to diagnose common errors.
Physical device required for full attestation. The iOS Simulator cannot run App Attest because it has no Secure Enclave. If you are developing in the Simulator, see Simulator Setup to configure API key fallback first.
Quick verification checklist
Run through these five checks after your firstvalidateAttestation() call:
1
result.isValid is true
The most basic check. If
false, the device did not pass attestation.2
result.token is a non-empty string
A JWT was issued. It should begin with
eyJ (base64-encoded JSON header).3
riskScore is present (if on Pro or above)
On a physical device with a Pro+ plan, A score of
riskScore is a non-nil integer between 0 and 100. On the Free tier or in the Simulator, it will be nil.nil on a physical device with a Pro plan indicates an issue — see riskScore is nil on a real device below. riskCategory is populated on every plan, so use it when you only need the band.4
You got a real JWT, not the API key placeholder
In API key mode the SDK skips App Attest entirely and hands back the literal placeholder string If this is
simulator-dev-token instead of a JWT.true on a physical device, the SDK fell back to API key auth — check that your production initialization does not pass an apiKey.5
Your app appears in the Grantiva dashboard
Open Dashboard → Analytics → Devices. You should see a new attestation event. If nothing appears after 30 seconds, the request may not be reaching the server — check network errors below.
What the response looks like
Physical device (full attestation)
customClaims values arrive as strings, whatever type you configured in the dashboard.
Simulator (API key fallback)
The SDK does not contact the attestation endpoint at all in API key mode. It returns a synthetic result built on-device, so these values are fixed:Sample JWT payload
After decoding the token (Base64URL-decode the middle segment), you will see something like this:
Claims present on Pro and above:
Free-tier tokens carry only the always-present claims plus
custom_claims. The numeric risk_score is withheld, but riskCategory is still returned on the AttestationResult.
Custom claims — claims you configure in the dashboard are nested under the custom_claims object, not merged into the top level.
Risk score reference
The SDK’sRiskCategory has three cases. The dashboard splits the middle band further for its histograms, but the SDK and JWT only ever use these three:
Common errors and fixes
deviceNotSupported
Cause: App Attest is unavailable on this device — for example, hardware without a Secure Enclave or a region where App Attest is restricted.
Fix: Gate attestation-required flows so they aren’t reached on unsupported hardware, or fall back to a non-attested code path.
simulatorAPIKeyRequired
Cause: validateAttestation() was called in the iOS Simulator without an API key. App Attest is unavailable in the Simulator, so the SDK requires an API key fallback.
Fix: Pass an apiKey parameter during initialization for Simulator builds:
configurationError
Cause: The Team ID you passed to Grantiva(teamId:) does not match the Bundle ID of the running app, or the app is not registered in the Grantiva dashboard.
Fix:
- Verify your Team ID in the Apple Developer portal under Membership.
- Confirm the Bundle ID in Xcode matches what you registered in the dashboard under Apps.
- Check that the
teamIdstring has no extra whitespace.
networkError
Cause: The SDK could not reach api.grantiva.io. Common causes: no internet connection, App Transport Security (ATS) misconfiguration, VPN interference, or the challenge request timing out.
Fix:
- Confirm the device has internet access.
- Check that your
Info.plistdoes not block outbound HTTPS toapi.grantiva.io. - In development, check the device is not behind a proxy that intercepts TLS.
- The SDK retries automatically — if
networkErroris consistently thrown, check the status page.
validationFailed
Cause: The Grantiva server rejected the attestation. This usually means the attestation object was malformed, the challenge expired before the SDK submitted it, or the device’s App Attest key is corrupted.
Fix:
- Call
grantiva.clearStoredData()to wipe the cached key and token, then retry. - If the problem persists across retries on the same device, the device’s App Attest key may need to be regenerated — this happens automatically after
clearStoredData().
challengeExpired
Cause: Challenges are single-use and expire 5 minutes after issue. Pausing in the debugger mid-attestation, a badly skewed device clock, or a very slow network can outlive the challenge.
Fix: The SDK requests a fresh challenge and retries. If this error reaches your code, the retry also failed. Check for:
- Device clock sync issues (Settings → General → Date & Time → Set Automatically)
- A breakpoint sitting between
generateKeyand the/validatecall - Network latency pushing attestation submission past 5 minutes
rateLimited
Cause: Your tenant has exceeded the request rate limit, or this device has submitted too many attestation requests in a short window.
Fix: The SDK includes backoff-aware retry logic. If rateLimited surfaces to your code:
- Ensure you are not calling
validateAttestation()in a tight loop — let the SDK handle caching. - If you see this in production at scale, contact support to review your plan limits.
tokenExpired returned by your backend
Cause: The JWT is issued with a 1-hour TTL by default. Your backend received a token that has already expired. The SDK treats its cached token as expired 5 minutes early, so a genuinely expired token usually means the app held onto it rather than re-reading it.
Fix: Call refreshToken() before sending the token to your backend, or catch the expired state in your backend and prompt the client to re-attest:
riskScore is nil on a real device
Cause: One of:
- Your account is on the Free tier — the numeric score requires Pro or above.
riskCategoryis still populated. - The SDK is in API key mode, so no attestation happened.
- The result came from
refreshToken()on a still-valid token, which returns placeholder device intelligence rather than re-reading it from the server.
- Check your plan in Dashboard → Settings → Billing.
- Confirm
result.deviceIntelligence.riskScoreisnilspecifically (not0) — a score of0is a valid low-risk score, not an error. - Confirm
result.token != "simulator-dev-token"and that you are not passing anapiKeyon device builds. - Read device intelligence from a
validateAttestation()result, not arefreshToken()one.
Next steps
Backend JWT verification
Verify the token server-side and read device claims in Node.js, Python, or Go.
Error handling reference
Full list of GrantivaError cases with suggested handling.
Risk scoring concepts
How risk scores are computed and how to set thresholds.
Simulator setup
Configure API key fallback for development builds.