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

# iOS SDK

> Integrate your iOS app via the Embedded SDK for iOS.

## Installation

**Requirements: iOS 14.0+, Xcode 15.0+, Swift 5.7+**

The iOS SDK ships as pre-built frameworks. Add them to your Xcode target:

| Framework                       | Action                |
| ------------------------------- | --------------------- |
| `PlaudBleSDK.framework`         | Embed & Sign          |
| `PlaudWiFiSDK.framework`        | Embed & Sign          |
| `PlaudDeviceBasicSDK.framework` | Embed & Sign          |
| `PlaudDeviceBasicSDK.bundle`    | Copy Bundle Resources |

Frameworks are located at `sdk/ios/` in the [Plaud SDK repo](https://github.com/Plaud-AI/plaud-template-app/tree/main/sdk/ios).

<Note>
  SDK frameworks are compiled for `arm64` (physical devices only). Simulator is not supported.
</Note>

<Tip>
  **Try the Plaud Embedded Skill** to have your coding agent help you with your iOS implementation.

  ```bash theme={"system"}
  npx skills add Plaud-AI/plaud-embedded-skills
  ```

  Visit our [GitHub repo](https://github.com/Plaud-AI/plaud-embedded-skills.git) for more details on the skill.
</Tip>

***

## Methods

### Plaud Device SDK Initialization

The SDK is initialized with a **User Token** and your **regional** domain.

<Note>
  If you haven't onboarded to the Plaud Developer Platform, see our [quickstart onboarding steps](/plaud-embedded/quickstart#onboard-to-the-plaud-developer-platform).

  If you'd like more details on how to retrieve your User Token and the token exchange flow, see the [Authentication API reference](/plaud-embedded/auth-api-overview).
</Note>

```swift Swift icon="swift" theme={"system"}
import PlaudDeviceBasicSDK

PlaudDeviceAgent.shared.initSDK(
    userAccessToken: "user-token",
    customDomain: "platform-us.plaud.ai"  // domain only, no https://
)
```

<ParamField path="userAccessToken" type="string" required>
  User Access Token (JWT), used for device authentication. The handshake token is automatically parsed from the JWT `sub` field.
</ParamField>

<ParamField path="customDomain" type="string" required>
  Your regional Plaud server domain **without `https://` prefix**. All SDK network requests use this domain.

  For more information on how to find your regional server domain, see the [Authentication API docs](/plaud-embedded/auth-api-overview#find-your-region).
</ParamField>

#### Refreshing User Token

If the User Token is refreshed (e.g., after re-login), you can update it:

```swift Swift icon="swift" theme={"system"}
PlaudDeviceAgent.shared.setUserAccessToken(newToken)
```

<ParamField path="newToken" type="string" required>
  Refreshed User Token (JWT)
</ParamField>

This automatically updates the handshake token and refreshes the RSA key pair.

***

### Connecting (binding) to a Plaud Device

The `.startScan()` will call the `bleScanResult` callback defined in the [**PlaudDeviceAgentProtocol**](#plauddeviceagentprotocol)

```swift Swift icon="swift" theme={"system"}
PlaudDeviceAgent.shared.delegate = self

PlaudDeviceAgent.shared.startScan()

extension DeviceManager: PlaudDeviceAgentProtocol {
    func bleScanResult(bleDevices: [BleDevice]) {
    //...
        let match = bleDevices.first(where: { $0.serialNumber == lastSN }),
        PlaudDeviceAgent.shared.connectBleDevice(bleDevice: match, deviceToken: userId)
    }
    //...
}
```

<ParamField path="bleDevice" type="BleDevice" required>
  A scanned/connected device
</ParamField>

<ParamField path="deviceToken" type="deviceToken">
  If needed, a unique identifier for that device
</ParamField>

#### PlaudDeviceAgentProtocol Callbacks

The [**PlaudDeviceAgentProtocol**](#plauddeviceagentprotocol) is a delegate protocol with a few key callbacks for the **PlaudDeviceAgent**. See the [Protocols](#protocols) section for the full list of callbacks.

| Callback                                                                              | Description                     |
| ------------------------------------------------------------------------------------- | ------------------------------- |
| `bleScanResult(bleDevices: [BleDevice])`                                              | Scan results updated            |
| `bleConnectState(state: Int)`                                                         | 1 = connected, 0 = disconnected |
| `bleBind(sn:status:protVersion:timezone:)`                                            | Binding status updated          |
| `blePenState(state:privacy:keyState:uDisk:findMyToken:hasSndpKey:deviceAccessToken:)` | Device state changed            |

<Warning>
  Binding is also tied to app installation. If a user is **uninstalling your mobile app, make sure to unbind your Plaud device.**
</Warning>

***

### Unbinding a device

Plaud devices **can only be bound to one application at a time**. This is done to properly secure and encrypt files stored on a Plaud device. When a user wants to unbind a Plaud device (whether to use with another Plaud Embedded App or the core Plaud app), use the `.depair` method.

```swift Swift icon="swift" theme={"system"}
PlaudDeviceAgent.shared.depair(clear: true)
```

<ParamField path="clear" type="Bool" required>
  **Set this to `true`** to clear all connections
</ParamField>

***

### File Synchronization

The `exportAudio` method exports audio files from Plaud device to your users' phone, reporting progress through the [**AudioExportCallback**](#audioexportcallback). The `.getFileList` accesses files on your users' Plaud device, and the `.deleteFile` method will delete an audio file off of your users' Plaud device.

```swift Swift icon="swift" theme={"system"}
// Get file list from device
PlaudDeviceAgent.shared.getFileList(startSessionId: 0)

extension SyncManager: AudioExportCallback {
    public func onComplete(outputPath: String) {
        // ...
        PlaudDeviceAgent.shared.exportAudio(
            sessionId: sessionId,
            outputDir: outputDir,
            format: .wav,
            channels: 1,
            callback: self
        )
    }
}

// Delete file from device
PlaudDeviceAgent.shared.deleteFile(sessionId: sessionId)
```

<ParamField path="sessionId" type="Int" required>
  Session ID
</ParamField>

<ParamField path="outputDir" type="String" required>
  Output Directory
</ParamField>

<ParamField path="format" type="AudioExportFormat" required>
  .opus | .mp3 | .wav | .pcm
</ParamField>

<ParamField path="channels" type="Int">
  pcm = 0, mp3 = 1, wav = 2, opus = 3
</ParamField>

<ParamField path="callback" type="AudioExportCallback" required>
  See [**AudioExportCallback**](#audioexportcallback) for details.

  func onProgress(\_ progress: Int, message: String)

  func onComplete(outputPath: String)

  func onError(\_ error: String)
</ParamField>

<Note>
  For large audio files, we recommend UX considerations:

  * Progress indicators and setting expectations for long transfers (i.e. "This file is large. May take \~X minutes")
  * Supporting **background/resumable** transfer so closing the app doesn't kill the session.
  * Using WiFi Fast Transfer (see below)
</Note>

***

### WiFi Fast Transfer

An alternative to a BLE (Bluetooth Low Energy) file transfer, WiFi Fast Transfer is \~10x faster than BLE transfers.

<Note>
  Requires the `Hotspot Configuration` entitlement in your iOS app settings.
</Note>

```swift theme={"system"}
PlaudDeviceAgent.shared.setDeviceWiFi(open: true)

extension DeviceManager: PlaudDeviceAgentProtocol {
    func bleWiFiOpen(_ status: Int, _ wifiName: String, _ wholeName: String, _ wifiPass: String) {
        PlaudWiFiAgent.shared.bleDevice = BleAgent.shared.bleDevice
        PlaudWiFiAgent.shared.connectWifi(ssid: wifiName, password: wifiPass)
    }
}

private class WiFiExportHandler: NSObject, AudioExportCallback {
    //...
    func onComplete(outputPath: String) {
        PlaudWiFiAgent.shared.exportAudioViaWiFi(
            sessionId: next.sessionId,
            outputDir: outputDir,
            format: AudioExportFormat.opus,
            channels: 1,
            callback: handler
        )
    }
}
```

#### Protocol Callbacks

The WiFi Fast Transfer has two key callbacks, one on the [**PlaudDeviceAgentProtocol**](#plauddeviceagentprotocol) and another on the [**AudioExportCallback**](#audioexportcallback).

| Protocol                  | Callback                                                                                  | Description                            |
| ------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------- |
| PlaudDeviceAgent Protocol | `bleWiFiOpen(_ status: Int, _ wifiName: String, _ wholeName: String, _ wifiPass: String)` | Called when Plaud device opens hotspot |
| AudioExportCallback       | `onComplete`                                                                              | Called when a file has downloaded      |

<Note>
  While faster than BLE transfers, we still recommend the following UX considerations for WiFi Fast Transfer syncs:

  * Progress indicators and setting expectations for long transfers (i.e. "This file is large. May take \~X minutes")
  * Supporting **background/resumable** transfer so closing the app doesn't kill the session.
</Note>

***

### Firmware Update (OTA)

The Embedded SDK handles the entire OTA flow: version query → download → MD5 verify → CRC → BLE packet push → device restart → reconnect.

#### Check for Firmware Updates

```swift Swift icon="swift" theme={"system"}
// Check for update
PlaudDeviceAgent.shared.checkFirmwareUpdate { result in
    guard result.hasUpdate else { return }
    print("New version: \(result.latestVersion), release notes: \(result.releaseNotes)")
}
```

<ParamField path="firmwareCheck" type="(PlaudFirmwareCheckResult) -> Void" required>
  Callback function with type PlaudFirmwareCheckResult

  <Expandable title="PlaudFirmwareCheckResult">
    <ParamField path="hasUpdate" type="Bool">
      Whether a firmware update is available
    </ParamField>

    <ParamField path="currentVersion" type="String">
      The device's current firmware version
    </ParamField>

    <ParamField path="latestVersion" type="String">
      The latest available firmware version
    </ParamField>

    <ParamField path="versionCode" type="Int">
      Numeric version code
    </ParamField>

    <ParamField path="releaseNotes" type="String">
      Release notes for the update
    </ParamField>

    <ParamField path="downloadUrl" type="String">
      URL to download the firmware
    </ParamField>

    <ParamField path="md5" type="String">
      MD5 checksum for verification
    </ParamField>

    <ParamField path="isForce" type="Bool">
      Whether the update is mandatory
    </ParamField>
  </Expandable>
</ParamField>

#### Download Firmware Update

```swift Swift icon="swift" theme={"system"}
// One-call firmware update
PlaudDeviceAgent.shared.startFirmwareUpdate(
    progress: { phase, percentage in
        // phase: .downloading / .installing / .restarting / .complete
        // percentage: 0.0 ~ 1.0
    },
    completion: { result in
        if result.success {
            print("Updated to \(result.version)")
        } else {
            print("Failed: \(result.errorMessage ?? "")")
        }
    }
)
```

<ParamField path="progress" type="(PlaudFirmwarePhase, Float) -> Void" required>
  Callback that reports firmware update progress.

  <Expandable title="PlaudFirmwarePhase">
    | Case          | Description                               |
    | ------------- | ----------------------------------------- |
    | `downloading` | Firmware binary is being downloaded       |
    | `installing`  | Firmware is being installed on the device |
    | `restarting`  | Device is restarting after installation   |
    | `complete`    | Update finished successfully              |
  </Expandable>

  * `phase`: Current phase of the update (`PlaudFirmwarePhase`)
  * `percentage`: Progress from 0.0 to 1.0
</ParamField>

<ParamField path="completion" type="(PlaudFirmwareUpdateResult) -> Void" required>
  Callback when the update completes.

  <Expandable title="PlaudFirmwareUpdateResult">
    <ParamField path="success" type="Bool">
      Whether the update succeeded
    </ParamField>

    <ParamField path="version" type="String">
      The firmware version after update
    </ParamField>

    <ParamField path="errorMessage" type="String">
      Error description if update failed
    </ParamField>
  </Expandable>
</ParamField>

If you already have the firmware file downloaded, use `pushFirmwareFile()` instead:

#### Push Firmware Update

```swift Swift icon="swift" theme={"system"}
PlaudDeviceAgent.shared.pushFirmwareFile(
    filePath: localPath,
    toVersion: "V1.2.8",
    progress: { phase, pct in },
    completion: { result in }
)
```

<ParamField path="filePath" type="String" required>
  Full path to the locally downloaded firmware file
</ParamField>

<ParamField path="toVersion" type="String" required>
  Target firmware version to update to (e.g., "V1.2.8")
</ParamField>

<ParamField path="progress" type="(PlaudFirmwarePhase, Float) -> Void" required>
  Callback that reports firmware update progress.

  * `phase`: Current phase (`downloading` / `installing` / `restarting` / `complete`)
  * `percentage`: Progress from 0.0 to 1.0
</ParamField>

<ParamField path="completion" type="(PlaudFirmwareUpdateResult) -> Void" required>
  Callback when the update completes with `success`, `version`, and `errorMessage` fields.
</ParamField>

***

## Protocols

The Embedded SDK is delegate-driven. You implement protocols and assign yourself as the delegate to receive device events, transfer progress, and results. These three protocols should cover most use cases.

<Note>
  Most callbacks are delivered on the SDK's internal dispatch queues — **not** the main thread. Marshal to the main queue before touching UIKit or published state.
</Note>

### PlaudDeviceAgentProtocol

The primary delegate for `PlaudDeviceAgent`. Assign it once and it drives the entire BLE lifecycle — scan, connect, bind, device state, recording, and file sync.

```swift Swift icon="swift" theme={"system"}
PlaudDeviceAgent.shared.delegate = self

extension DeviceManager: PlaudDeviceAgentProtocol {
    // REQUIRED — overall device state after handshake
    func blePenState(state: Int, privacy: Int, keyState: Int, uDisk: Int,
                     findMyToken: Int, hasSndpKey: Int, deviceAccessToken: Int) {
        // ...
    }
}
```

<Note>
  `blePenState` is the **only required** member. Every other callback is `@objc optional` — implement only the ones you need.
</Note>

The most commonly used callbacks, grouped by concern:

| Group        | Callback                                                                                  | Description                                                       |
| ------------ | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| Connection   | `blePenState(...)`                                                                        | **Required.** Device state reported after the handshake completes |
| Connection   | `bleConnectState(state: Int)`                                                             | `1` = connected, `0` = disconnected                               |
| Connection   | `bleScanResult(bleDevices: [BleDevice])`                                                  | Scan results updated                                              |
| Connection   | `bleScanOverTime()`                                                                       | Scan timed out with no results                                    |
| Connection   | `bleBind(sn:status:protVersion:timezone:)`                                                | Binding status updated                                            |
| Device state | `bleStorage(total: Int, free: Int, duration: Int)`                                        | Storage usage on the device                                       |
| Device state | `bleChargingState(isCharging: Bool, level: Int)`                                          | Charging / battery level changed                                  |
| Recording    | `bleRecordStart(sessionId:start:status:scene:startTime:reason:)`                          | Recording started                                                 |
| Recording    | `bleRecordStop(sessionId:reason:fileExist:fileSize:)`                                     | Recording stopped                                                 |
| Recording    | `blePcmData(sessionId:millsec:pcmData:isMusic:)`                                          | Live PCM chunks for waveform / metering                           |
| File sync    | `bleFileList(bleFiles: [BleFile])`                                                        | Result of `getFileList(...)`                                      |
| File sync    | `bleData(sessionId: Int, start: Int, data: Data)`                                         | A chunk of file data during sync                                  |
| File sync    | `bleDataComplete()`                                                                       | File transfer finished                                            |
| File sync    | `bleDeleteFile(sessionId: Int, status: Int)`                                              | Result of `deleteFile(...)`                                       |
| WiFi         | `bleWiFiOpen(_ status: Int, _ wifiName: String, _ wholeName: String, _ wifiPass: String)` | Device opened its hotspot — hand off to `PlaudWiFiAgent`          |
| OTA          | `bleFotaResult(uid: Int, status: Int, errmsg: String?)`                                   | Firmware push result                                              |

<Note>
  The SDK also exposes a lower-level `BleAgentProtocol` on `BleAgent` with 60+ **required** callbacks. Prefer `PlaudDeviceAgentProtocol` — the facade handles the handshake, decryption, and format conversion for you, and lets you implement only the callbacks you care about.
</Note>

### AudioExportCallback

Reports progress, completion, and errors for `.exportAudio` (BLE) and `.exportAudioViaWiFi` (WiFi).

```swift Swift icon="swift" theme={"system"}
extension SyncManager: AudioExportCallback {
    func onProgress(_ progress: Int, message: String) {
        // progress: 0–100
    }
    func onComplete(outputPath: String) {
        // decoded file is ready at outputPath
    }
    func onError(_ error: String) {
        // ...
    }
}
```

<ParamField path="onProgress(_ progress: Int, message: String)" type="func" required>
  Export progress, `0`–`100`, with a human-readable status message.
</ParamField>

<ParamField path="onComplete(outputPath: String)" type="func" required>
  Called when decoding finishes; `outputPath` is the full path to the written file.
</ParamField>

<ParamField path="onError(_ error: String)" type="func" required>
  Called if export fails, with a description of the error.
</ParamField>

### PlaudWiFiAgentProtocol

The delegate for `PlaudWiFiAgent`, used during WiFi Fast Transfer. Assign it before calling `connectWifi(...)`. The handshake must complete (`wifiHandshake` with status `0`) before listing or transferring files.

```swift Swift icon="swift" theme={"system"}
PlaudWiFiAgent.shared.delegate = self

extension DeviceManager: PlaudWiFiAgentProtocol {
    func wifiHandshake(_ status: Int) {
        guard status == 0 else { return }   // 0 = handshake done, ready to transfer
        // begin transfer
    }
}
```

All members are `@objc optional`.

| Callback                                                                                  | Description                                      |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `wifiHandshake(_ status: Int)`                                                            | `0` = handshake complete, ready to list/transfer |
| `wifiConnectionStatus(_ ssid: String, _ connected: Bool)`                                 | WiFi connection state changed                    |
| `wifiFileList(_ files: [BleFile])`                                                        | Result of `getFileList(...)` over WiFi           |
| `wifiSyncFileData(_ sessionId:_ offset:_ count:_ binData:)`                               | A chunk of file data                             |
| `wifiDataComplete()`                                                                      | Single-file transfer finished                    |
| `wifiDownloadAllProgress(_ totalFiles:_ currentFileIndex:_ currentFile:_ totalProgress:)` | Progress while downloading all files             |
| `wifiDownloadAllCompleted(_ completedFiles: Int, _ failedFiles: Int)`                     | Batch download finished                          |
| `wifiCommonErr(_ cmd: Int, _ status: Int)`                                                | A command failed                                 |
| `wifiClose(_ status: Int)`                                                                | WiFi transfer session closed                     |

<Note>
  WiFi Fast Transfer requires the `Hotspot Configuration` entitlement in your app. See [WiFi Fast Transfer](#wifi-fast-transfer) for the full connect-and-transfer flow.
</Note>
