The short answer
- Attest once per install. At first launch, the app creates a key on the device and Apple certifies it. Your server checks the certificate and stores the key’s public half.
- Sign every AI request. The app signs each request with that key. Signing happens on the phone, with no call to Apple, so it adds almost no delay.
- Check everything on the server, and refuse anything that fails. The certificate, your app’s ID, the signature, and a counter that must always go up. Sending a proof does nothing unless the server requires it.
- Plan for the devices where it fails. The Simulator, a small share of real devices, reinstalled apps and Apple outages. Give them a smaller, watched path, never an open door.
- Add limits. App Attest proves the app is genuine, not that its user is honest. It doesn’t identify people or stop heavy use.
This article assumes your app already calls the AI provider through your own server. If it still calls the provider directly with your key, start with How to protect your OpenAI API key in an iOS app.
How App Attest works
Your app can’t prove itself with a password: any secret inside the app can be extracted. App Attest uses a key instead, created in the phone’s Secure Enclave, where even the app can’t read it. The key can sign data but never leaves the device.
Apple vouches for that key once, confirming it belongs to your app, unchanged, on a genuine device. From then on, a request signed with the key can only have come from that install. The whole exchange looks like this:
Once per install: your server sends the app a one-time challenge. The app creates a key on the device and asks Apple to certify it with that challenge. Apple returns an attestation, which the app passes to your server. Your server checks it and stores the key’s public half. On every request after that: the app signs the request with the key and sends it to your server, which checks the signature and the counter before calling the AI provider. Apple is not contacted again.
App Attest works on iPhone and iPad with iOS 14 or later, Apple Watch, Apple TV and Vision Pro, and, since macOS 27, in full Mac apps. It needs a device with a Secure Enclave and doesn’t work in the Simulator.
App Attest or DeviceCheck?
Apple’s older DeviceCheck service answers a different question. It lets your server store two bits per device, which survive a reinstall: useful for “this device already had its free trial”. It doesn’t prove that the request came from your app, so it can’t protect an AI backend on its own.
Attest a key once per install
Attestation is the step that talks to Apple, so it is slow and limited. Do it once per install, not on every launch or sign-in:
- Your server creates a random one-time challenge and sends it to the app.
- The app calls
generateKey()and saves the returned key ID in the Keychain. The key ID can’t be recovered later. - The app calls
attestKey()with the key ID and a hash of the challenge. Apple returns an attestation object. - The app sends the attestation and key ID to your server, which checks it and stores the public key with a counter of 0.
attestKey() needs the network and can take several seconds; an Apple engineer called 4 to 8 seconds expected. Start it in the background at first launch, so it’s done before someone taps your AI feature.
Apple also limits how fast all your installs together can attest. It suggests staying under about 100 attestations a second and turning App Attest on gradually for a large existing user base. You won’t get near that if you attest once per install, but an app that attests on every launch can.
Sign every AI request
After attestation, the app proves itself on each request with an assertion: a signature made with the attested key. Assertions don’t contact Apple and have no rate limit; they cost only a little work on the phone.
Sign the parts of the request your server acts on: the method, the path and a hash of the body. Then a valid signature can’t be moved to a different request, and the prompt can’t be swapped on the way. Each assertion also carries a counter that goes up by one every time. Your server refuses any counter it has already seen, so a copied request can’t be sent twice.
Many setups check App Attest once, then hand the app a session token that lasts an hour. That token is a password again: someone can copy it from the app’s traffic and use it from a script until it expires. For an AI endpoint, where every request costs money, ask for an assertion on every request. An AI call takes seconds anyway, so the signature’s cost doesn’t show.
Apple’s own example also puts a one-time server challenge into each assertion. That costs an extra round trip before every request. Relying on the counter and a hash of the request, as above, gives the same protection against replays, provided your server stores the counter safely when two requests arrive at once.
A partial Swift example of both steps:
// Partial example: error handling, retries and storage are left out.
import DeviceCheck
import CryptoKit
let service = DCAppAttestService.shared
guard service.isSupported else { /* use your fallback path */ return }
// Once per install, at first launch, in the background.
let keyID = try await service.generateKey() // save keyID in the Keychain
let challenge = try await fetchChallenge() // one-time value from your server
let attestation = try await service.attestKey(
keyID, clientDataHash: Data(SHA256.hash(data: challenge)))
try await register(keyID: keyID, attestation: attestation)
// On every AI request: sign what the server will act on.
let clientData = method + path + body.sha256Hex // your own encoding
let assertion = try await service.generateAssertion(
keyID, clientDataHash: Data(SHA256.hash(data: Data(clientData.utf8))))
request.setValue(assertion.base64EncodedString(), forHTTPHeaderField: "X-App-Assertion")Performance of each step
Attestation is the one slow step, and it runs once per install. Everything that runs on every request is quick:
| Step | How often | Time and resources |
|---|---|---|
| Create a key, on the phone | Once per install | Quick, no network |
| Attest the key with Apple | Once per install | A network call to Apple, often 4 to 8 seconds, and rate-limited |
| Check the attestation, on the server | Once per install | Some CPU to decode it and check the certificates |
| Sign a request, on the phone | Every request | A small amount of CPU, no network |
| Check the signature, on the server | Every request | One signature check, plus reading and updating the stored counter |
Next to an AI call that takes seconds, the per-request work doesn’t show. Three mistakes make App Attest slow:
- Attesting on every launch or sign-in adds seconds each time and can hit Apple’s rate limit.
- Fetching a fresh challenge before every request adds a round trip to your server.
- Keeping the counter in a slow or distant database adds that delay to every AI call. Store it where your server can update it quickly and safely.
What your server must check
Apple doesn’t check attestations for you: there is no Apple endpoint to send them to, and no official server library. Your server decodes the attestation and runs these checks itself. Apple’s documentation lists each step:
| Check on the attestation | What it proves |
|---|---|
| The certificate chain ends at Apple’s App Attestation root | Apple issued it |
| The certificate contains a hash of your challenge | It was made just now, for you |
| The app ID hash equals SHA-256 of your Team ID and bundle ID | It’s your app, not another one |
| The key ID equals the hash of the public key | The key is the one the app named |
| The counter is 0 | The key is brand new |
| The environment is the one you allow | A development build can’t reach production |
Then, on every request:
| Check on each request | What it stops |
|---|---|
| The signature verifies with the stored public key | Requests from anything but that install |
| The signed data matches the request you received | A signature moved to a changed request |
| The app ID hash is yours | Proofs made for another app |
| The counter is higher than any you stored, saved in the same step | The same request sent twice |
Only when every check passes should the server call the AI provider. A proof the app sends but the server doesn’t require stops nothing: a script simply leaves it out.
On iOS 27, attestations and assertions also carry the app’s build version and whether it’s a TestFlight build, so your server can refuse old builds. Apple added a worked validation example in 2026, and the few points that trip people up are all in it: the attestation is CBOR, the public key is hashed in its uncompressed form, and the challenge sits inside a nested certificate extension.
You don’t have to write the parsing yourself. Open-source libraries exist for Node.js, Python, Go and Swift on the server; the References list a few that were updated in 2026. Before using one, check that it runs every step above, especially the counter and the environment.
Development, TestFlight and App Store builds
App Attest has two environments, set by an entitlement in your app. Builds you run from Xcode can use development. Builds from TestFlight, the App Store and the Enterprise program always use production, whatever the entitlement says. The attestation records which one it came from, as the value appattestdevelop or appattest.
Let your production server accept only production keys, or keep the two apart some other way. Otherwise anyone with a development build, which you don’t control once it’s shared, gets the same access as the App Store app. The common surprise runs the other way: a TestFlight build is production, so a server that accepts only development keys rejects it.
When App Attest fails
A small share of real users will fail App Attest, through no fault of their own. In one app, about 0.23% of devices reported it as unsupported. Each failure has its own fix:
| What happens | Why | What to do |
|---|---|---|
isSupported is false | The Simulator, or a device without support | Use your fallback path |
serverUnavailable when attesting | Apple’s service can’t be reached | Retry later with the same key and challenge hash |
invalidKey when signing | The app was reinstalled or offloaded, restored from a backup, or moved to a new device | Delete the key ID, then create and attest a new key |
invalidKey again with every new key | A bug from iOS 17.0 and earlier left some devices stuck | Treat the device as unsupported |
| Any other error when attesting | The key is unusable | Discard it and create a new key |
Apple’s service has had outages too: Apple confirmed one in May 2025 and another in June. From April to June 2025, people using ChatGPT on iPhone reported this message:
Failed to reach Apple’s App Attest server. Please check your network and try again.
Reinstalling and switching networks didn’t help them, and no official cause was posted. Since attestation happens once per install, an outage mostly hits new installs and reinstalls. Apps that already have a key keep signing requests without Apple.
Choose a fallback that can’t be abused
The tempting fallback is to let unsupported devices through without a proof. Then anyone can claim to be unsupported, and your server is open again. Pick one of these instead:
- A smaller allowance for devices without a proof, small enough that abusing it isn’t worth the effort.
- Require sign-in on that path, so each person gets one allowance.
- Show the feature as unavailable, with a clear message, if even a small allowance is too costly.
Watch how many requests use the fallback. Apple notes that a sudden rise in “unsupported” devices is itself a sign of abuse.
Keep every debug shortcut out of release builds, and test that it’s really gone. In 2026, every App Check request from one React Native app was marked invalid. A provider name the library didn’t recognise had quietly left the debug provider switched on in production.
What App Attest doesn’t stop
App Attest answers one question well: is this my genuine app on a real device? Several others stay open:
- A jailbroken device. Apple says App Attest can’t always spot a compromised operating system. On one, someone can hook your genuine app and make it sign their requests, and one such device can sign for others. Limits cap the damage.
- Who the user is. Attestation is anonymous. Each install has its own key, and a reinstall gets a fresh one with a fresh allowance. Sign-in ties an allowance to a person.
- Heavy use. A real person in your real app can still send expensive requests all day. Only allowances and budgets on the server stop that.
- Who has paid. Your paywall lives in the app. A server-side payment check decides who gets the larger allowance.
For reinstalls, Apple offers a fraud metric: through a receipt in the attestation, your server can ask roughly how many keys your app has attested on that device in the last 30 days. Apple suggests treating a high number as a reason to look closer, not to block.
How to add allowances, sign-in and a payment check, and when each is worth it, is covered in Check 2: Is this request within limits? and the sections after it.
Test it before you ship
A request that works only proves the happy path. Before shipping, try each of these, and confirm in your AI provider’s usage dashboard that none of the refused ones reached it:
| Try this | Expected result |
|---|---|
| Call your AI endpoint with curl, outside the app | Refused: no proof |
| Copy a real request from the app’s traffic and send it again | Refused: the counter was already used |
| Change the prompt in a copied request | Refused: the signature doesn’t match |
| Send a development build’s request to production | Refused, unless you allow development keys |
| Install a TestFlight build | Works: TestFlight is production |
| Delete and reinstall the app | The app attests a new key and works |
| Run in the Simulator | Your fallback path, not unlimited access |
| Inspect the release build | No debug provider or bypass switched on |
Ready-made App Attest checks
You don’t have to build the server checks yourself. A few services run them for you:
- Firebase App Check checks App Attest and gives the app a Firebase token that Firebase services, or your own server, can check. Standard tokens last from 30 minutes to 7 days, so a copied one keeps working until it expires; limited-use tokens work once. Firebase App Check vs App Attest compares the two.
- Claude App Attest lets an Apple app call Claude directly, with no server of your own. Anthropic checks App Attest and issues a one-hour token for its Messages API. It works only with Claude and needs the OS 27 releases.
- Mobile AI gateways hold your provider keys, check App Attest and forward the request to the provider. The comparison of AI gateways for mobile apps shows how each one checks requests.
Let App AI Gateway check App Attest
App AI Gateway, the product we build, is one of those gateways. It runs the checks in this article on its own server, so your app calls it instead of the AI provider:
- It checks each attestation against Apple’s root certificate, your challenge, your Team ID and bundle ID, and the environments you allow for the app.
- It requires a signed proof on every request by default. The signature covers the method, path, query and a hash of the body, and a counter for each install refuses repeats.
- Its Swift package attests in the app on first use and signs every request. After a reinstall or restore, it creates and attests a new key on its own.
The hosted service has a free plan with 1,000 requests a month, or you can self-host it. A few limits to know before choosing it:
- There’s no fallback for devices that can’t attest. Those users can’t reach the AI feature through the gateway, so plan what your app shows them.
- It doesn’t use Apple’s fraud metric or the new iOS 27 build version.
- It supports Apple apps only. Android’s Play Integrity isn’t supported yet.
- The Swift package needs iOS 16 or later.
Whichever you choose, the rule is the same: the server decides, request by request, and every request it pays for carries a fresh proof from your app.
The glossary explains App Attest next to the other protections, with what each one stops and what it leaves open.
References
- Validating apps that connect to your server — Apple Developer
- Establishing your app’s integrity — Apple Developer
- Preparing to use the App Attest service — Apple Developer
- Attestation object validation guide — Apple Developer
- Assessing fraud risk — Apple Developer
- Secure your apps with App Attest — WWDC26
- Mitigate fraud with App Attest and DeviceCheck — WWDC21
- App Attest not working in production - started today — Apple Developer Forums
- App Attest Suddenly Failing in Production - Error 4 — Apple Developer Forums
- DCAppAttestService errors: com.apple.devicecheck.error 3 and 4 — Apple Developer Forums
- Issue with DCAppAttestService for offloaded app — Apple Developer Forums
- DCAppAttestService.isSupported == false on some devices (~0.23%) — Apple Developer Forums
- How to block iOS App to run on a Rooted device? — Apple Developer Forums
- High Latency with attestKey (4-8s) — Apple Developer Forums
- Failed to reach Apple’s App Attest server. Please check your network and try again — OpenAI Developer Community
- iOS App Attest assertions categorized as “Unverified: invalid” at 100% rate fleet-wide on iOS 26.x — react-native-firebase on GitHub
- Claude App Attest — Claude Platform docs
- Get started using App Check with App Attest on Apple platforms — Firebase docs
- node-app-attest — JavaScript library on GitHub
- pyattest — Python library on GitHub
- app-attest — Go library on GitHub
- appattest-swift — Swift library on GitHub
