Docs/Magistrate/Get Started

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 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 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.

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.

// 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

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:

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.

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).

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 to read a result as YES, NO or UNKNOWN.

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.

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.

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:

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, 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.

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), 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 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 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 2.x:

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. Bug reports and questions are welcome as issues in the octet-magistrate-android repository.

Sample app

The sample app 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.

TermsPrivacy© 2026 Understone, Inc. d/b/a Octet