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?
| 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 |
Supported Methods
| Method | Platform | Enum Value |
|---|---|---|
| Apple DeviceCheck | iOS | APPLE_DEVICE_CHECK |
| Play Integrity | Android | ANDROID_PLAY_INTEGRITY |
Integration Flow
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐│ Your App │ │ Device OS │ │ Horizon API │└────────┬────────┘ └────────┬─────────┘ └────────┬────────┘ │ │ │ │ 1. API request │ │─────────────────────────────────────────────────>│ │ │ │ │ 2. Rate limit triggered (includes nonce) │ │<─────────────────────────────────────────────────│ │ │ │ │ 3. Request attestation│ │ │ 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" } ] } ] }}| Field | Description |
|---|---|
rateLimitingBucket | Which rate limiter was triggered |
captchaBypassAvailable | Available verification methods |
type | The attestation type |
siteKey | Nonce for attestation request |
Submitting Attestation
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 attestationfetch('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
Use the DCAppAttestService to generate attestation tokens:
import DeviceCheckimport CryptoKit
@available(iOS 14.0, *)func generateAttestation(nonce: String) async throws -> String { let keyId = try await getOrCreateAttestationKey()
let nonceData = Data(nonce.utf8) let hash = SHA256.hash(data: nonceData) let hashData = Data(hash)
let assertion = try await DCAppAttestService.shared.generateAssertion( keyId, clientDataHash: hashData )
return assertion.base64EncodedString()}Prerequisites:
- Enable App Attest capability in Xcode
- Configure App ID in Apple Developer Portal
- iOS 14.0 or later required
Android: Play Integrity API
Add the Play Integrity dependency:
dependencies { implementation 'com.google.android.play:integrity:1.3.0'}Generate integrity tokens:
import com.google.android.play.core.integrity.IntegrityManagerFactoryimport 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
| Failure | Cause | Resolution |
|---|---|---|
| Invalid token format | Malformed or corrupted token | Regenerate token |
| Nonce mismatch | Wrong nonce used | Use siteKey from rate limit response |
| Expired nonce | Token generated too long ago | Generate fresh token immediately |
| Replay detected | Same token used twice | Generate new token for each request |
| Device integrity failed | Rooted/jailbroken device | Cannot 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
| Scenario | Behaviour |
|---|---|
| iOS Simulator | DeviceCheck not available |
| Android Emulator | Play Integrity may fail |
| Rooted Android | Device integrity check fails |
| Jailbroken iOS | App Attest may fail |
| Sideloaded app | App integrity check fails |