Your backend cannot tell your app from a script pretending to be your app. Every request arrives as bytes over HTTPS, and anything the app can send, a bot, an emulator farm or a modified APK can send too. For most endpoints that is survivable. For payments it is not, which is how I ended up shipping device integrity on a live retail platform: Play Integrity on Android, App Attest on iOS, gating the payment flows.
This post is the shape of that integration: what the two systems actually prove, where the gate belongs, and the rollout discipline that keeps real users out of the blast radius. Examples are generic and invented; the patterns are what shipped.
What attestation proves, and what it does not
Both systems answer one narrow question with a cryptographic signature behind it: is this request coming from your genuine app binary on a reasonably healthy device? Google signs a verdict about the app identity and the device state. Apple attests that a key was generated inside the Secure Enclave of a real device running your real app.
Just as important is what they do not prove:
- Not who the user is. Attestation is not authentication; it composes with your login, it never replaces it.
- Not that the device is uncompromised forever. A verdict is a point-in-time statement.
- Not that nobody can ever abuse your API. A determined attacker with a real device and a real app still passes. Integrity raises the cost of abuse at scale; it does not make abuse impossible.
That framing decides everything downstream: integrity is a gate for your riskiest flows, not a moat around the whole app.
The round trip, done properly
The single most common mistake is treating the verdict as something the app checks. The app is the untrusted party; a client-side check is a polite suggestion to the attacker. The verdict only means something when the backend requested it and the backend verifies it:

The steps that carry the security:
- The backend issues a nonce for the operation, single-use and short-lived. The app never invents its own.
- The app asks the platform for a token bound to that nonce.
- The app forwards the token; it cannot read or alter what is signed inside.
- The backend verifies the token with Google's or Apple's verification path, checks the nonce matches the pending operation, and only then opens the gate.
Replay dies at step one. Tampering dies at step four. Nothing of value happens on the client.
The Flutter shape: one gateway, two native halves
Attestation APIs are native on both platforms, which makes this a textbook case for the gateway seam over a platform channel:
abstract class IntegrityGateway {
/// Returns a platform token bound to [nonce], to be sent to the
/// backend alongside the sensitive request.
Future<IntegrityToken> requestToken(String nonce);
}
One channel-backed implementation calls Play Integrity on Android and App Attest on iOS. One fake implementation returns canned tokens so every test and every simulator run works without the vendor infrastructure. App code depends on the interface and does not know which platform it is on.
On iOS there is one extra wrinkle worth designing for: App Attest generates a key once, attests it once, and then produces cheap assertions per request. Model that lifecycle inside the native half; the Dart interface above stays one method.
Gate the vault, not the lobby
Attestation calls cost latency and quota, and every gated flow is a flow that can fail. So the gate goes exactly where the risk is:

- Ungated: browsing, search, content. High traffic, low risk, zero attestation cost.
- Gated: opening a payment session, changing credentials, redeeming value. Low traffic, high risk, one attestation each.
And decide the failure behavior on purpose. A failed verdict on a payment flow should degrade to something safe and explicit, not to a dead app. An attestation outage (Google or Apple having a bad day) must never take your checkout down with it: that is a business decision encoded as a timeout and a fallback path, made before the incident, not during it.
Rolling it out on a live product
Shipping integrity onto an app with a live user base is where the discipline earns its keep. The sequence that worked:
- Monitor first. Ship the round trip with the gate open: collect verdicts, enforce nothing. Real user bases contain more odd-but-legitimate devices than you expect.
- Read the data before choosing thresholds. Decide which verdict levels you will accept based on what your actual users return, not on the documentation's ideal.
- Enforce with a kill switch. The gate goes live behind a remote flag that can turn it off faster than an incident call escalates.
- Keep telemetry on the failure path. Every rejected attempt is either an attack you stopped or a customer you lost; you want to know which.
Layers, not silver bullets
Integrity attestation is one layer. It composes with the rest of a payment-grade posture: PCI DSS-aligned flows, PII kept out of logs and analytics, and a boundary design where the sensitive SDK sits behind one seam the app cannot leak around. Emulators and dev builds fail attestation by design, so your test setup needs the fake gateway wired in from day one, and your nightly device suite runs against the monitor-only backend environment.
None of it is exotic. It is the same engineering habit applied to a security feature: narrow interfaces, server-side truth, staged rollout, and a kill switch you hope stays boring. The version of this that matters is live today, quietly deciding which requests get to open a payment session in Sharjah Coop.
