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

Start with the iOS SDK's high-level interfaces (**PlaudDeviceAgent and PlaudWiFiAgent**) outlined on this page to handle:

1. **Device Connection**: Connecting to & disconnecting from Plaud devices via mobile app
2. **File Management**: Syncing files from Plaud device to mobile app
3. **Firmware Updates**: Updating Plaud device firmware

These methods should cover majority of Plaud Embedded use cases. For advanced usage, see the [advanced iOS SDK usage](/plaud-embedded/advanced-ios-sdk).

## Installation

**Requirements: iOS 14.0+, Xcode 16.0+**

The iOS SDK ships as pre-built frameworks.

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

<Steps>
  <Step title="Clone the Plaud SDK repo">
    ```bash theme={"system"}
    git clone https://github.com/Plaud-AI/plaud-sdk-public.git
    ```
  </Step>

  <Step title="Copy the Framework files and add them to your Xcode target">
    Frameworks are located at `sdk/ios/` in the [Plaud SDK repo](https://github.com/Plaud-AI/plaud-sdk-public/tree/main/sdk/ios).

    ```bash theme={"system"}
    cp -R sdk/ios your/ios-app/library
    ```
  </Step>
</Steps>

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

***

## Getting Started

Import `PlaudDeviceAgent` and `PlaudWiFiAgent` through `PlaudDeviceBasicSDK`. Both facades are accessed through their shared singleton:

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

let deviceAgent = PlaudDeviceAgent.shared
let wifiAgent = PlaudWiFiAgent.shared

// Initialize once with your user token and regional domain
deviceAgent.initSDK(
    userAccessToken: "user-token",
    customDomain: "platform-us.plaud.ai"  // domain only, no https://
)

// Assign delegates to receive device and transfer events
deviceAgent.delegate = self
wifiAgent.delegate = self
```

Use the facade methods and callbacks to drive device interactions between your mobile app and your users' Plaud devices.

***

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

Binding a Plaud device generates a key pair using the **user token** and creates an ownership lock on your users' Plaud device. This makes sure their files on device is always encrypted and can only be decrypted with a valid user token by your application.

Binding is a two step process:

1. **Local bind** over BLE, where the Embedded SDK runs the encrypted handshake and generates the key pair on the device

2. **Cloud bind** that records the device/owner association so you can track device status remotely and via API.

<Steps>
  <Step title="Scan and connect over BLE">
    `.startScan()` calls 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]) {
            guard let device = bleDevices.first(where: { $0.serialNumber == targetSN }) else { return }
            PlaudDeviceAgent.shared.connectBleDevice(bleDevice: device, deviceToken: userId)
        }

        func bleConnectState(state: Int) {
            // 1 = GATT link up (handshake still pending), 0 = disconnected,
            // 2 / -1 / -2 = connection failed. Wait for bleBind before declaring success.
        }
    }
    ```

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

    <ParamField path="deviceToken" type="String">
      If needed, a unique identifier for that device. There is also a `connectBleDevice(bleDevice:)` overload that omits it.
    </ParamField>
  </Step>

  <Step title="Cloud bind to sync binding states">
    `bleBind(status: 0)` means the handshake completed and the key pair exists on the device. Call the `sdk/bind` API after a successful call.

    <Tip>
      Re-binding a device to the same owner is idempotent server-side, so the repeat calls have no side effects. See [Binding a Plaud Device to a User](/plaud-embedded/device-binding-api-overview#binding-a-plaud-device-to-a-user) for the full endpoint reference.
    </Tip>

    ```swift Swift icon="swift" theme={"system"}
    func bleBind(sn: String?, status: Int, protVersion: Int, timezone: Int) {
        guard status == 0, let sn = sn else { return }   // 0 = handshake complete
        cloudBind(sn: sn)
    }

    // POST https://platform-us.plaud.ai/developer/api/open/partner/sdk/bind
    private func cloudBind(sn: String) {
        let body: [String: String] = ["type": snType, "sn": sn]

        var request = URLRequest(url: URL(string: "https://platform-us.plaud.ai/developer/api/open/partner/sdk/bind")!)
        request.httpMethod = "POST"
        request.setValue("Bearer \(userAccessToken)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try? JSONSerialization.data(withJSONObject: body)

        URLSession.shared.dataTask(with: request) { _, response, _ in
            // 403 = the device is already bound to another account
        }.resume()
    }
    ```

    <ParamField path="type" type="String" required>
      Device type string derived from the SN prefix (`881` → `notepro`, `882` → `notepins`).
    </ParamField>

    <ParamField path="sn" type="String" required>
      Device serial number.
    </ParamField>
  </Step>
</Steps>

#### 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` = **GATT link up, handshake still pending**, `0` = disconnected, `2` / `-1` / `-2` = connection failed |
| `bleBind(sn:status:protVersion:timezone:)` | Handshake complete — `status == 0` is the real connection success |
| `blePenState(state:privacy:keyState:uDisk:findMyToken:hasSndpKey:deviceAccessToken:)` | Device state reported after the handshake |
| `bleDepair(_ status: Int)` | Device confirmed the depair |
| `bleConnectStage(sn: String?, stage: String, detail: String?)` | Observability into the device handshake process |
| `bleState(powered: Bool)` | Phone Bluetooth power state |

<Warning>
  Plaud devices can only be bound to one mobile application. If a user is **uninstalling your mobile app, make sure to unbind your Plaud device.**
</Warning>

***

### Depair (unbind) a device

#### Standard unbind

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), unbind over both cloud and BLE.

Unbinding via the cloud allows you to track Connected Device statuses remotely and via API.

<Steps>
  <Step title="Unbind from the cloud">
    Removes the device/owner association in the Plaud registry (visible on the [Plaud developer portal](https://portal.plaud.ai/)). Unbinding an already-unbound device is idempotent, so multiple calls have no side effects. See [Unbinding a Plaud Device to a User](/plaud-embedded/device-binding-api-overview#unbinding-a-plaud-device-to-a-user) for the full endpoint reference.

    ```swift Swift icon="swift" theme={"system"}
    // POST https://platform-us.plaud.ai/developer/api/open/partner/sdk/unbind
    let body: [String: String] = ["type": snType, "sn": sn]

    var request = URLRequest(url: URL(string: "https://platform-us.plaud.ai/developer/api/open/partner/sdk/unbind")!)
    request.httpMethod = "POST"
    request.setValue("Bearer \(userAccessToken)", forHTTPHeaderField: "Authorization")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONSerialization.data(withJSONObject: body)

    URLSession.shared.dataTask(with: request).resume()
    ```

    <ParamField path="type" type="String" required>
      Device type string derived from the SN prefix (`881` → `notepro`, `882` → `notepins`).
    </ParamField>

    <ParamField path="sn" type="String" required>
      Device serial number.
    </ParamField>
  </Step>

  <Step title="Depair the device over BLE">
    Clears the ownership lock on the device itself using the `.depair(clear: true)` method. On success the SDK disconnects and clears the session. The device reports the outcome on the `bleDepair(_ status: Int)` callback, but the standard unbind flow does not wait on it — tear down your app state immediately and let the callback land whenever the device answers.

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

    extension DeviceManager: PlaudDeviceAgentProtocol {
        func bleDepair(_ status: Int) {
            // 0 = depaired. The standard unbind does not wait on this.
        }
    }

    PlaudDeviceAgent.shared.depair(clear: true)
    ```

    <ParamField path="clear" type="Bool" default="false">
      Clears the SDK's stored connection/bond state for the device.

      **Pass `false` during [Device Recovery](#device-recovery)**
    </ParamField>
  </Step>
</Steps>

#### Device Recovery

In certain situations, the cloud and local bind state can go **out-of-sync**, causing the device to lock. The firmware still holds a previous user's client ID, so the current user's handshake is rejected — on iOS this usually surfaces as GATT connect followed by a silent drop.

Device recovery re-handshakes with each previously bound client ID using the `GET /sdk/binding` endpoint. On a match, the stale ownership lock is wiped and the device can be bound to the current user. No reflashing tool is needed.

<Note>
  Recovery only applies when the cloud reports the device as **unbound** (`is_bind` is `false` or `null`). If `is_bind` is `true`, the device is genuinely owned by another account and that owner must unbind it first. The SDK cannot override an active binding.
</Note>

<Steps>
  <Step title="Get all previous clients">
    Use the [`sdk/binding` API](/plaud-embedded/device-binding-api-overview#recovering-a-plaud-device) to retrieve all previously bound clients, including bindings in other apps.

    ```swift Swift icon="swift" theme={"system"}
    // GET /developer/api/open/partner/sdk/binding?type={type}&sn={sn}
    let url = URL(string: "https://platform-us.plaud.ai/developer/api/open/partner/sdk/binding?type=\(snType)&sn=\(sn)")!
    var request = URLRequest(url: url)
    request.setValue("Bearer \(userAccessToken)", forHTTPHeaderField: "Authorization")

    URLSession.shared.dataTask(with: request) { data, _, _ in
        guard let data = data,
              let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return }
        let isBind = json["is_bind"] as? Bool              // nil = signed but never bound
        let bindHistory = (json["bind_history"] as? [String]) ?? []
    }.resume()
    ```

    <ParamField path="type" type="String" required>
      Device type string derived from the SN prefix (`881` → `notepro`, `882` → `notepins`).
    </ParamField>

    <ParamField path="sn" type="String" required>
      Device serial number.
    </ParamField>

    **Response:**

    <ParamField path="is_bind" type="Bool | null">
      `true` = bound to another account (stop — recovery is not possible), `false` = unbound, `null` = signed but never bound. Use Device Recovery only when this is **not** `true`.
    </ParamField>

    <ParamField path="bind_history" type="String[]">
      Previously bound client IDs, newest first.

      <Warning>
        `bind_history` records one entry **per bind event**, so the same client ID repeats for every past connect — 50+ duplicates of a single ID is normal. **Dedupe before you bound the list**, or all your attempts will retry the same key.
      </Warning>
    </ParamField>
  </Step>

  <Step title="Run device recovery on each client">
    For each previous client ID, `recoveryConnectBleDevice` uses that ID as the handshake token and connects with force-clear, so the firmware wipes its stale key material. The current user's SN signature and RSA material are reused — nothing has to be signed for the historical user — and the SDK restores the current user's handshake identity when the attempt ends.

    On a match the device accepts the handshake (`bleBind(status: 0)` / `blePenState`). Then depair to wipe the stale bond, rescan (depair changes the device's MAC, so the cached `BleDevice` is stale), and reconnect as the current user, which rebinds the device.

    ```swift Swift icon="swift" theme={"system"}
    // Try one previously bound client ID (dedupe bind_history first).
    PlaudDeviceAgent.shared.recoveryConnectBleDevice(bleDevice: bleDevice, historicalUserId: historicalId)

    extension DeviceManager: PlaudDeviceAgentProtocol {
        func bleBind(sn: String?, status: Int, protVersion: Int, timezone: Int) {
            guard status == 0 else { return }                 // this ID matched the firmware lock
            PlaudDeviceAgent.shared.depair(clear: false)      // wipe the stale bond
        }

        func bleDepair(_ status: Int) {
            PlaudDeviceAgent.shared.disconnect()
            PlaudDeviceAgent.shared.startScan()               // depair changes the MAC — rescan
        }

        func bleScanResult(bleDevices: [BleDevice]) {
            guard let fresh = bleDevices.first(where: { $0.serialNumber == sn }) else { return }
            // Reconnect as the current user — this rebinds the device.
            PlaudDeviceAgent.shared.connectBleDevice(bleDevice: fresh, deviceToken: currentUserId)
        }

        func bleConnectState(state: Int) {
            // 0 / 2 / -1 / -2 while awaiting the handshake = this ID was refused;
            // disconnect and try the next bind_history entry.
        }
    }
    ```

    <ParamField path="bleDevice" type="BleDevice" required>
      The scanned device, from `bleScanResult`.
    </ParamField>

    <ParamField path="historicalUserId" type="String" required>
      One `bind_history` entry to try as the handshake token. Accepts either the raw `client_user_id` or its 32-hex form.
    </ParamField>
  </Step>
</Steps>

***

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

<Note>
  Plaud devices will record **up to 5 hours**. Recordings longer should be broken up.
</Note>

Request the file list, then export each session from the `bleFileList` callback. `exportAudio` reports back on an [**AudioExportCallback**](#audioexportcallback) you supply — the delegate below and the export callback are separate objects.

```swift Swift icon="swift" theme={"system"}
PlaudDeviceAgent.shared.getFileList(startSessionId: 0)

extension SyncManager: PlaudDeviceAgentProtocol {
    func bleFileList(bleFiles: [BleFile]) {
        guard let next = bleFiles.first else { return }
        exportHandler = ExportHandler()        // retain it — see the note below
        PlaudDeviceAgent.shared.exportAudio(
            sessionId: next.sessionId,
            outputDir: outputDir,
            format: .mp3,
            channels: 1,
            callback: exportHandler!
        )
    }
}

private final class ExportHandler: NSObject, AudioExportCallback {
    func onProgress(_ progress: Int, message: String) { }
    func onComplete(outputPath: String) {
        // decoded file is ready at outputPath
    }
    func onError(_ error: String) { }
}
```

<Note>
  `AudioExportCallback` is an `@objc` protocol, so your conformer must be an `NSObject` subclass.
</Note>

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

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

<ParamField path="format" type="AudioExportFormat" required>
  `.pcm` (0) | `.mp3` (1) | `.wav` (2) | `.opus` (3). We recommend `.mp3` — it plays everywhere and is accepted directly by the transcription upload API.
</ParamField>

<ParamField path="channels" type="Int" default="1">
  Number of audio channels in the exported file (`1` = mono).
</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** transfers so the app can sync files while the screen is off or another app is in the foreground.
    * See our [background sync reference](/plaud-embedded/background-syncs)
  * Supporting **resumable** transfer so the app can resume transfers after closing.
  * Using WiFi Fast Transfer (see below)
</Note>

#### Delete File from Device

Once an audio file has been synced, you can delete the file from your users' Plaud device with the `.deleteFile` method.

```swift Swift icon="swift" theme={"system"}
PlaudDeviceAgent.shared.deleteFile(sessionId: sessionId)
```

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

***

### WiFi Fast Transfer

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

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

`PlaudWiFiAgent` is a high-level facade that manages the WiFi Fast Transfer lifecycle for most cases.

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

extension DeviceManager: PlaudDeviceAgentProtocol {
    func bleWiFiOpen(_ status: Int, _ wifiName: String, _ wholeName: String, _ wifiPass: String) {
        guard status == 0 else { return }

        PlaudWiFiAgent.shared.bleDevice = BleAgent.shared.bleDevice
        PlaudWiFiAgent.shared.delegate = self

        // 2. Give the device ~3s to bring its hotspot fully up before joining 
        //    join to the SDK with timeout and retry
        DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
            PlaudWiFiAgent.shared.connectWifi(wholeName, wifiPass, 180)
        }
    }
}

extension DeviceManager: PlaudWiFiAgentProtocol {
    func wifiHandshake(_ status: Int) {
        guard status == 0 else { return }
        PlaudWiFiAgent.shared.getFileList(Int(Date().timeIntervalSince1970), 0, false)
    }

    func wifiFileList(_ files: [BleFile]) {
        guard let next = files.first else { return }
        wifiExportHandler = WiFiExportHandler()   // retain it — the SDK does not
        PlaudWiFiAgent.shared.exportAudioViaWiFi(
            sessionId: next.sessionId,
            outputDir: outputDir,
            format: .mp3,
            channels: 1,
            callback: wifiExportHandler!
        )
    }
}
```

<ParamField path="ssid" type="String" required>
  The `wholeName` value from `bleWiFiOpen`.
</ParamField>

<ParamField path="passphrase" type="String" required>
  The `wifiPass` value from `bleWiFiOpen`.
</ParamField>

<ParamField path="overtimeSec" type="Int" default="60">
  Join timeout in seconds. The SDK re-applies the hotspot configuration and retries internally until handshake or timeout.
</ParamField>

#### Ending Wifi Fast Transfer

Close the session on every exit path — success, failure, and user cancel — or the device stays in WiFi mode (and keeps draining battery) until its own \~2 minute firmware timeout.

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

<Note>
  `endWiFiTransfer()` only reaches the device while BLE is up.

  Use `PlaudDeviceAgent.shared.isWiFiTransferActive` to check whether a session is currently open.
</Note>

For lower-level control, you can use `PlaudWiFiAgent.shared.syncFile`.

<Accordion title="Low-level WiFi Fast Transfer implementation">
  ```swift theme={"system"}
  PlaudWiFiAgent.shared.syncFile(
       file.sessionId, // sessionId
      0, // start offset
      0, // end (0 = whole file)
      file.scenes // scene — match the file's own value, not the default 1
  )

  extension SyncManager: PlaudWiFiAgentProtocol {
      func wifiSyncFile(_ sessionId: Int, _ status: Int) {
          // status == 0 → accepted; non-zero → rejected (e.g. scene mismatch)
      }
      func wifiSyncFileData(_ sessionId: Int, _ offset: Int, _ count: Int, _ binData: Data) {
          // Append binData at offset to your file handle
      }
      func wifiDataComplete() {
          // All bytes received — close the file
      }
      func wifiSyncFileStop(_ status: Int) {
          // Transfer stopped/aborted (also triggered by stopSyncFile(_:_ scene:))
      }
  }
  ```

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

  <ParamField path="start" type="Int" required>
    Start offset in bytes. Use `0` to transfer from the beginning, or a `BleFile.offset` to resume.
  </ParamField>

  <ParamField path="end" type="Int" default="0">
    End offset in bytes. `0` transfers the whole file.
  </ParamField>

  <ParamField path="scene" type="Int" default="1">
    The file's scene, from `BleFile.scenes`. Must match the file's own value or the transfer is rejected; defaults to `1`.
  </ParamField>

  <Note>
    If you encounter the error message: `wifiCommonErr(cmd: 16, status: 0)`, this is an expected behavior and indicates a successful sync.
  </Note>
</Accordion>

#### 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** transfers so the app can sync files while the screen is off or another app is in the foreground.
    * See our [background sync reference](/plaud-embedded/background-syncs)
  * Supporting **resumable** transfer so the app can resume transfers after closing.
</Note>

***

### Firmware Update (OTA)

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

<Warning>
  Firmware updates will wipe recordings from your users' Plaud device. Make sure their recordings are synced/exported to your API services before pushing firmware updates!
</Warning>

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

#### Run the Firmware Update

`startFirmwareUpdate` performs the whole flow in one call — download, install, and device restart — reporting each stage through the `progress` closure.

```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` = GATT link up (**not** connection success — the handshake follows), `0` = disconnected, `2` / `-1` / `-2` = connection failed. See [Connecting (binding) to a Plaud Device](#connecting-binding-to-a-plaud-device) |
| Connection | `bleConnectStage(sn: String?, stage: String, detail: String?)` | The stages include `"start"`, `"gatt_connect"`, `"set_notify"`, `"set_battery_notify"`, `"read_battery"`, `"set_data_notify"`, `"pre_handshake"`, `"send_rsa_public"`, `"first_handshake"`, `"two_handshake"`, `"handshake_get_ssn"`, `"change_handshake_timeout"`, `"sync_time"` |
| Connection | `bleScanResult(bleDevices: [BleDevice])` | Scan results updated |
| Connection | `bleScanOverTime()` | Scan timed out with no results |
| Connection | `bleBind(sn:status:protVersion:timezone:)` | Handshake complete — `status == 0` is the real connection success |
| Connection | `bleDepair(_ status: Int)` | Device confirmed the depair |
| Connection | `bleState(powered: Bool)` | Phone Bluetooth power state |
| 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 97 members, **96 of them required**. 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(_ uid:_ sessionId:_ single:)` over WiFi — all three arguments are unlabeled; pass a unique `uid` (e.g. a timestamp), `0` for `sessionId` to list everything, and `false` for `single` |
| `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>


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