# Add Magistrate to Your Android App

The Magistrate module sits on top of the Octet Mobile SDK. One call, `decide`, fetches your policy, makes the location proofs it needs, and returns the engine's signed decision.

This page covers Magistrate 0.1.0 for Android, which brings in Octet SDK 3.1.2. See the [Changelog](/docs/magistrate/reference/changelog/) for what each release changes.

## Prerequisites

- Android `minSdk` 30 or later, the Octet Mobile SDK's minimum, and `compileSdk` 35 or later.
- The Octet Mobile SDK set up in your app, with the `ACCESS_FINE_LOCATION` permission. Follow the [Android Quickstart](/docs/mobile/getting-started/android-quickstart/) first.
- Your app registered on your Octet license. The SDK signs in with hardware attestation, so there's no license key to paste.
- A release-signed build on a real device for production. A debug build marks its locations as mock, so it can't make a proof for production. A debug build or an emulator needs a sandbox token.
- A policy published from the [Magistrate Studio](/docs/magistrate/get-started/write-a-policy/).

## 1. Add the dependency

Add the Magistrate Maven repository and the dependency. Magistrate brings in the Octet SDK (`com.octetproof:sdk:3.1.2`) for you. The native library loads through JNA, which comes in the same way.

```kotlin
// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        maven("https://raw.githubusercontent.com/octetproof/octet-magistrate-android/mvn-repo")
    }
}

// app/build.gradle.kts
dependencies {
    implementation("com.octetproof:magistrate:0.1.0")
}
```

## 2. Start the SDK and wrap it

```kotlin
import com.octetproof.magistrate.Magistrate
import com.octetproof.sdk.api.Octet
import com.octetproof.sdk.api.OctetConfig

val sdk = Octet.start(context = applicationContext, config = OctetConfig())
val magistrate = Magistrate(sdk)
```

To keep the verified policy across launches, pass a store:

```kotlin
val magistrate = Magistrate(sdk, persistentStore = EncryptedPrefsPolicyBundleStore(context))
```

## 3. Ask for a decision

Call `decide` when the user does something your policy protects, such as opening an account or placing a bet. It is a `suspend` function.

```kotlin
val license = sdk.licenseStatus ?: return   // activation didn't complete

try {
    val result = magistrate.decide(license)
    if (result.cause != null) {
        // UNKNOWN: the location couldn't be settled, for example no fix yet.
        // Ask the user to retry.
    }
    // Send result.receipt to your backend with the request.
    // Your backend verifies it and decides what happens.
    api.submit(order, decisionReceipt = result.receipt)
} catch (e: PolicyException) {
    // No decision was made. Treat it as NO.
}
```

`decide` runs the whole exchange: it fetches the policy, checks Octet's signature on it, gets a one-time challenge, has the SDK make the proofs, and sends them to the engine. Don't manage the challenge yourself, unless your backend starts the check (see [Start the check from your backend](/docs/magistrate/get-started/verify-and-enforce/#start-the-check-from-your-backend)).

A `deny` is a result, not an error. `result.isPermit` is `false` and `result.reasons` says why. When `result.cause` is set, the answer is UNKNOWN: the proofs couldn't settle the location. A `PolicyException` means no decision was made at all. See [Decision Response](/docs/magistrate/reference/decision/#yes-no-and-unknown) to read a result as YES, NO or UNKNOWN.

:::warning[The app is not where you enforce]

`result.isPermit` is fine for the screen you show next, but a user can change anything that runs on their device. Your backend enforces the decision, from the verified receipt. See [Verify and Enforce](/docs/magistrate/get-started/verify-and-enforce/).

:::

## Several policies in one check

`decideMany` decides 1 to 16 policies with one set of proofs. The device proves once, and the engine decides each policy from the same proofs. Each result carries its policy's name, and `forPolicy(policy)` looks one up.

```kotlin
val set = magistrate.decideMany(license, policies = listOf("p_...", "p_..."))
set.results.forEach { println("${it.policy} ${it.decision}") }
val one = set.forPolicy("p_...")
```

## Lower-level calls

Most apps only need `decide`. Two building blocks are public as well:

```kotlin
suspend fun prove(license: LicenseStatus, nonce: String): List<PolicyProof>
suspend fun evaluate(license: LicenseStatus, decisionRef: String): PolicyEvaluation
```

`prove` makes the location proofs the policy needs, bound to a nonce. Your backend uses it to [start the check itself](/docs/magistrate/get-started/verify-and-enforce/#start-the-check-from-your-backend), together with `attestationEnrolment()` and `challengeEnv(license)`. `evaluate` reports a verdict for each region of the policy (`PolicyRegionResult`), without asking the engine for a decision.

## Obligations

A policy can attach obligations to a `permit`, for example a step-up authentication. A permit with obligations counts only once your app and backend have fulfilled every one. Treat an obligation you don't recognise as NO. See [Decision Response](/docs/magistrate/reference/decision/#obligations).

## Things to know

- **The first check after start.** Location takes a moment to warm up after `Octet.start`. The first check can come back UNKNOWN, with `cause` `absence`. Retry after a few seconds.
- **Real devices for production.** Production needs hardware attestation on a real device with a release-signed build. An emulator or a debug build signs in with a sandbox token (see the [Android Quickstart](/docs/mobile/getting-started/android-quickstart/)), and its results and receipts say `env` `sandbox`. Your backend accepts only `release` in production. Checks from a sandbox build also need a sandbox app registered on your license.
- **When the policy can't be refreshed.** Magistrate falls back to the last policy bundle it verified, but only for the same bundle URL, and only while that bundle still verifies against your license's current keys.
- **Geofence policies.** A policy with a `disc` needs an Octet SDK release that can prove one. Until then `decide` throws `BindingUnavailable` for it. Policies from the Magistrate Studio don't use discs.
- **Errors.** See [Errors](/docs/magistrate/reference/errors/) for each `PolicyException` case.

## Verify the download

The Maven artifacts on the `mvn-repo` branch carry the standard `.md5`, `.sha1`, `.sha256` and `.sha512` checksum files. Each release also attaches the AAR, `SHASUMS256.txt`, its Sigstore bundle `SHASUMS256.txt.sigstore.json`, a CycloneDX SBOM and the third-party notices.

The `sign-release.yml` workflow in the `octet-magistrate-android` repository signs `SHASUMS256.txt` with [Sigstore](https://www.sigstore.dev/) keyless signing when a release is published, so the signing certificate names that workflow and the exact release tag.

To check a release yourself, download its assets into one folder and run these commands with [cosign](https://docs.sigstore.dev/cosign/system_config/installation/) 2.x:

```sh
V=0.1.0
IDENTITY="https://github.com/octetproof/octet-magistrate-android/.github/workflows/sign-release.yml@refs/tags/$V"
cosign verify-blob --bundle SHASUMS256.txt.sigstore.json \
  --certificate-identity "$IDENTITY" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  SHASUMS256.txt              # must print "Verified OK"
sha256sum -c SHASUMS256.txt   # every file must print "OK"
```

A signature from any other workflow, repository or tag fails the `cosign` step.

## Security

Report a vulnerability privately to [security@octetproof.com](mailto:security@octetproof.com). Bug reports and questions are welcome as issues in the `octet-magistrate-android` repository.

## Sample app

The [sample app](https://github.com/octetproof/octet-magistrate-android/tree/main/sample) starts the SDK, calls `decide` and shows the decision, reasons and obligations. It needs a real device and a release-signed build.

## License

Magistrate is licensed under the [Octet SDK License Agreement](/terms/mobile/v06.11.2026/).
