# Capacitor reference

> Every public type in the FacePing Capacitor plugin: setup, enrolment, verification, the camera screen and results.

Source: https://docs.faceping.ai/sdk/capacitor/

Package: [`@faceping/capacitor`](https://www.npmjs.com/package/@faceping/capacitor) (npm, preview, under the `preview` tag). Starter app: [faceping/capacitor-starter](https://github.com/faceping/capacitor-starter). Capacitor 8. Android 7 (API 24) or later, on 64-bit phones (`arm64-v8a`) and `x86_64` emulators. iOS 16 or later with Swift Package Manager, or iOS 15.1 or later with CocoaPods (see [iOS versions](#ios-versions)); Xcode 26 or later.

The plugin is a thin bridge over the native [Android](https://docs.faceping.ai/sdk/android/) and [iOS](https://docs.faceping.ai/sdk/ios/) SDKs, so the face checks and results behave exactly as they do there. It works in any Capacitor 8 app, whatever the web framework: Ionic, Angular, React, Vue or plain TypeScript. FacePing runs on the phone, not in a web browser: in `ionic serve` or a desktop browser, every call throws an `unavailable` error.

## Install

```bash
npm install @faceping/capacitor@preview
npx cap sync
```

Then:

- **iOS:** add `NSCameraUsageDescription` to `ios/App/App/Info.plist` (Xcode: the **App** target's **Info** tab, **Privacy - Camera Usage Description**). Without it, iOS stops the app when the camera starts.
- **Android:** nothing to add. The FacePing library declares the `CAMERA` and `INTERNET` permissions.

### iOS versions

The FacePing iOS SDK needs iOS 15.1 or later.

- **Swift Package Manager** (the default for new Capacitor 8 apps): Capacitor writes the app's iOS version into its package as a whole number, so iOS 15.1 becomes iOS 15 and the build fails with "requires minimum platform version 15.1". Set the **App** target's **Minimum Deployments** to **iOS 16.0**, then run `npx cap sync ios`.
- **CocoaPods:** set the deployment target to 15.1 or later (`platform :ios, '15.1'` in the Podfile).

### How the native SDKs get into your app

- **Android:** the plugin depends on `ai.faceping:faceping-android` from Maven Central. Gradle fetches it like any other dependency.
- **iOS, Swift Package Manager:** the plugin's package depends on [faceping/faceping-ios](https://github.com/faceping/faceping-ios), a binary Swift package. Xcode downloads the framework from its GitHub release and checks its checksum.
- **iOS, CocoaPods:** `pod install` downloads `FacePing.xcframework.zip` once from the [faceping-ios release](https://github.com/faceping/faceping-ios/releases), checks its SHA-256 against the checksum pinned in the package, and vendors it. Offline or behind a proxy, set `FACEPING_IOS_ZIP=/path/to/FacePing.xcframework.zip` (checked the same way) or `FACEPING_IOS_URL` to a mirror before `npx cap sync ios`.

The simulator has no front camera: test the live check on an iPhone.

## Setup

```ts
import { FacePing } from '@faceping/capacitor';

// Once, early. Resolves at once and loads the face models in the background.
await FacePing.init({ sandboxKey: 'fp_test_…' });   // sandbox: faces enrolled on the device
// await FacePing.init({ deviceToken: 'fpd_…' });   // production: faces synced from your check-in list

// Activates the sandbox key now, so a key problem shows up before the first person arrives.
await FacePing.sync();
```

`FacePing` is a single object you can call from anywhere in the app. Calling `init` again with the same options is fine (for example after a live reload); with different options it throws (call `close()` first). It resolves to `{ isSandbox, nativeSdkVersion }`.

A key in the wrong format throws a `setup` error from `init`. A revoked key, or a first run with no connection, shows up on the first call that needs the key, so call `sync()` at start-up.

### Options

| Option | Type | Default | Notes |
|---|---|---|---|
| `sandboxKey` | `string` | | A `fp_test_` key from [**Developers → Sandbox keys**](https://app.faceping.ai/developers). |
| `deviceToken` | `string` | | Production. A `fpd_` token for one check-in list. Set this or `sandboxKey`, not both. |
| `region` | `'eu'` | `'eu'` | Your account's region, which sets the API address. Today the only value is EU (`https://api-eu.faceping.ai/`). |
| `apiBaseUrl` | `string` | | Overrides the region's address. UK and US regions are available on request: we give you the address to put here. |
| `passiveLiveness` | `boolean` | `true` | Passive liveness on every check. Turn it off only if your app runs its own liveness check first. |
| `autoSyncInterval` | `number \| null` | `300` | Production only, in seconds. `null` turns automatic sync off; call `sync()` yourself. |

## FacePing

| Call | Returns | Notes |
|---|---|---|
| `enroll(id, photo?, camera?)` | `EnrollResult` | Sandbox only. With no photo, opens the camera screen and enrols the photo the person takes. Production enrolment happens with consent on your server or through the FacePing widget. Replaces any face already stored under `id`. |
| `verifyLive(id, camera?)` | `VerifyResult` | A live check on the camera screen: match, a head turn in a random direction, match again. The screen shows the prompts and closes itself when the check is done. About 10 seconds at most. |
| `verify(id, photo?, camera?)` | `VerifyResult` | One photo, passive liveness only. With no photo, opens the camera screen. |
| `forget(id)` | `boolean` | Sandbox: deletes the face now; `false` if there was none. Production: throws `invalidOperation` (remove the person through your server). |
| `forgetAll()` | | Deletes every face on the device now. In production the next sync brings back the people still enrolled. |
| `enrolledIds()` | `string[]` | Who can be verified on this device, as of the last enrol, forget or sync. |
| `sync()` | `SyncResult` | Production: downloads the latest faces for the device's check-in list; a revoked device wipes its faces. Sandbox: activates the key, or renews its licence when due. Either way it refreshes `enrolledIds()`, so it's a good first call when the app starts. |
| `close()` | | Stops automatic sync and frees the engine. Call `init` again to start over. |

Every call returns a promise.

## The camera screen

A web view can't hold a native camera view, so FacePing brings its own screen. When you call `enroll` or `verify` without a photo, or `verifyLive`, the plugin opens a full-screen native camera over your app, with a title, the oval face guide and a cancel button:

- **`enroll` and `verify`:** the person lines up their face and taps **Take photo**.
- **`verifyLive`:** the check starts at once, with the liveness prompts ("Turn your head left", "Look back at the camera").

When the check is done, the guide turns green or red for a moment and the screen closes itself; then the promise resolves with the result. The screen asks for camera permission the first time it opens. If the person taps **Cancel** (or Android's back button), the call throws a `FacePingError` with the code `canceled`.

```ts
const result = await FacePing.verifyLive(ticketId, {
  title: 'Look at the camera to check in',
  cancelLabel: 'Use my ticket instead',
});
```

| `camera` option | Type | Default | Notes |
|---|---|---|---|
| `title` | `string` | "Enrol your face" (`enroll`), "Verify your face" (`verify`, `verifyLive`) | The line at the top of the screen. |
| `captureLabel` | `string` | "Take photo" | The capture button, for `enroll` and `verify`. |
| `cancelLabel` | `string` | "Cancel" | The cancel button. |
| `resultDelayMs` | `number` | `800` | How long the result stays on screen before it closes, in milliseconds (0 to 10,000). |

Only one camera screen can be open at a time: a second call while one is open throws `invalidOperation`.

To translate or reword the liveness prompts, use the native SDKs' strings: on Android the string resources `faceping_prompt_look_at_camera`, `faceping_prompt_turn_left`, `faceping_prompt_turn_right` and `faceping_prompt_look_back`; on iOS a `FacePing.strings` table with the keys `faceping.prompt.lookAtCamera`, `faceping.prompt.turnLeft`, `faceping.prompt.turnRight` and `faceping.prompt.lookBack`.

## Photos

A `photo` is any of:

| From | Example |
|---|---|
| Nothing: the camera screen | `FacePing.enroll('me')` |
| A file path or URI (`file://…`, or on Android `content://…`), for example the `path` from [`@capacitor/camera`](https://capacitorjs.com/docs/apis/camera) | `FacePing.enroll('me', photo.path)` or `{ uri }` |
| Base64 JPEG or PNG (HEIC too on iOS) | `{ base64 }`, or a `data:image/jpeg;base64,…` URI |

Photos are turned upright from their EXIF orientation and scaled down to 1280 pixels on the device before anything else happens.

## Backups

The face store's encryption keys stay in the phone's Keychain or Keystore. On iOS, FacePing's folder is already excluded from backups. On Android, exclude `files/faceping` and the shared preferences `faceping.keys` from Auto Backup, or turn backup off (`android:allowBackup="false"` in `android/app/src/main/AndroidManifest.xml`).

## Results

```ts
type EnrollResult = {
  outcome: 'enrolled' | 'noFace' | 'multipleFaces' | 'faceTooSmall' | 'notLive' | 'limitReached';
  id: string;
  isEnrolled: boolean;
};

type VerifyResult = {
  outcome: 'match' | 'noMatch' | 'notEnrolled' | 'noFace' | 'livenessFailed'
         | 'challengeFailed' | 'expired' | 'leaseExpired';
  similarity: number;           // cosine similarity; the match threshold is 0.363
  livenessScore: number | null; // passive liveness of the deciding frame
  elapsedMs: number;            // how long the check took on the device
  isSandbox: boolean;
  isMatch: boolean;
};

type SyncResult = {
  status: 'ok' | 'gone' | 'revoked' | 'unavailable';
  enrolled: number;
  leaseUntil: Date | null;      // production: verifies offline until then
  deleteAfter: Date | null;
};
```

The outcome types are exported as `EnrollOutcome` and `VerifyOutcome`. `limitReached` means the sandbox already holds 25 faces on this device: forget one first. `leaseExpired` (production) means the device hasn't synced for longer than its lease, 24 hours by default; it needs a connection before it verifies again.

## Errors

Set-up and device problems throw a `FacePingError` with a `message` ready to show and a `code`. `isFacePingError(e, 'canceled')` checks for one.

| Code | When |
|---|---|
| `setup` | The key or token is missing, invalid or revoked, the sandbox key couldn't be activated (the first run needs a connection), or `init` hasn't been called. |
| `model` | The face models couldn't load on this device. Send people to your normal way in. |
| `invalidOperation` | Not allowed in this mode or state, for example enrolling on the device in production, or a second camera screen. |
| `invalidArgument` | A bad argument: an empty id, or a photo that isn't a readable image. |
| `storage` | The device's secure storage failed. |
| `camera` | The camera couldn't start (permission refused, no front camera) or didn't deliver a picture. |
| `canceled` | The person closed the camera screen before the check finished. |
| `unavailable` | Not running in the native app: a web browser, or the app wasn't synced (`npx cap sync`) and rebuilt after installing the plugin. |
| `unknown` | Anything else. The message says what. |

Everything else is a result, not an exception: a face check never throws because someone looked away.
