> ## Documentation Index
> Fetch the complete documentation index at: https://docs.grantiva.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Attestation

> Validate devices using Apple App Attest and receive signed JWT tokens

## Validate a device

```swift theme={null}
let result = try await grantiva.validateAttestation()
```

This performs the full attestation flow:

1. Requests a one-time challenge from the server
2. Generates or retrieves an attestation key via Apple's App Attest
3. Creates an attestation object on-device
4. Sends it to the Grantiva server for validation
5. Returns a signed JWT with device intelligence

### AttestationResult

```swift theme={null}
result.isValid              // Bool — whether the device passed
result.token                // String — signed JWT token
result.expiresAt            // Date — token expiration
result.deviceIntelligence   // DeviceIntelligence
result.customClaims         // [String: Any] — your custom JWT claims
```

### DeviceIntelligence

```swift theme={null}
result.deviceIntelligence.deviceId           // String — unique device ID
result.deviceIntelligence.riskScore          // Int? — 0–100 (Pro and above); nil on Free tier
result.deviceIntelligence.deviceIntegrity    // String — integrity status
result.deviceIntelligence.jailbreakDetected  // Bool
result.deviceIntelligence.attestationCount   // Int — total attestations from this device
result.deviceIntelligence.lastAttestationDate // Date?
```

## Token caching

The SDK automatically caches valid tokens. Calling `validateAttestation()` multiple times reuses the cached token until it expires, then performs a fresh attestation.

```swift theme={null}
// First call — full attestation flow
let result1 = try await grantiva.validateAttestation()

// Second call — returns cached token instantly
let result2 = try await grantiva.validateAttestation()
```

## Refresh tokens

Check if the current token is still valid, and refresh if expired:

```swift theme={null}
if let result = try await grantiva.refreshToken() {
    // Token is valid or was refreshed
    print(result.token)
}
```

Returns `nil` if no token has been stored yet (call `validateAttestation()` first).

## Check token status

```swift theme={null}
// Synchronous check — does a valid token exist?
if grantiva.isTokenValid() {
    let token = grantiva.getCurrentToken()
    // Use token in API requests
}
```

## Clear stored data

Force a fresh attestation on the next call:

```swift theme={null}
grantiva.clearStoredData()
```

This clears cached keys and tokens from the Keychain. Useful for testing or user logout.

## Use the token

Send the JWT to your backend as a Bearer token:

```swift theme={null}
var request = URLRequest(url: yourAPIEndpoint)
request.setValue("Bearer \(result.token)", forHTTPHeaderField: "Authorization")
```

Your backend can decode the JWT to read:

* 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_expired` and 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 `clientDataHash` sent to Apple must be the SHA256 of the exact challenge string. A mismatch surfaces as `client_data_hash_invalid`.

## Troubleshooting

Common causes when the server rejects an attestation (`apple_validation_failed`):

1. **Bundle ID / Team ID mismatch** — the values in the request don't match what App Attest signed against. Check that the SDK's `teamId` matches your Apple Developer Team ID and the build's bundle identifier is registered in the dashboard.
2. **Running in the Simulator** — App Attest only works on real hardware. Use [API key mode](/simulator-setup) for Simulator and CI builds.
3. **Reused or tampered attestation object** — attestation objects are single-use and bound to their challenge.
4. **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.
5. **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](/dashboard/billing).

## Error handling

```swift theme={null}
do {
    let result = try await grantiva.validateAttestation()
} catch GrantivaError.simulatorAPIKeyRequired {
    // Running in Simulator without an API key — see Simulator Setup
} catch GrantivaError.deviceNotSupported {
    // Hardware/region does not support App Attest
} catch GrantivaError.networkError(let error) {
    // Network issue — retry later
} catch GrantivaError.validationFailed {
    // Device failed attestation — potentially compromised
} catch GrantivaError.challengeExpired {
    // Challenge timed out — retry automatically
} catch {
    print(error.localizedDescription)
}
```

See [Error Handling](/sdk/error-handling) for the full list of error types.

## Backend verification

See the [Backend JWT Verification](/backend-verification) guide for examples of verifying the token server-side in Node.js, Python, and Go, including JWKS fetching and risk-based access control.
