> ## Documentation Index
> Fetch the complete documentation index at: https://docs.synheart.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# App attestation

> Verify that traffic comes from your real, unmodified app on a normal device, using Play Integrity (Android) and App Attest (iOS)

App attestation lets Synheart confirm that traffic comes from your real, unmodified build on a normal device, not from emulators, repackaged binaries, or scripted clients. The platform supports:

* **Android**: [Google Play Integrity API](https://developer.android.com/google/play/integrity/overview)
* **iOS**: [Apple App Attest](https://developer.apple.com/documentation/devicecheck/establishing-your-app-s-integrity)

Both platforms follow the same client flow: the SDK requests a one-time challenge, asks the OS for an attestation over that challenge, and registers the attested device with Synheart. You configure each App in the [platform dashboard](https://platform.synheart.ai/) once; the SDKs handle the runtime path.

<Note>
  The Synheart `app_id` (shown on the App in the [platform dashboard](https://platform.synheart.ai/)) is the platform's identifier for that App record: a short code like `app_focus_and_kB8mPx`. Your Android `packageName` and iOS bundle ID are separate values you also configure on the App. Don't conflate them: Play Console steps below refer to your Play listing's `packageName`, not the platform `app_id`.
</Note>

## How Android attestation works at Synheart

Google Play Integrity's `decodeIntegrityToken` is authorized per Google Cloud project: only callers that can act as your Cloud project may decode tokens minted for your `packageName`. You supply that authorization by creating a dedicated service account in your own Cloud project and uploading its JSON key to your App's settings page. Synheart stores the key encrypted at rest and uses it server-side only to call `decodeIntegrityToken` for your `packageName`.

This means:

* The credential authorizes one specific operation against one specific package.
* Decode calls bill against your Cloud project's quota.
* If you rotate or revoke the key in Cloud Console, decode access stops immediately. No Synheart-side action needed.
* iOS attestation needs no equivalent credential: App Attest verifies against Apple's global public CA chain.

## Configure attestation in the dashboard

For each App in the [platform dashboard](https://platform.synheart.ai/), open **App settings → SDK attestation** and fill in the block. Fields differ by OS:

| OS      | Required                                                                                                                                                             | Optional                                                                                                      |
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Android | **Bundle ID** (your Android `packageName`), **Google project ID** (the GCP project linked to Play Console), **Play Integrity decoder credential** (uploaded SA JSON) | **Allow development builds**: accept attestations from `flutter run` / debug-signed / emulator builds         |
| iOS     | **Bundle ID** (your iOS bundle ID), **Apple Developer Team ID**                                                                                                      | **Allow development builds**: accept attestations from Xcode-installed builds and TestFlight internal testing |

Both platforms expose the same **Allow development builds** toggle on the App settings page. It relaxes attestation verdicts so debug / pre-release builds pass; the server then treats the app as running in dev mode. Turn it off before App Store / Play Store release.

## Android: Play Integrity setup

You complete a few one-time steps in Play Console and your own Google Cloud project, then upload one credential to the platform dashboard.

### Before you start

You need:

* A Google Play Developer account and access to Play Console for the app you want attested.
* The app created in Play Console (a draft or any release track is enough).
* **Owner or IAM Admin** access on a Google Cloud project you'll dedicate to this app.

### Steps

<Steps>
  <Step title="Link Play Console to your Cloud project">
    Play Integrity decode authorization is granted by the **Cloud project linked to your Play Console app**, not by an IAM role you grant Synheart.

    1. Open [Google Play Console](https://play.google.com/console/) and select your app.
    2. In the per-app left nav, open **Protected with Play → Play Integrity API settings**.
    3. Confirm the **Google Cloud project** row shows your own GCP project. If it doesn't, click **Edit project** and pick your project from the picker.

    Note the project ID exactly; you'll use it in step 4.
  </Step>

  <Step title="Enable the Play Integrity API in that Cloud project">
    <Tabs>
      <Tab title="Cloud Console UI">
        Switch the project picker in [Google Cloud Console](https://console.cloud.google.com/) to the project you linked above, then:

        [Open the Play Integrity API library page →](https://console.cloud.google.com/apis/library/playintegrity.googleapis.com)

        Click **Enable**. If the button already says **Manage**, the API is on; you can continue.
      </Tab>

      <Tab title="gcloud CLI">
        ```bash theme={null}
        export PROJECT_ID=your-cloud-project-id
        gcloud services enable playintegrity.googleapis.com --project="$PROJECT_ID"
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Create a service account scoped to Play Integrity">
    <Tabs>
      <Tab title="Cloud Console UI">
        Still in Google Cloud Console, in the same project:

        1. Open **IAM & Admin → Service Accounts**.
        2. Click **+ Create Service Account**.
        3. Name it predictably, e.g. `play-integrity-decoder`.
        4. On **Grant this service account access to project**, give it one role: **Service Usage Consumer** (`roles/serviceusage.serviceUsageConsumer`).
        5. Skip **Grant users access to this service account** and click **Done**.

        Then download a key:

        1. Click into the service account.
        2. Open the **Keys** tab.
        3. **Add key → Create new key → JSON**.
        4. Save the downloaded `.json` somewhere temporary.
      </Tab>

      <Tab title="gcloud CLI">
        ```bash theme={null}
        export PROJECT_ID=your-cloud-project-id
        export SA_NAME=play-integrity-decoder
        export SA_EMAIL=${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com

        # 1. Create the service account
        gcloud iam service-accounts create "$SA_NAME" \
          --project="$PROJECT_ID" \
          --display-name="Play Integrity Decoder" \
          --description="Per-app decoder credential for Synheart attestation."

        # 2. Grant the only role it needs
        gcloud projects add-iam-policy-binding "$PROJECT_ID" \
          --member="serviceAccount:$SA_EMAIL" \
          --role="roles/serviceusage.serviceUsageConsumer" \
          --condition=None

        # 3. Mint a JSON key
        gcloud iam service-accounts keys create ./play-integrity-decoder.json \
          --iam-account="$SA_EMAIL" \
          --project="$PROJECT_ID"
        ```

        If the last command fails with `constraints/iam.disableServiceAccountKeyCreation`, see the [org-policy workaround](#if-your-org-disables-service-account-key-creation) below.
      </Tab>
    </Tabs>

    Treat the downloaded `.json` like a password: anyone with this file can decode integrity tokens for your app.

    <Note>
      Play Integrity decode authorization itself comes from the Play Console linkage in step 1, not from a project IAM role; the SA only needs to be able to **call** an enabled API in your Cloud project, which is exactly what Service Usage Consumer grants. Don't add Owner, Editor, or other broad roles.
    </Note>
  </Step>

  <Step title="Set the App's attestation fields in the platform dashboard">
    On the matching App in the [platform dashboard](https://platform.synheart.ai/), under **SDK attestation → Android**:

    * `bundle_id`: your Android `packageName` (for example `com.example.focus`).
    * `google_project_id`: the GCP project ID from step 1. **It must match the project\_id inside the SA JSON you'll upload next**; if they disagree the upload is rejected.
    * **Allow development builds**: keep off for Play Store releases. Turn it on only while testing emulator or debug-signed builds (`flutter run`, `./gradlew installDebug`); the server then accepts `UNRECOGNIZED_VERSION` and emulator verdicts that would otherwise fail.

    Click **Save changes**.
  </Step>

  <Step title="Upload the service account JSON">
    Click **Edit** again, then under **Play Integrity decoder credential** click **Upload SA JSON…** and pick the file from step 3.

    The dashboard validates the file (shape, `type == "service_account"`, project\_id match) and stores it encrypted. On success the card flips to:

    ```
    Service account   play-integrity-decoder@<your-project>.iam.gserviceaccount.com
    Cloud project     <your-project>
    Uploaded          <timestamp> · by <you>                                  fp <short hash>
    ```

    You can delete the downloaded JSON from your machine now; re-creating it from Cloud Console takes one click if you ever need to.
  </Step>
</Steps>

### Rotating the credential

If the JSON key leaks or you rotate on a schedule:

1. Create a fresh JSON key in Cloud Console (same SA, or a new one in the same project).
2. In the dashboard, click **Replace…** on the credential card and upload the new file.
3. Delete the old key in Cloud Console.

The card records the rotation time and which user did it.

### Removing the credential

Click **Remove**. There is no fallback decoder: every device registration for your app will start failing with `DEV_003` immediately and continue failing until a new credential is uploaded. Only remove it if you mean to.

### What gets enforced

Synheart calls `decodeIntegrityToken` using your service account and checks:

* **Real device.** Tokens from emulators or virtual devices are rejected unless **Allow development builds** is on.
* **Recognized app.** The build must match a Play-distributed version of your `packageName`. Sideloaded or repackaged builds fail.
* **Token freshness.** Tokens older than five minutes are rejected to prevent replay.

The service account permission above (`roles/serviceusage.serviceUsageConsumer`) lets the SA call enabled APIs in your project. It cannot read your store listing, publish releases, view financials, or access end-user data.

### If your org disables service account key creation

Some Google Cloud orgs enforce `constraints/iam.disableServiceAccountKeyCreation`, which blocks step 3's JSON-key download. You'll see this error from `gcloud`:

```
ERROR: (gcloud.iam.service-accounts.keys.create) FAILED_PRECONDITION:
Key creation is not allowed on this service account.
type: constraints/iam.disableServiceAccountKeyCreation
```

You have two options.

#### Option A: Self-service workaround (if you have `orgpolicy.policy.set`)

If your role on the Cloud project lets you set org policies, you can temporarily override the constraint at the project level, mint the one key you need, then restore the enforcement. The key keeps working; only *future* key creation is blocked again.

```bash theme={null}
export PROJECT_ID=your-cloud-project-id
export SA_EMAIL=play-integrity-decoder@${PROJECT_ID}.iam.gserviceaccount.com

# Enable the Org Policy API on this project (one-time)
gcloud services enable orgpolicy.googleapis.com --project="$PROJECT_ID"

# Override the constraint at project level
cat > /tmp/allow-key-creation.yaml <<EOF
name: projects/${PROJECT_ID}/policies/iam.disableServiceAccountKeyCreation
spec:
  rules:
    - enforce: false
EOF
gcloud org-policies set-policy /tmp/allow-key-creation.yaml

# Wait a few seconds for propagation, then mint the key
# (retry the create call below if the first attempt still says FAILED_PRECONDITION)
gcloud iam service-accounts keys create ./play-integrity-decoder.json \
  --iam-account="$SA_EMAIL" \
  --project="$PROJECT_ID"

# Restore the inherited enforcement
gcloud org-policies reset iam.disableServiceAccountKeyCreation --project="$PROJECT_ID"

# Wipe the local YAML
rm /tmp/allow-key-creation.yaml
```

Upload the resulting `play-integrity-decoder.json` via the dashboard as in step 5. Your project is now back to the same security posture as before, with one extra credential outstanding which you can revoke via Cloud Console any time you rotate.

#### Option B: Workload Identity Federation (no key download required)

Workload Identity Federation lets Synheart mint short-lived federated tokens to act as your service account, with no long-lived key ever leaving your org. You register Synheart's OIDC issuer as a trust source in your project and add an IAM binding letting the federated identity impersonate `play-integrity-decoder`.

WIF support is on the roadmap but not yet generally available. If you're blocked by `disableServiceAccountKeyCreation` and Option A isn't appropriate for your org, contact [support@synheart.ai](mailto:support@synheart.ai) for early access.

## iOS: App Attest setup

iOS attestation uses Apple App Attest, which is built into iOS 14+. There is no Apple-side invite step and no credential upload: the OS attests directly to Apple, and Synheart verifies the resulting attestation object against Apple's global public CA chain.

### Before you start

You need:

* An Apple Developer account with the app's bundle ID provisioned.
* iOS 14 or later on a real device; App Attest is not available in the iOS Simulator.

### Steps

<Steps>
  <Step title="Note your Apple Team ID and bundle ID">
    Both are visible at [developer.apple.com/account](https://developer.apple.com/account) under **Membership** (Team ID) and **Identifiers** (bundle ID).
  </Step>

  <Step title="Set the App's attestation fields in the platform dashboard">
    On the matching App in the [platform dashboard](https://platform.synheart.ai/), under **SDK attestation → iOS**:

    * `bundle_id`: your iOS bundle ID (for example `com.example.focus`).
    * `team_id`: your 10-character Apple Developer Team ID.
    * **Allow development builds**: keep off for App Store / TestFlight external builds (the server then expects Apple's production App Attest environment). Turn it on while testing with Xcode-installed builds or TestFlight internal testing, which use Apple's development App Attest environment.
  </Step>

  <Step title="Build with the App Attest entitlement">
    Add the `com.apple.developer.devicecheck.appattest-environment` entitlement to your app's entitlements plist. Set it to `production` for App Store / TestFlight external, and `development` for Xcode-installed / TestFlight internal, and match the dashboard's **Allow development builds** toggle (on when entitlement is `development`, off when `production`).
  </Step>
</Steps>

### What gets enforced

The Synheart verifier checks:

* **Apple-signed certificate chain.** The attestation must chain to Apple's App Attest root.
* **Bundle ID + Team ID.** The relying party hash inside the attestation must match `team_id` + `bundle_id` configured on the App.
* **Challenge nonce.** The attestation must cover the exact challenge nonce Synheart issued (5-minute TTL).
* **Development vs production env.** The attestation environment carried by the App Attest object must match the **Allow development builds** toggle on the App (Apple's `development` env when the toggle is on, `production` env when off).

## Client flow

Both platforms follow the same three-step pattern; the SDKs implement this for you:

1. Request a one-time challenge from Synheart; the response includes a challenge ID and nonce with a 5-minute TTL.
2. The OS produces an attestation over the nonce (Play Integrity token on Android, App Attest object on iOS).
3. Submit the attestation to Synheart; the server verifies it and binds the device to your App.

Subsequent SDK requests are signed by the registered device; see [Synheart Auth](/synheart-auth/overview).

## Troubleshooting

| Symptom                                                                           | Platform | What to check                                                                                                                                                                                                 |
| --------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `service account project_id does not match the app's google_project_id` on upload | Android  | The dashboard's **Google project ID** field has to equal the `project_id` inside the SA JSON. Update the field, save, then re-upload.                                                                         |
| `Service account JSON is not valid JSON` / `expected "service_account"` on upload | Android  | Re-download the key from Cloud Console; pick **JSON** as the format, not P12.                                                                                                                                 |
| `DEV_003` / device registration failing immediately                               | Android  | The Cloud project linked under Play Console → **Protected with Play** → **Play Integrity API settings** must equal the dashboard's **Google project ID**, and the uploaded SA must live in that same project. |
| Credential upload returns "Unauthorized"                                          | Android  | Hard-refresh the dashboard page (`Cmd+Shift+R`) to renew the session, then retry.                                                                                                                             |
| `attestation environment mismatch`                                                | iOS      | The entitlement (`production` / `development`) must match the dashboard `production` toggle on the App.                                                                                                       |
| `App Attest not supported` at runtime                                             | iOS      | App Attest requires iOS 14+ on a real device. Simulator builds cannot attest.                                                                                                                                 |
| Tokens rejected as too old                                                        | Both     | The challenge expires after five minutes. Re-fetch a fresh challenge before attesting.                                                                                                                        |

## Need help?

Contact [support@synheart.ai](mailto:support@synheart.ai) with your Synheart `app_id`, the `bundle_id` you configured, and the time of the failing attestation. For Android, include the credential fingerprint shown on the App settings page (a short blake2s hex). For iOS, include the entitlement value from your build.

## Next

<CardGroup cols={2}>
  <Card title="API keys" icon="key" href="/platform/api-keys">
    Issue per-app keys and understand ingestion allowlisting.
  </Card>

  <Card title="Synheart Auth" icon="key" href="/synheart-auth/overview">
    How registered devices sign every outbound request once attestation has succeeded.
  </Card>
</CardGroup>
