Skip to content

Native App Attestation

Native app attestation allows mobile apps to bypass rate limiting by proving the request originates from a genuine device running your authentic app. Native apps must use device attestation rather than CAPTCHA when encountering rate limits.

Why Use Attestation?

BenefitDescription
Seamless UXRate limits bypassed automatically without user interaction
Native experienceNo web views or external verification flows
Invisible to usersNo “unusual activity” warnings or challenges

Supported Methods

MethodPlatformEnum Value
Apple DeviceCheckiOSAPPLE_DEVICE_CHECK
Play IntegrityAndroidANDROID_PLAY_INTEGRITY

Integration Flow

The flow differs slightly by platform. Apple DeviceCheck does not use a server-issued nonce — the device generates a token directly. Android Play Integrity uses a nonce provided by Horizon via the siteKey field.

iOS (Apple DeviceCheck)

┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Your App │ │ Device OS │ │ Horizon API │
└────────┬────────┘ └────────┬─────────┘ └────────┬────────┘
│ │ │
│ 1. API request │
│─────────────────────────────────────────────────>│
│ │ │
│ 2. Rate limit triggered (siteKey: null) │
│<─────────────────────────────────────────────────│
│ │ │
│ 3. Request device │ │
│ token (no nonce) │ │
│───────────────────────>│ │
│ │ │
│ 4. Device generates │ │
│ token │ │
│<───────────────────────│ │
│ │ │
│ 5. Retry with attestation headers │
│─────────────────────────────────────────────────>│
│ │ │
│ 6. Success (rate limit bypassed) │
│<─────────────────────────────────────────────────│

Android (Play Integrity)

┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Your App │ │ Device OS │ │ Horizon API │
└────────┬────────┘ └────────┬─────────┘ └────────┬────────┘
│ │ │
│ 1. API request │
│─────────────────────────────────────────────────>│
│ │ │
│ 2. Rate limit triggered (siteKey: nonce) │
│<─────────────────────────────────────────────────│
│ │ │
│ 3. Request integrity │ │
│ token with nonce │ │
│───────────────────────>│ │
│ │ │
│ 4. Device generates │ │
│ signed token │ │
│<───────────────────────│ │
│ │ │
│ 5. Retry with attestation headers │
│─────────────────────────────────────────────────>│
│ │ │
│ 6. Success (rate limit bypassed) │
│<─────────────────────────────────────────────────│

Detecting Rate Limits

When rate limiting triggers, the GraphQL response includes attestation options:

{
"data": {
"login": null
},
"errors": [
{
"message": "Rate limit exceeded",
"extensions": {
"code": "RATE_LIMITED"
}
}
],
"extensions": {
"rateLimitersFiring": [
{
"rateLimitingBucket": "LOGIN",
"captchaBypassAvailable": [
{
"type": "APPLE_DEVICE_CHECK",
"siteKey": null
},
{
"type": "ANDROID_PLAY_INTEGRITY",
"siteKey": "a1b2c3d4-nonce-value"
}
]
}
]
}
}
FieldDescription
rateLimitingBucketWhich rate limiter was triggered
captchaBypassAvailableAvailable verification methods
typeThe attestation type
siteKeyNonce for Android Play Integrity. Always null for Apple DeviceCheck (not required)

Submitting Attestation

Include these headers when retrying a rate-limited request:

HeaderValue
X-Captcha-TypeAPPLE_DEVICE_CHECK or ANDROID_PLAY_INTEGRITY
X-Captcha-ResponseBase64-encoded token from device API
// Mobile API with attestation
fetch('https://api.thehut.net/myprotein/en/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Captcha-Type': 'ANDROID_PLAY_INTEGRITY',
'X-Captcha-Response': attestationToken,
'Authorization': 'Opaque ' + authToken
},
body: JSON.stringify({ query, variables })
});

iOS: Apple DeviceCheck

Horizon uses Apple’s DeviceCheck framework (DCDevice), not the App Attest framework (DCAppAttestService). The device generates a token directly — no server-issued nonce, no clientDataHash, and no key registration is involved.

import DeviceCheck
func generateDeviceToken() async throws -> String {
guard DCDevice.current.isSupported else {
throw AttestationError.deviceCheckUnavailable
}
let tokenData = try await DCDevice.current.generateToken()
return tokenData.base64EncodedString()
}

Send the token as the X-Captcha-Response header. A fresh token is required per attempt — Horizon enforces single-use.

Prerequisites:

  • iOS 11.0 or later required
  • Physical device only (DeviceCheck is not available in the iOS Simulator)

Android: Play Integrity API

Add the Play Integrity dependency:

dependencies {
implementation 'com.google.android.play:integrity:1.3.0'
}

Generate integrity tokens using the nonce from the siteKey field:

import com.google.android.play.core.integrity.IntegrityManagerFactory
import com.google.android.play.core.integrity.IntegrityTokenRequest
suspend fun generateIntegrityToken(nonce: String): String {
val integrityManager = IntegrityManagerFactory.create(context)
val request = IntegrityTokenRequest.builder()
.setNonce(nonce)
.build()
val response = integrityManager
.requestIntegrityToken(request)
.await()
return response.token()
}

Prerequisites:

  • Link app in Google Play Console
  • Enable Play Integrity API in Google Cloud Console

Error Handling

FailurePlatformCauseResolution
Invalid token formatBothMalformed or corrupted tokenRegenerate token
Nonce mismatchAndroidWrong nonce usedUse siteKey from rate limit response
Expired nonceAndroidToken generated too long agoGenerate fresh token immediately
Replay detectedBothSame token used twiceGenerate new token for each request
Device integrity failedBothRooted/jailbroken deviceCannot bypass on compromised devices

Handling Attestation Failures

If attestation fails (e.g., on a compromised device), inform the user that the action cannot be completed:

async function handleRateLimit(response) {
const rateLimitInfo = response.extensions?.rateLimitersFiring?.[0];
if (!rateLimitInfo) return;
const attestation = await getAttestation(rateLimitInfo.captchaBypassAvailable);
if (attestation) {
return retryWithAttestation(attestation);
}
// Attestation unavailable or failed
showError('Unable to verify device. Please try again later.');
}

Device Limitations

ScenarioBehaviour
iOS SimulatorDeviceCheck not available
Android EmulatorPlay Integrity may fail
Rooted AndroidDevice integrity check fails
Jailbroken iOSDeviceCheck may fail
Sideloaded appApp integrity check fails