# Add Magistrate to Your iOS 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 iOS, which needs Octet SDK 3.1.2. See the [Changelog](/docs/magistrate/reference/changelog/) for what each release changes.

## Prerequisites

- iOS 16 or later, Swift 5.9 or later.
- The Octet Mobile SDK set up in your app, with the App Attest capability and the location and motion usage descriptions in `Info.plist`. Follow the [iOS Quickstart](/docs/mobile/getting-started/ios-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 policy published from the [Magistrate Studio](/docs/magistrate/get-started/write-a-policy/).

## 1. Add the packages

Add both Swift packages to your app. Swift Package Manager doesn't pass the Octet SDK through Magistrate, so your app adds it as well. Pin it to exactly the version this Magistrate release uses, 3.1.2.

```swift
dependencies: [
    .package(url: "https://github.com/octetproof/octet-magistrate-ios", exact: "0.1.0"),
    .package(url: "https://github.com/octetproof/octet-sdk-ios", exact: "3.1.2"),
],
targets: [
    .target(name: "YourApp", dependencies: [
        .product(name: "Magistrate", package: "octet-magistrate-ios"),
        .product(name: "OctetSDK", package: "octet-sdk-ios"),
    ]),
]
```

Magistrate ships as a precompiled `xcframework`. Octet SDK 3.1.2 is a dynamic framework, which Xcode embeds and signs with your app.

## 2. Start the SDK and wrap it

```swift
import OctetSDK
import Magistrate

let sdk = try await Octet.start(config: OctetConfig())
let magistrate = Magistrate(sdk: sdk)
```

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

```swift
let magistrate = Magistrate(sdk: sdk, persistentStore: KeychainPolicyBundleStore())
```

## 3. Ask for a decision

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

```swift
guard let license = sdk.licenseStatus else { return }   // activation didn't complete

do {
    let result = try await magistrate.decide(license: license)
    if result.cause != nil {
        // 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.
    try await api.submit(order, decisionReceipt: result.receipt)
} catch let error as PolicyError {
    // 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 `PolicyError` 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(_:)` looks one up.

```swift
let set = try await magistrate.decideMany(license: license,
                                          policies: ["p_...", "p_..."])
for decision in set.results {
    print(decision.policy, decision.decision)
}
let one = set.forPolicy("p_...")
```

## Lower-level calls

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

```swift
func prove(license: LicenseStatus, nonce: String) async throws -> [PolicyProof]
func evaluate(license: LicenseStatus, decisionRef: String) async throws -> 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. A simulator or a debug build signs in with a sandbox token (see the [iOS Quickstart](/docs/mobile/getting-started/ios-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 `PolicyError` case.

## Verify the download

The package's `binaryTarget` pins the `xcframework` by checksum, so Swift Package Manager refuses a zip that differs from the released one. Each release also attaches `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-ios` 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-ios/.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"
shasum -a 256 -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-ios` repository.

## Sample app

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

## License

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