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

# License keys

> How the offline license key works, how to install it, and what updatesUntil means for a perpetual license with maintenance.

The binary SDK is gated by a license key that NEUROFIT issues for your app identifiers. Verification happens
entirely on the device: the SDK checks the key's signature against a public key compiled into the SDK. There is
no network call, no persistence and no telemetry. Source licensees receive a key of type `source` and use the
same call.

## Installing the key

Call `activate` once, early in your app's life and before creating a session. The key is a single string.

<CodeGroup>
  ```swift Swift theme={null}
  let status = try VitalsSDK.activate(licenseKey: Secrets.neurofitVitalsLicenseKey)
  ```

  ```kotlin Kotlin theme={null}
  val status = VitalsSDK.activate(context, BuildConfig.NEUROFIT_VITALS_LICENSE_KEY)
  ```

  ```java Java theme={null}
  LicenseStatus status = VitalsSDK.activate(context, BuildConfig.NEUROFIT_VITALS_LICENSE_KEY);
  ```

  ```typescript React Native theme={null}
  const status = await NeurofitVitals.activate(licenseKey);
  ```

  ```typescript Capacitor theme={null}
  const status = await NeurofitVitals.activate({ licenseKey });
  ```

  ```dart Flutter theme={null}
  final status = await NeurofitVitals.activate(licenseKey);
  ```
</CodeGroup>

Keep the key out of source control the same way you would any configuration value: an `xcconfig` that is not
committed, a `BuildConfig` field fed from `local.properties` or CI secrets, or your existing config system. The
key is bound to your app identifiers and cannot be used by another app, so a leaked key does not let anyone else
ship the SDK, but it is still licensed material.

`activate` is synchronous and fast, and safe from any thread. After it succeeds, `VitalsSDK.licenseStatus`
returns the parsed status; before that it is `nil` / `null`, and creating a session (or calling `start()`) throws
`notActivated`. The app identifier it checks is `Bundle.main.bundleIdentifier` on iOS and the application id
(`context.packageName`) on Android. The key is held in memory only; activate again on every launch.

## Key format

```
NFV1.<base64url(payload JSON)>.<base64url(ECDSA P-256 signature)>
```

The payload is JSON with these fields:

| Field | Meaning |
| - | - |
| `v` | Payload version, the integer `1`. |
| `licensee` | Your organisation's name, as it appears in the license agreement. Non-empty. |
| `apps` | The iOS bundle identifiers and Android application ids the key is valid for. A non-empty list of non-empty strings. |
| `type` | `binary` or `source`. |
| `updatesUntil` | The last SDK build date this key accepts, `YYYY-MM-DD` (a real calendar date). See below. |
| `features` | Licensed features. `["vitals"]` in 1.0.0. Optional in the payload; `LicenseStatus.features` is empty when absent. |
| `issued` | Issue date, `YYYY-MM-DD`. Optional. |
| `kid` | Identifier of the NEUROFIT signing key, used for key rotation. Optional. |

`LicenseStatus` exposes `licensee`, `apps`, `features`, `updatesUntil`, `type` (iOS `LicenseType.binary` /
`.source`, Android `LicenseType.BINARY` / `SOURCE`) and `warnings`.

## What updatesUntil means

Your license is perpetual. `updatesUntil` is the end of your maintenance period, and it works like this:

* Every SDK build carries a build date (`VitalsSDK.buildDate`, for example `2026-09-30`).
* A key accepts any SDK build whose build date is on or before `updatesUntil`.
* An SDK build released after `updatesUntil` refuses the key with `licenseUpdatesExpired(updatesUntil:)`.
* Builds you already ship keep working forever. Nothing expires inside your users' installed apps.

So when maintenance lapses you keep using the last SDK version released during your maintenance period; when you
renew, NEUROFIT issues a key with a later `updatesUntil` and newer builds accept it. The check compares dates
inside the key and the SDK only; it never reads the device clock, so a wrong system time cannot break a reading.

## Verification order and errors

The checks run in this order and stop at the first failure:

| Check | Fails with | Notes |
| - | - | - |
| Leading and trailing ASCII whitespace (space, tab, CR, LF) is trimmed; the rest must be pure ASCII, exactly three dot-separated segments, the first `NFV1`, the other two non-empty strict base64url (no padding, canonical) | `licenseInvalid` | Malformed key (copy-paste error, truncated string, a stray quote or line break inside). |
| The ECDSA P-256 / SHA-256 signature verifies over the literal bytes `NFV1.<payload>` with the compiled-in public key | `licenseInvalid` | Wrong key, edited payload, or a key for another product. |
| The payload is a JSON object with the typed fields above (`v` the integer `1`, dates exactly `YYYY-MM-DD`) | `licenseInvalid` | |
| SDK build date on or before `updatesUntil` (string comparison of two `YYYY-MM-DD` dates) | `licenseUpdatesExpired(updatesUntil:)` | Maintenance lapsed for this build. |
| The app identifier is listed in `apps`, compared exactly and case-sensitively | `licenseNotValidForApp(appId:)` | The running app's identifier is not on the key. |

Swift throws `VitalsError`; Kotlin throws the matching `VitalsException` subclass (`LicenseInvalid`,
`LicenseUpdatesExpired`, `LicenseNotValidForApp`); the wrappers reject with the same case name in `code`.

### Debug builds

The app-identifier check can be relaxed for development: instead of throwing `licenseNotValidForApp`, `activate`
succeeds and adds a message to `LicenseStatus.warnings` naming the licensed apps. This lets you run the sample
apps and your own debug flavours under identifiers that are not on the key. The two platforms decide differently:

* **Android** decides at runtime: the relaxation applies when the host app is debuggable
  (`ApplicationInfo.FLAG_DEBUGGABLE`). A debug build of your app gets the warning; a release build is rejected.
* **iOS** decides at compile time of the SDK module (`#if DEBUG` inside `NeurofitVitals`). With the SwiftPM source
  integration that follows your build configuration, so Debug builds get the warning. The binary xcframework is
  built Release and never relaxes the check, whatever your app's configuration: use a key that lists every bundle
  identifier you run under, including development ones.

Release builds enforce the check on both platforms.

### Several app identifiers

One key can list several identifiers (production, staging, a white-label variant). Ask NEUROFIT to include every
identifier you ship under. Adding an identifier later means a new key; the old key keeps working.

## Rotation and re-issue

Keys are signed by NEUROFIT's production signing key and carry its `kid`. If NEUROFIT ever rotates the signing
key, the SDK build that carries the new public key ships together with re-issued keys for every licensee under
maintenance, and the release notes say so. Builds you already ship are unaffected. Lost keys are re-issued on
request; there is nothing to revoke on the device.

## Source licenses

A source license ships the engine and SDK sources. Your build still calls `activate` with a key of type `source`,
which verifies the same way. Because you build from source, you can also replace the compiled-in public key or the
check itself; the license agreement, not the key, is what governs your use.

## Questions

Licensing questions go to [contact@neurofit.app](mailto:contact@neurofit.app). Include your licensee name and
the `kid` from your key.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.