Skip to Content
App AttestationTroubleshooting

Troubleshooting attestation

For everyone working on attestation: mobile developers, console admins, platform engineers and backend developers. Start from where you see the symptom. This page tells you which component is at fault, the likely cause and the fix, and links to the page with the detail.

Each attestation page has its own troubleshooting table for problems within its component. This page covers the failures that show up in one place but are caused in another.

Find the failing stage

Check the stages in the order of the setup path. The first check that fails tells you where to look:

StageCheckIf it fails
1. PolicyOn the project’s Attestation page, the Policy tab shows the status Active and the published version. An amber dot on Policy means changes that aren’t published yetSigning and publishing
2. Verifierattest-verifier check ends with Result: ready to serve (exit 0), and /readyz on port 9090 reports okIn the verifier’s operation
3. Reachability and pinningOn the Status tab, Verifier checks, Check now for the environment: the row reads pinned and policy vN applied with the published versionOn the console’s Status tab
4. AppThe SDK’s attestation state reaches readyIn the app
5. APIYour API logs outcome pass, the expected app, and bound true if the app bindsIn your API’s log

Failures that cross components

These are the failures most often met during setup. Each one shows up in one component and is fixed in another.

The app reports verifierUnavailable

  • Shows up as: Swift failed(.verifierUnavailable, …), Kotlin Failed(VerifierUnavailable, …), Flutter and React Native VERIFIER_UNAVAILABLE, every time. Requests go out without the header. On the Status tab, the environment’s row reads host not registered or pin mismatch.
  • Cause: the SDKs pin their calls to the verifier, and in strict mode they never reach a host that isn’t registered and pinned in the same TrustPin project. Other causes: the verifier’s certificate was renewed with a new key and the pin wasn’t updated, the pinning instance wasn’t set up before attestation, the verifier URL in the app is wrong, or the verifier is down. On Android, a release build on an emulator or a non-OEM operating system image is refused by the pinning SDK.
  • Fix: register and pin the verifier’s domain, then Publish config, and run Check now until Pinned for the SDKs passes. For the other causes, see the SDK page’s troubleshooting: iOS and macOS, Android.

Attestation fails with app_not_allowed

  • Shows up as: the app reports attestationFailed with the reason app_not_allowed (Flutter and React Native: ATTESTATION_FAILED). The verifier answers 403 app_not_allowed, in both modes. In monitor mode, a listed Android package signed with an unlisted certificate gets a token with tp.result fail and app_not_allowed instead.
  • Cause: the policy doesn’t describe the build that runs:
    • Apple: the Team ID or Bundle ID differs from the build’s.
    • Android: the package name differs, for example a debug build with an applicationIdSuffix, or the build is signed with a certificate the policy doesn’t list. Builds installed from Google Play are signed with the Play app signing key, not your upload key.
    • Simulators and emulators: a debug token presents the app ID of the build. On a simulator build without a signing team, set TRUSTPIN_DEBUG_APP_ID.
    • The policy change that adds the app was saved but not published, or was published less than one refresh interval (5 minutes) ago.
    • The app is configured with a verifier that serves another TrustPin project.
  • Fix: compare the policy with the build: codesign -d --entitlements - on Apple, apksigner verify --print-certs on Android. See Apple apps and Android apps, then Sign and publish.

Your API refuses the token: binding mismatch

  • Shows up as: bound to another session in your API’s attestation refused: log line, or in the gateway’s log. The app reaches ready and sends the header.
  • Cause: your API hashes a different value from the one the app bound. Typical causes: the API drops the scheme (Bearer ) before hashing, a proxy or gateway rewrites the Authorization header, or the app uses fixed binding (setTokenBinding) while the API hashes Authorization.
  • Fix: hash the header value exactly as received, scheme included, or the value agreed on for fixed binding. See Binding.

A staging token reaches the production API

  • Shows up as: ERR_JWKS_NO_MATCHING_KEY, or ERR_JWT_CLAIM_VALIDATION_FAILED unexpected "iss", in your API’s log, for tokens from one app build only. Gateways log unknown kid or wrong iss.
  • Cause: each environment has its own verifier, with its own signing keys and issuer, while sharing one policy. A build configured with the staging verifier’s URL that calls the production API, or the other way round, presents a token the API can’t verify.
  • Fix: point each build at its own environment’s verifier, and give each API the values from the Go-live guide with its own Environment picked. See Staging and production.

The same ERR_JWKS_NO_MATCHING_KEY appears for tokens issued before a restart of a verifier without TRUSTPIN_TOKEN_SIGNING_KEYS, or signed by a key removed before the end of a rotation. See Signing keys.

Debug tokens don’t work, or work where they shouldn’t

A debug token works only when both hold: the published policy has Allow debug tokens on, and the verifier the app calls has the token’s hash in TRUSTPIN_DEBUG_TOKEN_HASHES. Production verifiers have no hashes, so they never accept one.

SymptomLikely causeFix
On a simulator: Swift unsupportedDevice, Flutter and React Native UNSUPPORTED_DEVICE. On an emulator: Kotlin EvidenceUnavailable or AttestationFailedNo debug token reached the app, or this verifier doesn’t accept debug tokensPass the token as the SDK page says, and use the staging verifier. See Staging: debug tokens
attestationFailed with debug_token_invalidThe token’s hash isn’t in this verifier’s TRUSTPIN_DEBUG_TOKEN_HASHES: it was replaced, or belongs to another environmentGenerate a token for this verifier with keygen --debug-token
attestationFailed with app_not_allowed, with a debug tokenThe app ID taken from the build isn’t in the policySee app_not_allowed
debug token in your API’s logA debug-token token reached an API without TRUSTPIN_ALLOW_DEBUG=trueExpected on production. On a staging API, set it. Gateway recipes always refuse debug tokens
A real Android phone attests with a debug tokenA debuggable build with a debug token, and Allow debug tokens onBuild without the token to test device evidence. See Emulators and development
/readyz reports degraded with debugTokensTRUSTPIN_DEBUG_TOKEN_HASHES is setExpected on staging. Remove it anywhere else

Clocks out of step

  • Shows up as: ERR_JWT_EXPIRED in your API’s log for tokens the app just received; a [warn] on the clock line of attest-verifier check; a note on Challenge and applied policy in Check now that the verifier’s clock is off; Play Integrity refusals with token_too_old.
  • Cause: your API’s host or the verifier’s host is off. Your API accepts 60 seconds of skew, and check warns beyond 30 seconds. The device’s clock doesn’t matter: the SDKs schedule against the verifier’s clock.
  • Fix: run NTP on every API and verifier host.

The verifier exits with code 3

  • Shows up as: the verifier, or attest-verifier check, exits with code 3.
  • Cause: no policy is published for the project in TRUSTPIN_ORG_ID and TRUSTPIN_PROJECT_ID. The first publish must be made in the console; the CLI refuses it.
  • Fix: on the Policy tab, Sign and publish, then start the verifier again. See Signing and publishing.

Play Integrity credentials are missing

  • Shows up as: a restarted verifier exits with code 2, naming the Play Integrity credentials. A running verifier stays on the previous policy version, /readyz reports degraded with policy refresh incompatible, and the Status tab row reads policy vN applied, vM published.
  • Cause: a policy whose Evidence is Both or Play Integrity only was published before the verifier had TRUSTPIN_PLAY_INTEGRITY_CREDENTIALS.
  • Fix: set the credential on every instance of every environment, and restart. Deploy configuration before the policy that needs it. See Policy updates.

With Either, a verifier without the credential keeps working: Android devices pass on Key Attestation alone.

Every device attests again after a deploy

  • Shows up as: a spike of attestations, Apple and Google calls and Play Integrity quota after a verifier deploy; refreshes answer 401 invalid_key_certificate.
  • Cause: TRUSTPIN_TOKEN_SIGNING_KEYS changed, was dropped (so the verifier generates keys in memory at each start), or differs between instances.
  • Fix: restore the previous list, give every instance of the environment the same value, and rotate in three steps.

A policy change doesn’t seem to apply

SymptomLikely causeSee
Nothing changed at allThe change is saved but not published (amber dot on Policy), or verifiers haven’t refreshed yet (5 minutes by default)Policy updates
A tightened device check doesn’t refuse devicesDevice checks apply at each device’s next attestation, within one Key certificate lifetimeWhen changes take effect
tp.result fail still appears after switching to enforceTokens issued in monitor mode stay valid until they expireRolling out
A verifier keeps an older versionIt refuses the new one; its log and /readyz say whyReadiness

In the app

Read the SDK’s state and turn on debug logging; the SDK page explains how. Error names follow each language:

SwiftKotlinFlutter, React NativeUsually meansDetails
invalidConfigurationInvalidConfigurationINVALID_CONFIGURATIONA configuration key is missing or invalid, or the file isn’t in the appThe SDK page’s configuration section
alreadyInitializedAlreadyInitializedALREADY_INITIALIZEDSetup ran twice. Flutter: expected after a hot restartThe SDK page’s set-up section
notSetUpNotSetUpNOT_SET_UPA call before setup. React Native: the native start never ran, or the app wasn’t rebuiltReact Native troubleshooting
unsupportedDeviceUnsupportedDeviceUNSUPPORTED_DEVICEApple: App Attest unavailable (simulators without a debug token, some Macs). Both platforms: new keys keep failing on this deviceDebug tokens, Support flow
evidenceUnavailableEvidenceUnavailableEVIDENCE_UNAVAILABLEThe policy’s Evidence asks for something this device can’t provide, or Play Integrity without a project number in the appAndroid apps
bindingRequiredBindingRequiredBINDING_REQUIREDRequire token binding is on and no binding is set yet. Expected before sign-inBind tokens
attestationFailed(reasons:)AttestationFailed(reasons)ATTESTATION_FAILEDThe verifier refused. reasons says whyReason codes
verifierUnavailableVerifierUnavailableVERIFIER_UNAVAILABLEThe verifier’s domain isn’t registered or pinned, or the verifier can’t be reachedverifierUnavailable
verifierIncompatibleVerifierIncompatibleVERIFIER_INCOMPATIBLEThe verifier is older than 1.0.0Upgrade the verifier; no app change is needed
invalidDigestInvalidDigestINVALID_DIGESTassertionHeaders got something other than a 32-byte SHA-256 digestAssertions for sensitive operations

After a failure, the SDK tries again only after the retry time in the failed state, at the next request, getToken() or prefetch(). A refusal makes the device wait TRUSTPIN_REFUSAL_RETRY_AFTER (10 minutes by default), and that wait survives a relaunch. Once the cause is fixed, reset() ends it.

Requests arrive without the header

The SDKs never block a request: without a token, it goes out without the header. Your API logs outcome missing.

Likely causeCheck
The host isn’t in the attested hosts, or the URL is http://The configuration’s attested hosts
The request doesn’t go through a TrustPin integrationThe “Send the header” section of the SDK page: iOS and macOS, Android, Flutter, React Native
No token yet: the state is attesting, or failedThe state and the error, in the table above
The device can’t attest, or the app version has no attestationExpected for some requests. Your API decides: see Requests without the header

Reason codes

Reason codes appear in the app’s attestationFailed reasons, in the verifier’s 403 answers and log, and in tp.reasons of tokens issued in monitor mode. They come in two classes.

Binding failures mean the evidence doesn’t match its device key, challenge or app. They never yield a token, in either mode, and no policy setting relaxes them. A sustained rate fires the AttestVerifierForgeries alert (Alerts and dashboard).

CodeMeaningWhat to do
chain_invalidThe evidence’s certificate chain doesn’t lead to the platform’s root, or a certificate has expiredNothing for isolated devices. Many at once: contact support with the verifier’s log lines
chain_revokedGoogle has revoked a certificate in the device’s attestation chainNothing: the device can’t attest with Key Attestation
nonce_mismatch, key_mismatch, counter_invalid, request_hash_mismatch, assertion_invalidThe evidence or a signature doesn’t match its challenge, key or request. On a renewal, the SDK discards the key and attests again, onceNothing for isolated cases
token_too_oldThe Play Integrity verdict is older than Maximum verdict age (seconds)Check the verifier host’s clock; raise the setting only if slow networks explain it
debug_token_invalidThe debug token isn’t among this verifier’s hashesSee Debug tokens
key_not_hardware_generatedThe Android key wasn’t generated in secure hardware, as on emulatorsUse a debug token on emulators
payload_mismatchPOST /v1/assertion only: your API’s digest covers different bytes from the app’sDigest the exact request bytes on both sides. See Assertions

Policy failures mean the device or app doesn’t meet a setting of your policy. In monitor mode the device still gets a token, with tp.result fail; in enforce mode it gets 403 attestation_failed. Each setting is described in Setting up apps and the policy, and the rollout page says how to judge each reason before enforcing.

CodeSettingTypical fix
app_not_allowedThe app listsapp_not_allowed
dev_environmentAccept development buildsTurn it on while testing builds run from Xcode
app_not_recognizedRequire an app recognized by Google PlayOff while testing builds not installed from Play, or use a Play testing track
unlicensedRequire a licensed appOff unless you distribute only through Play
device_integrity_insufficientMinimum device integrityAccept the loss, or lower the level
app_access_risk, play_protect_risk, device_activity_excessiveApp access risk, Play Protect: maximum risk, Recent device activity: maximum levelMeasure what each costs in monitor mode first
verdict_unavailableInformational: an optional Play Integrity verdict was missing, and the check passedTurn the response on in Play Console, Change responses
security_level_insufficientMinimum security levelSTRONG_BOX refuses most devices
verified_boot_failed, bootloader_unlockedRequire verified boot, Require a locked bootloaderExpected for modified devices; relax only for testing
patch_level_too_oldMinimum OS patch level (YYYYMM, optional)Accept the loss, or lower it
evidence_missingEvidenceThe device didn’t supply the evidence the option requires. Consider Either
app_identity_unverifiedKey Attestation on a device whose bootloader isn’t lockedExpected for such devices

The SDKs may report their own reason when the verifier gave none: http_<status> for an answer without a reason, invalid_key and invalid_input. too_many_attestations is the verifier’s per-key limit, below.

In the verifier’s answers

The SDK reacts to each error on its own; you see these codes in the verifier’s log, in the trustpin_attest_http_requests_total metric by code, and in the reasons the app reports.

HTTPerrorWhat the SDK doesWhat to check
400bad_requestReports it, and waits as after a refusalThe verifier log’s message. A Play Integrity token Google rejected answers “play integrity token rejected by Google”
400binding_requiredReports bindingRequiredExpected before sign-in with Require token binding on
401invalid_challengeFetches a new challenge and retries onceRepeated: instances with different TRUSTPIN_CHALLENGE_SECRETS, or a host clock that is off
401invalid_key_certificateDiscards its key and attests againExpected once after a stricter policy (an app or digest removed, the switch to enforce). Repeated: instances with different TRUSTPIN_TOKEN_SIGNING_KEYS
403attestation_failedReports the reasons and waitsReason codes
403app_not_allowedReports it and waitsapp_not_allowed
409challenge_reusedStarts again with a new challengeRepeated: a gateway or load balancer that retries POST requests to the verifier
413payload_too_largeReports it and waitsRare. Send the verifier’s log line to support
429rate_limitedWaits for Retry-AfterYour gateway’s limit, or TRUSTPIN_RATE_LIMIT. With TRUSTPIN_RATE_LIMIT set and no TRUSTPIN_TRUSTED_PROXIES, every app shares the gateway’s address and one limit
429too_many_attestationsKeeps its key and waitsA device key over TRUSTPIN_KEY_RATE_LIMIT (60 an hour). Look for an app loop calling reset() or assertionHeaders
502upstream_unavailableBacks off and retriesThe verifier can’t reach Play Integrity, or its credentials are refused. Existing tokens stay valid
503not_readyBacks off and retriesNo policy loaded yet, or the store is down. See Readiness
404No TrustPin error bodyThe attempt failsThe gateway forwards the URL’s path to the verifier: strip it. See Route HTTPS to it

401 invalid_token comes only from POST /v1/assertion, which your API calls; see Assertions for sensitive operations. The full list is in the verifier API reference.

In the verifier’s operation

Exit codes

attest-verifier check and the verifier exit with the same codes. check prints a [FAIL] line for each problem.

Exit codeUsually meansFix
1The CDNs are unreachable and there is no cached policy; the policy fails validation because TRUSTPIN_PROJECT_PUBLIC_KEY belongs to another project; or the store is unreachable or its schema is behindDeploying the verifier, troubleshooting
2A configuration error: quotes passed by docker run --env-file, an example placeholder left in, or Play Integrity credentials the policy needsPlay Integrity credentials, Fill in and check the configuration
3No policy is published for the projectExit code 3

Read every [warn] line too. The usual ones: the clock is more than 30 seconds off, the signing keys or challenge secrets are generated in memory, the store is memory:// with several instances, or an unknown TRUSTPIN_* variable, usually a misspelling.

Readiness

GET /readyz on port 9090 answers ok, degraded (serving, but needs attention) or unavailable (no traffic). Send the monitor key to see which check fails.

CheckStatusUsually means
policyunavailableNo policy loaded
storeunavailableRedis or Postgres unreachable: /v1/attest and /v1/token answer 503 not_ready
policydegradedpolicy refresh incompatible: Play Integrity credentials. not_found: the policy is no longer published. Three failed refreshes: the outbound path to the CDN
signingKeysdegradedTRUSTPIN_TOKEN_SIGNING_KEYS is unset: every device attests again at each restart
debugTokensdegradedDebug token hashes are set: expected on staging only
revocationListdegradedNo outbound HTTPS to android.googleapis.com for 7 days

The causes and fixes for each are in Readiness. If probes from outside the container can’t connect at all, set TRUSTPIN_MONITOR_LISTEN_ADDR=:9090 with TRUSTPIN_MONITOR_API_KEY.

On the console’s Status tab

Check now, under Verifier checks, connects to the environment’s verifier URL as an app would. Each step passes, warns or fails, and the environment’s row sums them up.

StepFailureFix
Reachable over TLSThe connection fails, or the certificate expires soonCheck DNS, TLS and the firewall for the environment’s URL. When you renew the certificate with a new key, update the pin
Pinned for the SDKshost not registered, pin mismatch, or every pin has expiredRegister and pin the domain. Apps in strict mode report verifierUnavailable meanwhile
IssuerIssuer mismatchSet TRUSTPIN_TOKEN_ISSUER to the environment’s URL, or fix the URL on the Deployment tab. Your API’s Issuer (iss) must match too
IssuerDiscovery is not servedThe issuer isn’t an https:// URL. Gateways that find keys through OIDC discovery can’t use this verifier
Signing keysThe JWKS can’t be fetched, or publishes no keyRoute /.well-known/ to the verifier
Challenge and applied policyThe verifier applies no policy yet (not_ready)Check TRUSTPIN_ORG_ID, TRUSTPIN_PROJECT_ID and TRUSTPIN_PROJECT_PUBLIC_KEY, and the verifier’s log
Challenge and applied policypolicy vN applied, vM publishedWait one refresh interval. If it stays behind, the verifier refuses the new version: see Readiness
Challenge and applied policyThe applied version is ahead of the published oneThe URL reaches a verifier of another project, or one that loads its policy from another source (TRUSTPIN_ATTEST_CONFIGURATION_URL)
Challenge and applied policyA note that the clock is off, or that debug tokens are acceptedClocks; on a production environment, remove TRUSTPIN_DEBUG_TOKEN_HASHES
Any stepThe verifier redirected the requestApps and TrustPin follow no redirects. Point the environment’s URL at the final address

The Verifier fleet card reads Never fetched or No recent fetch when no policy fetch has been logged. Fetch logs arrive a few hours late, per UTC day, so a verifier deployed today can take a while to appear. Check now gives the exact state at once.

In your API’s log

The checkAttestation middleware logs one attestation line per request, with outcome, and an attestation refused: line when a token fails verification. A gateway recipe logs trustpin attestation rejected: <reason>.

LogLikely causeFix
outcome missingThe request had no headerRequests arrive without the header
ERR_JWKS_NO_MATCHING_KEYA token from another environment’s verifier, or signed by a key the verifier no longer hasA staging token reaches the production API
ERR_JWT_CLAIM_VALIDATION_FAILED unexpected "iss" or "aud", or wrong projectThe API’s issuer, audience or project differ from the verifier’s. A verifier without a public URL issues tprn::attest::…Copy the values from the Go-live guide for the right environment
ERR_JWT_CLAIM_VALIDATION_FAILED unexpected "typ"Something other than an access token was sent in the headerSend the token from the SDK only
ERR_JWT_EXPIREDThe token expired, or the clocks differ by more than 60 secondsClocks
ERR_JWKS_TIMEOUT or a fetch errorThe API can’t reach the JWKS URLcurl the JWKS URL from the API’s host
bound to another sessionBinding mismatchBinding mismatch
unbound tokenRequire-binding is on in the API, and the app doesn’t bindBind in the app, or turn require-binding off while Require token binding is off
debug tokenA debug-token token on an API without TRUSTPIN_ALLOW_DEBUG=trueDebug tokens
outcome failA token issued in monitor mode, with reasonsReason codes
outcome pass, but env development or debug on productionThe policy accepts development builds or debug tokens, which every environment sharesPrepare for enforce

The full table is in Verifying tokens in your API.

At the gateway

The gateway recipes check the same things as your API, with these differences, which explain most surprises:

SymptomCauseSee
Requests are refused while the policy is in monitor modeThe recipes refuse every request without the header, whatever the modeGateway or API
The app can’t get a token once a recipe is deployedThe recipe sits in front of the verifier’s own routesWhat every recipe checks
Simulator and emulator builds are refusedThe recipes always refuse debug tokensWhat every recipe checks
Every token is refused as wrong project on EnvoyPROJECT in the Lua file still holds the placeholderEnvoy
Tokens are refused just after a key rotationThe gateway’s JWKS cache doesn’t know the new key yetSigning keys
AWS answers 401 to a request with a tokenThe request has no Authorization headerAWS API Gateway

The rest is in Checking tokens at the gateway.

Before you contact support

TrustPin sees no attestation traffic, so the answers are in your own logs. Collect these before writing to support@trustpin.cloud:

  • the verifier version (attest-verifier version), the output of attest-verifier check, and the /readyz body;
  • the Check now result for the environment;
  • the app’s SDK and version, its state and error, with the reasons, and the time of the attempt;
  • the verifier’s log lines for that time (they carry request_id and the first characters of the device key, never tokens or evidence), and your API’s attestation line.

Never send a valid token, a debug token or a secret.

Next steps