Skip to main content
When a device passes attestation, Grantiva returns an RS256-signed JWT. On paid plans that token is not just a proof of attestation — it carries device intelligence: risk score, integrity level, jailbreak status, and attestation history, all signed. Your backend can read them without calling Grantiva. This page is the authoritative reference for what is actually in the token.
risk_score is a snapshot taken at attestation time, not a live value.Tokens are valid for 1 hour by default. A device that becomes suspicious after its token was issued keeps presenting a signed token carrying the old, lower score until that token expires and the device re-attests.Treat the claim as advisory. It is a good input for routine gating (rate limits, feature access, logging). For high-stakes actions — payments, transfers, credential changes, account recovery — require a fresh attestation (call refresh or re-attest) or check the device server-side, and do not rely on a token minted up to an hour ago.

The JWT and the attestation response are two different things

The POST /api/v1/attestation/validate response body and the signed token contain overlapping but different data. Mixing them up is the most common integration bug.
There is no risk_category claim in the JWT. It exists only in the attestation response body. If your backend reads payload.risk_category it will always be undefined. Derive the category from risk_score instead — the thresholds are on Risk Scoring.

Absent, not null

Claims that do not apply to your plan are omitted from the token entirely — they are not present with a null value. Check for absence, not for null:

Claim reference

Availability by plan. present · conditional, see the note for that claim · omitted from the token entirely.

Standard claims

Device identity

Device intelligence

device_model, os_version, and app_version are Enterprise-only in the token. They are collected on every paid plan and are visible in the dashboard and analytics API on Pro and Business — they are simply not projected into the JWT below Enterprise. If your Pro or Business backend needs the device model, read it from the analytics API, not the token.

Custom claims

Your configured claims are nested under custom_claims, preserving their JSON types. Grantiva’s managed subscription object also lives here. See Custom Claims and Subscription Claims. The key is omitted entirely when no claims are configured on Free, and is an empty object {} on paid plans when no claims evaluate to a value.

Decoded examples

Business plan

Note what is not there: no risk_category, no device_model, no country, no device_id.

Free plan

On Free the token proves attestation succeeded and nothing more. The risk category is still returned in the attestation response body as deviceIntelligence.riskCategory — your app has to forward it if your backend needs it, and it is not signed.

Issuer and audience

iss and aud are per-organization, not fixed Grantiva strings. The default issuer is app-attest-<your-organization-name-lowercased> and the default audience is your organization name exactly as it was registered. There is no self-serve editor for these today — if you need them changed, contact support@grantiva.io.
Do not hardcode issuer: "grantiva" in your verification code — no Grantiva token is issued with that value and verification will fail. Read the exact values from a real token — decode one from your own app and copy iss and aud verbatim — then pin them.
Pinning iss and aud matters: it is what stops a token minted for a different organization from being accepted by your backend.

device_id

device_id is not a reliable identifier and is populated inconsistently:
  • POST /attestation/validate — carries the device id supplied on the challenge request (GET /attestation/challenge?device_id=...). The iOS SDK does not send that parameter, so the claim is absent on the normal SDK attestation path.
  • POST /attestation/refresh — carries Grantiva’s internal device-profile UUID.
Use sub (the App Attest key id) as your per-device key. It is present on every token on every plan, and it is the identity Apple’s attestation actually binds.

Verifying the token server-side

Grantiva signs with RS256. Publish keys are at https://api.grantiva.io/.well-known/jwks.json (alias: GET /api/v1/attestation/public-key). Match the kid in the JWT header — not the kid claim in the payload — against the key set. Standard libraries do this for you. Cache the key set. It is served with Cache-Control: public, max-age=3600 and changes only on rotation.

Node.js

step-up means: ask the client for a fresh attestation (the SDK’s refresh) and re-run the gate, or require a second factor. It does not mean “allow anyway”.

Python

Both examples verify iss and aud and pin algorithms to RS256. Never call a decode function without an algorithm allowlist — an attacker-supplied alg: none or HMAC-with-the-public-key token would otherwise verify.

Claims you should not build on

These are present in some tokens but carry no dependable signal today. They are documented here so you do not discover that the hard way.