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.
After your iOS app calls grantiva.validateAttestation(), it receives a signed JWT. Your backend should verify this token on every authenticated request to confirm the device passed attestation and to read device intelligence claims.
How it works
- Your app sends the JWT as a
Bearer token in the Authorization header
- Your backend fetches Grantiva’s public key from
/api/v1/attestation/public-key
- You verify the JWT signature and expiry locally (no network call per request)
- You extract device claims:
risk_score, risk_category, jailbreak status, and any custom claims
JWT Claims Reference
| Claim | Type | Description |
|---|
device_id | string | Unique device identifier |
risk_score | integer|null | 0–100 device risk score. null on Free tier |
risk_category | string | "trusted" (0–20), "suspicious" (21–75), or "blocked" (76–100) |
device_integrity | string | Integrity status string from Apple’s attestation |
jailbreak_detected | boolean | Whether jailbreak indicators were found |
attestation_count | integer | Total attestations from this device |
custom_claims | object | Your custom JWT claims configured in the dashboard |
exp | integer | Standard JWT expiry (Unix timestamp) |
iss | string | "grantiva" |
Node.js
Uses jose (ESM/CJS, works in Node.js and edge runtimes).
import { createRemoteJWKSet, jwtVerify } from 'jose'
const JWKS = createRemoteJWKSet(
new URL('https://api.grantiva.io/api/v1/attestation/public-key')
)
async function verifyGrantivaToken(token) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'grantiva',
algorithms: ['RS256'],
})
const riskScore = payload.risk_score // number | null
const riskCategory = payload.risk_category // "trusted" | "suspicious" | "blocked"
const jailbreak = payload.jailbreak_detected // boolean
const customClaims = payload.custom_claims // object | undefined
// Block high-risk devices
if (riskCategory === 'blocked') {
throw new Error('Device blocked due to high risk score')
}
return payload
}
// Express middleware example
async function requireAttestedDevice(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) return res.status(401).json({ error: 'Missing token' })
try {
req.deviceClaims = await verifyGrantivaToken(token)
next()
} catch (err) {
res.status(401).json({ error: 'Invalid or expired device token' })
}
}
Python
Uses PyJWT with cryptography for RS256.
pip install PyJWT[crypto] requests
import requests
import jwt
from jwt import PyJWKClient
JWKS_URL = "https://api.grantiva.io/api/v1/attestation/public-key"
# Reuse the client across requests — it caches the public key
jwks_client = PyJWKClient(JWKS_URL)
def verify_grantiva_token(token: str) -> dict:
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
issuer="grantiva",
)
risk_score = payload.get("risk_score") # int | None
risk_category = payload.get("risk_category") # "trusted" | "suspicious" | "blocked"
jailbreak = payload.get("jailbreak_detected", False)
custom_claims = payload.get("custom_claims", {})
# Block high-risk devices
if risk_category == "blocked":
raise ValueError(f"Device blocked: risk_score={risk_score}")
return payload
# FastAPI / Flask decorator example
from functools import wraps
from flask import request, jsonify, g
def require_attested_device(f):
@wraps(f)
def decorated(*args, **kwargs):
token = request.headers.get("Authorization", "").removeprefix("Bearer ")
if not token:
return jsonify({"error": "Missing token"}), 401
try:
g.device_claims = verify_grantiva_token(token)
except Exception as e:
return jsonify({"error": str(e)}), 401
return f(*args, **kwargs)
return decorated
Uses golang-jwt/jwt with JWKS fetching via MicahParks/keyfunc.
go get github.com/golang-jwt/jwt/v5
go get github.com/MicahParks/keyfunc/v3
package main
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"github.com/MicahParks/keyfunc/v3"
"github.com/golang-jwt/jwt/v5"
)
const jwksURL = "https://api.grantiva.io/api/v1/attestation/public-key"
type GrantivaClaims struct {
DeviceID string `json:"device_id"`
RiskScore *int `json:"risk_score"` // nil on Free tier
RiskCategory string `json:"risk_category"`
DeviceIntegrity string `json:"device_integrity"`
JailbreakDetected bool `json:"jailbreak_detected"`
AttestationCount int `json:"attestation_count"`
CustomClaims map[string]any `json:"custom_claims"`
jwt.RegisteredClaims
}
// Initialize once at startup; keyfunc handles key rotation automatically
var jwks keyfunc.Keyfunc
func init() {
var err error
jwks, err = keyfunc.NewDefault([]string{jwksURL})
if err != nil {
panic(fmt.Sprintf("failed to fetch JWKS: %v", err))
}
}
func VerifyGrantivaToken(tokenStr string) (*GrantivaClaims, error) {
claims := &GrantivaClaims{}
token, err := jwt.ParseWithClaims(tokenStr, claims, jwks.Keyfunc,
jwt.WithIssuedAt(),
jwt.WithIssuer("grantiva"),
jwt.WithValidMethods([]string{"RS256"}),
)
if err != nil || !token.Valid {
return nil, fmt.Errorf("invalid token: %w", err)
}
// Block high-risk devices
if claims.RiskCategory == "blocked" {
return nil, errors.New("device blocked due to high risk score")
}
return claims, nil
}
// HTTP middleware example
func RequireAttestedDevice(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
token := strings.TrimPrefix(auth, "Bearer ")
if token == "" {
http.Error(w, "missing token", http.StatusUnauthorized)
return
}
claims, err := VerifyGrantivaToken(token)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "device_claims", claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Reading custom claims
Custom claims you configure in the Grantiva dashboard appear in the custom_claims object:
// Node.js
const userTier = payload.custom_claims?.user_tier // "premium"
const region = payload.custom_claims?.region // "us-east"
# Python
user_tier = payload.get("custom_claims", {}).get("user_tier")
// Go
userTier, _ := claims.CustomClaims["user_tier"].(string)
Risk-based access control
// Allow, warn, or block based on risk category
switch (payload.risk_category) {
case 'trusted': // riskScore 0–20
break // full access
case 'suspicious': // riskScore 21–75
logSuspiciousDevice(payload.device_id)
break // allow but log
case 'blocked': // riskScore 76–100
return res.status(403).json({ error: 'Device blocked' })
}
Caching the public key
All three examples cache the JWKS response and refresh it automatically when key rotation occurs. Do not fetch the public key on every request — it adds latency and will trigger rate limiting.