> ## 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.

# Validate Attestation

> Submit an attestation object for validation and receive a JWT token

<Note>This endpoint is called automatically by the SDK. You typically don't need to call it directly.</Note>

## Request

```
POST /api/v1/attestation/validate
```

### Headers

| Header        | Required | Description             |
| ------------- | -------- | ----------------------- |
| `X-Bundle-ID` | Yes      | App bundle identifier   |
| `X-Team-ID`   | Yes      | Apple Developer Team ID |

### Body

```json theme={null}
{
  "challenge": "a1b2c3d4e5f6...",
  "keyId": "device-key-id",
  "attestationObject": "base64-encoded-attestation",
  "clientDataHash": "base64-encoded-hash",
  "bundleId": "com.yourapp.example",
  "teamId": "ABBM6U9RM5",
  "deviceModel": "iPhone15,2",
  "osVersion": "18.0",
  "appVersion": "2.1.0",
  "appBuildNumber": "42",
  "platform": "iOS",
  "deviceFingerprint": "9f2b6c..."
}
```

| Field               | Type     | Required | Description                                                                                                                                                                                                                                                            |
| ------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `challenge`         | `string` | Yes      | Challenge from the challenge endpoint                                                                                                                                                                                                                                  |
| `keyId`             | `string` | Yes      | App Attest key identifier                                                                                                                                                                                                                                              |
| `attestationObject` | `string` | Yes      | Base64-encoded attestation object                                                                                                                                                                                                                                      |
| `clientDataHash`    | `string` | Yes      | Base64-encoded client data hash                                                                                                                                                                                                                                        |
| `bundleId`          | `string` | Yes      | App bundle identifier                                                                                                                                                                                                                                                  |
| `teamId`            | `string` | Yes      | Apple Team ID                                                                                                                                                                                                                                                          |
| `deviceModel`       | `string` | No       | Device model identifier                                                                                                                                                                                                                                                |
| `osVersion`         | `string` | No       | OS version                                                                                                                                                                                                                                                             |
| `appVersion`        | `string` | No       | App version                                                                                                                                                                                                                                                            |
| `appBuildNumber`    | `string` | No       | Build number                                                                                                                                                                                                                                                           |
| `platform`          | `string` | No       | Platform (iOS, macOS)                                                                                                                                                                                                                                                  |
| `deviceFingerprint` | `string` | No       | Stable hashed device fingerprint (sent by SDK 2.0.4+). Used as a secondary key for MAD counting so a device whose App Attest key is regenerated mid-month still counts once toward billing                                                                             |
| `subjectId`         | `string` | No       | Opaque sharing-unit id (family/household UUID) linking this device to a subscription entitlement. Blank values are treated as absent. Also accepted on `/refresh`, where omitting it keeps the existing link. See [Subscription Claims](/concepts/subscription-claims) |

## Response

```json theme={null}
{
  "isValid": true,
  "token": "eyJhbGciOi...",
  "expiresAt": "2025-03-11T12:00:00Z",
  "deviceIntelligence": {
    "deviceId": "abc123",
    "riskScore": 12,
    "riskCategory": "trusted",
    "deviceIntegrity": "valid",
    "jailbreakDetected": false,
    "attestationCount": 5,
    "lastAttestationDate": "2025-03-09T10:00:00Z"
  },
  "customClaims": {
    "user_tier": "premium"
  }
}
```

| Field                | Type      | Description                                                                                                                                                 |
| -------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `isValid`            | `boolean` | Whether the device passed attestation                                                                                                                       |
| `token`              | `string`  | Signed JWT token                                                                                                                                            |
| `expiresAt`          | `string`  | Token expiration (ISO 8601)                                                                                                                                 |
| `deviceIntelligence` | `object`  | Device risk and integrity data. `riskScore` is `null` on the Free tier — use `riskCategory`                                                                 |
| `customClaims`       | `object`  | Your configured custom claims, flattened to strings. Nested claim objects are stringified in this map — read structured claims from the decoded JWT instead |

## Errors

Error responses carry a stable `code`, a developer-readable `error` message, a `requestId` for support, and (where useful) a `hint`:

```json theme={null}
{
  "error": "Challenge expired — challenges are valid for 5 minutes. Request a new one from GET /api/v1/attestation/challenge.",
  "code": "challenge_expired",
  "hint": "See https://docs.grantiva.io/sdk/attestation#challenges for the challenge flow.",
  "requestId": "0AF2-4C61"
}
```

| Status | Code                         | Meaning                                                                                                                                                                  |
| ------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 400    | `challenge_invalid`          | Challenge not found or invalid                                                                                                                                           |
| 400    | `challenge_expired`          | Challenges are valid for 5 minutes and single-use                                                                                                                        |
| 400    | `challenge_already_used`     | Each challenge is single-use                                                                                                                                             |
| 400    | `attestation_object_invalid` | Not valid base64-encoded CBOR from `attestKey()`                                                                                                                         |
| 400    | `key_id_invalid`             | Key ID must come from `DCAppAttestService.generateKey()`                                                                                                                 |
| 400    | `client_data_hash_invalid`   | Must be a base64-encoded SHA256 hash of the challenge                                                                                                                    |
| 401    | `apple_validation_failed`    | Cryptographic validation failed — wrong Bundle/Team ID, reused or tampered attestation, or certificate chain failure                                                     |
| 402    | `quota_exceeded`             | MAD limit reached and grace period expired. Response includes `limit`, `current`, and `upgradeUrl`. Existing devices keep working — only new device onboarding is paused |
| 409    | `reattest_required`          | The device's stored key state has diverged from the server (key mismatch or signature failure). Drop local key state and re-attest — the SDK does this automatically     |
| 429    | —                            | Rate limited. Check `Retry-After` and the `X-RateLimit-*` headers                                                                                                        |

<Note>The SDK (2.0.3+) self-heals `reattest_required` automatically: it clears the cached keyId and re-runs the full attestation once. You only see this error if the retry also fails.</Note>
