Skip to main content
After adding the SDK and calling 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 first validateAttestation() 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, riskScore is a non-nil integer between 0 and 100. On the Free tier or in the Simulator, it will be nil.
A score of 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 simulator-dev-token instead of a JWT.
If this is 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:
Flag and feedback requests made in this mode authenticate with the API key rather than the token.

Sample JWT payload

After decoding the token (Base64URL-decode the middle segment), you will see something like this:
Claims always present: 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’s RiskCategory 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:
Get a development API key from Dashboard → Settings → API Keys. See Simulator Setup for the full fallback flow.

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:
  1. Verify your Team ID in the Apple Developer portal under Membership.
  2. Confirm the Bundle ID in Xcode matches what you registered in the dashboard under Apps.
  3. Check that the teamId string 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:
  1. Confirm the device has internet access.
  2. Check that your Info.plist does not block outbound HTTPS to api.grantiva.io.
  3. In development, check the device is not behind a proxy that intercepts TLS.
  4. The SDK retries automatically — if networkError is 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:
  1. Call grantiva.clearStoredData() to wipe the cached key and token, then retry.
  2. 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 generateKey and the /validate call
  • 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:
  1. Ensure you are not calling validateAttestation() in a tight loop — let the SDK handle caching.
  2. 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:
  1. Your account is on the Free tier — the numeric score requires Pro or above. riskCategory is still populated.
  2. The SDK is in API key mode, so no attestation happened.
  3. The result came from refreshToken() on a still-valid token, which returns placeholder device intelligence rather than re-reading it from the server.
Fix:
  1. Check your plan in Dashboard → Settings → Billing.
  2. Confirm result.deviceIntelligence.riskScore is nil specifically (not 0) — a score of 0 is a valid low-risk score, not an error.
  3. Confirm result.token != "simulator-dev-token" and that you are not passing an apiKey on device builds.
  4. Read device intelligence from a validateAttestation() result, not a refreshToken() 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.