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.

Benefit Description
Seamless UX Rate limits bypassed automatically without user interaction
Native experience No web views or external verification flows
Invisible to users No “unusual activity” warnings or challenges
Method Platform Enum Value
Apple DeviceCheck iOS APPLE_DEVICE_CHECK
Play Integrity Android ANDROID_PLAY_INTEGRITY

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.

┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ 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) │
│<─────────────────────────────────────────────────│
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ 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) │
│<─────────────────────────────────────────────────│

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"
}
]
}
]
}
}
Field Description
rateLimitingBucket Which rate limiter was triggered
captchaBypassAvailable Available verification methods
type The attestation type
siteKey Nonce for Android Play Integrity. Always null for Apple DeviceCheck (not required)

Include these headers when retrying a rate-limited request:

Header Value
X-Captcha-Type APPLE_DEVICE_CHECK or ANDROID_PLAY_INTEGRITY
X-Captcha-Response Base64-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 })
});

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)

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
Failure Platform Cause Resolution
Invalid token format Both Malformed or corrupted token Regenerate token
Nonce mismatch Android Wrong nonce used Use siteKey from rate limit response
Expired nonce Android Token generated too long ago Generate fresh token immediately
Replay detected Both Same token used twice Generate new token for each request
Device integrity failed Both Rooted/jailbroken device Cannot bypass on compromised devices

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.');
}
Scenario Behaviour
iOS Simulator DeviceCheck not available
Android Emulator Play Integrity may fail
Rooted Android Device integrity check fails
Jailbroken iOS DeviceCheck may fail
Sideloaded app App integrity check fails