# Get Partner Token
Source: https://docs.plaud.ai/api-reference/authentication-api/get-partner-token
/openapi/auth.json post /oauth/partner/access-token
Application-level token. Exchange your `client_id` and `secret_key` for a partner access token, which is used to mint individual user tokens.
# Get User Token
Source: https://docs.plaud.ai/api-reference/authentication-api/get-user-token
/openapi/auth.json post /open/partner/users/access-token
User-level token. Exchange a partner access token for a per-user access token. This is the token the Embedded SDK and file upload endpoints accept.
# Refresh Partner Token
Source: https://docs.plaud.ai/api-reference/authentication-api/refresh-partner-token
/openapi/auth.json post /oauth/partner/access-token/refresh
Exchange a valid `refresh_token` for a new partner access token. Refresh before the current token expires.
# Bind Device
Source: https://docs.plaud.ai/api-reference/device-binding-api/bind-device
/openapi/binding.json post /open/partner/sdk/bind
Registers the device/owner association in the Plaud registry. Re-binding a device to the same owner is idempotent, so multiple calls will not have side effects.
# Get Device Binding
Source: https://docs.plaud.ai/api-reference/device-binding-api/get-device-binding
/openapi/binding.json get /open/partner/sdk/binding
Returns the remote binding state of a device along with every client ID it has previously been bound to, including bindings made in other apps.
Use this endpoint to check device state remotely, and to drive [Device Recovery](/plaud-embedded/ios-sdk#device-recovery) when the cloud and local bind state go out of sync and the device locks. Recovery only applies when `is_bind` is **not** `true`; if `is_bind` is `true` the device is owned by another account and that owner must unbind it first.
# Unbind Device
Source: https://docs.plaud.ai/api-reference/device-binding-api/unbind-device
/openapi/binding.json post /open/partner/sdk/unbind
Removes the device/owner association in the Plaud registry. Unbinding an already-unbound device is idempotent, so multiple calls have no side effects.
Plaud devices can only be bound to one application at a time. Unbind over both cloud and BLE — call this endpoint and call `depair(clear: true)` on the Embedded SDK — so the cloud and local bind state stay in sync.
# Complete Multipart Upload
Source: https://docs.plaud.ai/api-reference/file-upload-api/complete-multipart-upload
/openapi/file.json post /open/partner/files/upload/complete-upload
Merge the uploaded parts into a single file.
Submit the `file_id` and `upload_id` from generate-presigned-urls along with the `ETag` collected from each part's `PUT` response.The returned `DownloadUrl` is valid for **24 hours**.
# Generate Presigned Upload URLs
Source: https://docs.plaud.ai/api-reference/file-upload-api/generate-presigned-upload-urls
/openapi/file.json post /open/partner/files/upload/generate-presigned-urls
Request presigned S3 URLs for a multipart upload.
The file is split into chunks of 5 MB with one presigned URL per part.
For each part returned:
1. **Send raw bytes to S3** — `PUT PresignedUrl` the raw bytes of the chunk to (no auth needed).
2. **Collect the `ETag`** from each S3 response header — These will be needed in the [complete-upload endpoint](/api-reference/file-upload-api/complete-multipart-upload).
# Get Transcription Task
Source: https://docs.plaud.ai/api-reference/transcription-api/get-transcription-task
/openapi/transcription.json get /open/partner/ai/transcriptions/{transcription_id}
Retrieve the status and results of a transcription task. Keep polling while the status is `PENDING`, `RECEIVED`, `STARTED`, or `PROGRESS`. When the status is `SUCCESS`, the `data` object is populated.
# Submit Audio for Transcription
Source: https://docs.plaud.ai/api-reference/transcription-api/submit-audio-for-transcription
/openapi/transcription.json post /open/partner/ai/transcriptions/
Submit an audio file for transcription. Pass the pre-signed download URL returned by complete-upload. Transcription runs asynchronously — use the returned `transcription_id` to poll for results.
Rate Limit: **60 req/min**, Max recording duration: **24 hr**, Max diarization: **6 hr**
# Platform Overview
Source: https://docs.plaud.ai/overview
Build with real-world conversation data — power your own product with our device SDK and transcription API, or automate workflows by extracting your Plaud data through the MCP.
**If you're building your own user-facing product**, integrate your mobile app with:
1. **Plaud devices** for your users to record on first-class hardware
2. **Plaud's transcription models** to turn speech to insights
Plaud becomes infrastructure for your application.
Connect your agent to **your personal Plaud data** to access all of your recordings and notes.
1. Search your recordings
2. Retrieve transcripts
3. AI-generated notes
Works with any MCP-compatible or terminal-based agent.
## Build Your Way
Whether you are:
* Building a product on top of Plaud's hardware and infrastructure with [Plaud Embedded](/plaud-embedded/overview)
* Connecting your agents to your personal Plaud data with [Plaud MCP & CLI](/plaud-mcp-cli/mcp)
**Start building on Plaud's Developer Platform** for your conversation use cases.
# Low-Level Android SDK Methods
Source: https://docs.plaud.ai/plaud-embedded/advanced-android-sdk
Low-level Embedded SDK methods for advanced use cases
## High-level Facades
The Plaud Embedded Android SDK has two high-level interfaces:
1. **PlaudDeviceAgent**: a Kotlin `object` you call statically
2. **IWifiTransferAgent**: the WiFi Fast Transfer session and file operations
These high-level methods and callbacks should cover most use cases including:
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
See our [Android SDK reference](/plaud-embedded/android-sdk) for examples and documentation on these high-level facades.
```
Your app
│
├── PlaudDeviceAgent ─────┬───────────────┐ (High-level)
├── IWifiTransferAgent ───┤ │
│ ▼ ▼
│ NiceBuildSdk FirmwareUpdate
│ (low-level) Manager
│ │ │
│ ▼ │
│ IBleAgent ◄──────────┘
│ (low-level)
└─────────────────────────┴───────────────────► device
```
Additionally, using PlaudDeviceAgent abstracts many details that low-level interfaces will not handle.
| Managed Behavior | Purpose |
| --------------------------- | ------------------------------------------------------------------------------------------------------ |
| Response management | One listener to handle different response objects (`StorageRsp`, `GetStateRsp`, …) |
| Callback plumbing | Supplies the `OnRequest` / `OnResponse` pairs every `IBleAgent` call requires |
| Audio pipeline | `exportAudio` handles download, caching, E2EE decrypt, and transcode to `mp3` / `wav` / `opus` / `pcm` |
| Firmware orchestration | Version query, MD5 verify, CRC, BLE packet push, and reconnect after restart |
| Optional listener callbacks | `PlaudDeviceAgentListener` has **0** required members — override only what you need |
You can mix the high-level and low-level facades. For example if there's a behavior that `PlaudDeviceAgent` does not include, you can use `IBleAgent` to cover that functionality.
## Low-level Facades
The Android SDK has a few lower-level interfaces that `PlaudDeviceAgent` wraps over:
1. **IBleAgent**: the BLE protocol library
2. **NiceBuildSdk**: helper SDK for crypto and cloud API calls
3. **PartnerApiManager**: manages the backend endpoints for device signing and RSA keypair generation
**We do not recommend using these low-level facades as main drivers. Use them as coverage for functionality not exposed by PlaudDeviceAgent**.
The `workflow` methods and models have been deprecated and will be removed in future SDK versions.
***
### IBleAgent Reference
`IBleAgent` is the raw BLE transport, reached through the protocol library's `TntAgent` singleton:
```kotlin Kotlin icon="android" theme={"system"}
import com.tinnotech.penblesdk.TntAgent
import com.tinnotech.penblesdk.core.IBleAgent
val ble: IBleAgent = TntAgent.getInstant().bleAgent
```
It exposes **165 methods** — roughly four times the facade's surface — so most device features the SDK supports but `PlaudDeviceAgent` doesn't expose are reachable here.
#### The callback convention
Every `IBleAgent` command takes an `OnRequest` / `OnResponse` pair instead of reporting to a global listener.
```kotlin Kotlin icon="android" theme={"system"}
ble.getStorage(
object : AgentCallback.OnRequest {
override fun onRequestResult(success: Boolean, code: Int, msg: String?) {
// the command reached the device (or didn't)
}
},
object : AgentCallback.OnResponse {
override fun onResponse(rsp: StorageRsp) {
// the device replied — typed response object, not primitives
}
}
)
```
`OnRequest` fires when the command is written to the device; `OnResponse` fires when the device answers. A successful `OnRequest` with no `OnResponse` means the device accepted the command but never replied.
#### Device Connection
| `PlaudDeviceAgent` | `IBleAgent` |
| ------------------------------------------------------- | -------------------------------------------------------------------- |
| `startScan()` | `scanBle(true)` |
| `stopScan()` | `scanBle(false)` |
| `isConnected()` | `isBtConnected()` |
| `connectBleDevice(bleDevice, deviceToken)` | `connectionBLE(device, handshakeToken, deviceToken, "", false)` |
| `recoveryConnectBleDevice(bleDevice, historicalUserId)` | `connectionBLE(device, , "", "", true)` |
| `disconnect()` | `disconnectBle()` |
| `depair(clear)` | `depair(clear, onRequest, onResponse)` |
| `getState()` | `getState(onRequest, onResponse)` |
```kotlin Kotlin icon="android" theme={"system"}
val ble = TntAgent.getInstant().bleAgent
TntAgent.getInstant().addBleAgentListeners(myBleAgentListener)
ble.scanBle(true)
// From scanBleDeviceReceiver(...) on your BleAgentListener:
ble.connectionBLE(
device, // the scanned BleDevice
NiceBuildSdk.resolveHandshakeToken(userJwt), // handshake token from the JWT `sub`
deviceToken, // your per-device token
"", // userName
false // isForceClear
)
```
The device to connect to, as delivered by `scanBleDeviceReceiver(...)`.
Parsed from the user JWT's `sub` claim. `NiceBuildSdk.resolveHandshakeToken(...)` does this for you — the facade calls it internally.
Per-device token. This is the value `PlaudDeviceAgent.connectBleDevice(bleDevice, deviceToken)` forwards; pass `""` for the no-token overload.
Owner name written to the device. The facade passes `""` (iOS passes `"Plaud"`).
`connectBleDevice(...)` passes `false`; `recoveryConnectBleDevice(...)` passes `true`. See [Device Recovery at the BLE Layer](#device-recovery-at-the-ble-layer).
#### Recording & Device State
```kotlin Kotlin icon="android" theme={"system"}
// Recording — each takes OnRequest/OnResponse callbacks
ble.startRecord(scene, onRequest, onResponse)
ble.stopRecord(onRequest, onResponse)
ble.recordPause(sessionId, onRequest, onResponse)
ble.recordResume(sessionId, onRequest, onResponse)
ble.startImmediateRecord(...); ble.stopImmediateRecord(...)
ble.startSyncRecord(...)
// State reads
ble.getState(...); ble.getStorage(...); ble.getDeviceStatus(...)
ble.getBatteryLevel(...); ble.isCharging(); ble.getBattStatus(...)
ble.getSerialNumber(...); ble.getSsn(...); ble.getMacAddress()
ble.getCurrentConnectedDevice(); ble.getCurrentConnectedBleMac()
ble.isRecording(); ble.isScanningBle(); ble.isSupportWifi(); ble.isUsbState()
```
`IBleAgent` also exposes the full device-settings surface the facade only partially wraps. Each setting is a `get…` / `set…` pair.
```kotlin Kotlin icon="android" theme={"system"}
// Backlight
getBackLightTime() / setBackLightTime(...)
getBackLightBrightness() / setBackLightBrightness(...)
// Language, scene & mode
getLanguage() / setLanguage(...)
getRecScene() / setRecScene(...)
getRecMode() / setRecMode(...)
openVAD(...)
getVadSensitivity() / setVadSensitivity(...)
// Gains & audio
getVpuGain() / setVpuGain(...)
getMICGain() / setMICGain(...)
getVpuCLK() / setVpuCLK(...)
getBatteryMode() / setBatteryMode(...)
getAudioChannel()
isOggAudio(); isNoNsAgc()
// Automation toggles
getAutoRecord() / setAutoRecord(...)
getAutoStopRecord() / setAutoStopRecord(...)
getAutoPowerOff() / setAutoPowerOff(...)
getAutoSync() / setAutoSync(...)
getAutoDeleteRecordFile() / setAutoDeleteRecordFile(...)
getSaveRAWFile() / setSaveRAWFile(...)
getFindMyState() / setFindMyState(...)
getIBeaconWakeup() / setIBeaconWakeup(...)
getSwitchHandlerId() / setSwitchHandlerId(...)
// Naming, LED, alarm, privacy
getBleDeviceName() / setBleDeviceName(...)
getLedStatus() / setLedStatus(...)
getAlarmRec() / setAlarmRec(...)
setPrivacy(...); setActive(...); restoreFactorySettings(...)
// Generic channel for settings without a dedicated method
getCommonParams(...) / setCommonParams(...) / commonSettings(...)
// Device logs, markings, WiFi credentials, websocket, misc
getDeviceLogFileList(...); startSyncDeviceLogFile(...); stopSyncDeviceLogFile(...)
deleteDeviceLogFile(...)
getRecMarkings(...); getRecTags(...)
getWifiList(...); getWifiInfo(...); setSyncWifi(...); deleteWifiInfo(...)
testWifiInfo(...); testWifiResult(...); getRouterSsid(...) / setRouterSsid(...)
getWebsocket(...) / setWebsocket(...); setWebsocketUrl(...); testWebsocket(...)
getSDFlashCID(...); resetPassword(...); resetFindMy(...)
setSoundPlusToken(...); syncTime(...); bleRateTest(...)
setMtu(...); setPriorityHigh(); setPriorityLow(); updateRssi(...)
```
#### File Sync
```kotlin Kotlin icon="android" theme={"system"}
ble.getRecSessions(startSessionId, onRequest, onResponse)
ble.syncFileStart(sessionId, start, end, onRequest, onHeadRsp, onTailRsp, voiceDataCollector)
ble.syncFileStop(onRequest, onResponse)
ble.syncFileDel(sessionId, onRequest, onResponse)
ble.clearRecordFile(onRequest, onResponse)
ble.fileDataCheck(...)
```
The session ID to start listing from. `0` lists everything.
The recording to sync or delete.
Start byte offset. Use `0` for the whole file, or resume from a prior offset.
End byte offset. `0` transfers to the end of the file.
Receives streamed audio bytes. Build one with `VoiceDataCreatorFactory.newOriginalData()` to write raw device bytes to a file. `PlaudDeviceAgent` handles this.
There is no format conversion and no decryption at this layer — the collector receives exactly what the device sends. `PlaudDeviceAgent.exportAudio(...)` handles this conversion and decryption.
#### Firmware Push
```kotlin Kotlin icon="android" theme={"system"}
ble.appFotaPush(filePath, fromVersion, toVersion, thirdVersion, otaPushListener)
ble.isFotaPushing()
ble.interruptFotaPush()
ble.getSyncOtaFileInfo(onRequest, onResponse)
ble.appGetThirdVersion(...)
```
`FirmwareUpdateManager` composes the full flow on top of this from querying the version to MD5 verification, CRC, and the post-restart reconnect.
Android's firmware version check queries `GET /api/sdk/latest-version` with an `Authorization` header, while iOS uses the partner endpoint with `X-Device-Signature`.
#### BleAgentListener
`IBleAgent`'s listener is registered on `TntAgent`:
```kotlin Kotlin icon="android" theme={"system"}
TntAgent.getInstant().addBleAgentListeners(listener)
TntAgent.getInstant().removeBleAgentListeners(listener)
```
| Interface | Members | Required |
| -------------------------- | ------- | -------- |
| `BleAgentListener` | 29 | **27** |
| `PlaudDeviceAgentListener` | 32 | **0** |
Conforming to `BleAgentListener` means implementing 27 members, and it hands you undocumented protocol response types instead of primitives. Unless you need an event the facade doesn't re-emit, keep your listener on `PlaudDeviceAgentListener`.
| `BleAgentListener` | `PlaudDeviceAgentListener` |
| --------------------------------------------------------- | ----------------------------------------------------------------------- |
| `scanBleDeviceReceiver(BleDevice)` — one device at a time | `bleScanResult(List)` — accumulated list |
| `deviceStatusRsp(String, GetStateRsp)` | `blePenState(state, privacy, keyState, uDisk)` |
| `deviceOpStorageRsp(String, StorageRsp)` | `bleStorage(total, free, duration)` |
| `deviceOpRecordStart(String, RecordStartRsp)` | `bleRecordStart(sessionId, start, status, scene, startTime, reason)` |
| `deviceFotaResult(String, AppFotaPushRsp)` | `bleFotaResult(sessionId, status, message)` |
| `batteryLevelUpdate` **+** `chargingStatusChange` | `bleChargingState(isCharging, level)` — merged |
| `bleConnectStage(String, String, String)` | `bleConnectStage(sn, stage, detail)` — identical, straight pass-through |
Events available only on `BleAgentListener`:
| Group | Callbacks |
| ----------------- | --------------------------------------------------------------------------------------------- |
| Connection detail | `btStatusChange`, `bleConnectFail`, `scanFail`, `handshakeWaitSure`, `sendMoreFailDisconnect` |
| Link quality | `rssiChange`, `mtuChange` |
| Sensors | `stickAngles`, `motorStatus`, `deviceNewFeature`, `deviceStatusDetailed` |
| Device logs | `deviceLogSyncData`, `deviceLogSyncStop`, `deviceLogSyncEnd` |
| WiFi / OTA detail | `deviceWifiSyncStartRsp`, `deviceSwitchWifiMode`, `deviceFotaThirdVersion` |
| Settings | `commonSettingNotify` *(default)* |
#### Connect stages — `bleConnectStage`
```kotlin Kotlin icon="android" theme={"system"}
fun bleConnectStage(sn: String?, stage: String, detail: String?)
```
`bleConnectStage` is the diagnostics stream for the connect/handshake sequence — the only way to see *which* layer refused a rejected connection. `sn` may be `null` before the serial number is known.
| Argument | Values |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `stage` | `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` |
| `detail` | Stage detail or failure reason — `ok`, `old_protocol_ok`, `status_` (`status_1` = token mismatch), `sn_signature_empty`, `sn_signature_invalid`, `user_rsa_public_key_empty`, `bind_token_empty`, `send_fail` |
| `detail` | What it means |
| --------------------------- | --------------------------------------------------------------------------------------- |
| `sn_signature_empty` | Your app skipped or lost the `sn-sign` step |
| `sn_signature_invalid` | The cached signature is stale — re-sign the SN |
| `user_rsa_public_key_empty` | `gen-key` has not completed; poll `NiceBuildSdk.isPartnerDataReady()` before connecting |
| `bind_token_empty` | No handshake token was supplied to the connect |
| `status_1` | Token mismatch — the firmware is locked to another account |
This matters more on Android than on iOS, because [your app drives `sn-sign` itself](#device-security). A rejection at `pre_handshake` / `send_rsa_public` / `first_handshake`, or a `detail` of `status_1`, is the signature of a device still locked to a previous account — the cue for [Device Recovery](#device-recovery-at-the-ble-layer).
Before SDK 1.0.13 all of these collapsed into an undifferentiated `bleConnectState(2)`. The callback itself is not new — it has been a default-bodied member of `BleAgentListener` since the first public release — but 1.0.13 is the release where the facade's bridge started forwarding it instead of swallowing it, so you no longer have to implement `BleAgentListener` to receive it.
***
### PartnerApiManager
`PartnerApiManager` wraps the partner authentication endpoints. Like `IBleAgent`, reach it via the lower facade:
```kotlin Kotlin icon="android" theme={"system"}
val api = NiceBuildSdk.getPartnerApiManager()
```
```kotlin Kotlin icon="android" theme={"system"}
class PartnerApiManager { // sdk.network.manager
fun setUserAccessToken(token: String)
fun getUserAccessToken(): String
fun hasUserAccessToken(): Boolean
fun updateBaseUrl(url: String)
fun clearToken()
// → POST …/open/partner/sdk/sn-sign
suspend fun signDeviceSn(deviceType: String, sn: String): SnSignResponse?
// → POST …/open/partner/sdk/gen-key
suspend fun generateRsaKeyPair(): GenKeyResponse?
// → POST …/open/partner/sdk/metadata
suspend fun postDeviceMetadata(signature: String, request: MetadataRequest): Boolean
}
```
#### Device Security
Partner endpoints for device authentication:
| Endpoint | Method | Auth header | Driven by |
| --------------- | ------ | ------------------------------------ | -------------------------------------- |
| `…/sdk/gen-key` | `POST` | `Authorization: Bearer ` | `PartnerApiManager.generateRsaKeyPair` |
| `…/sdk/sn-sign` | `POST` | `Authorization: Bearer ` | `PartnerApiManager.signDeviceSn` |
`initSDK(...)` fetches the key pair asynchronously. Poll `isPartnerDataReady()` before connecting.
```kotlin Kotlin icon="android" theme={"system"}
val deadline = System.currentTimeMillis() + 10_000L
while (!NiceBuildSdk.isPartnerDataReady() && System.currentTimeMillis() < deadline) {
delay(200)
}
```
Or, without a coroutine, use the callback form:
```kotlin Kotlin icon="android" theme={"system"}
NiceBuildSdk.ensurePartnerDataReady { ready -> /* … */ }
```
`signAndStoreDeviceSn(...)` signs the SN and writes the result where the BLE layer reads it during the pre-handshake.
```kotlin Kotlin icon="android" theme={"system"}
val signed = NiceBuildSdk.signAndStoreDeviceSn(deviceType, sn) // suspend
// or: NiceBuildSdk.signDeviceSnAsync(deviceType, sn) { ok -> /* … */ }
```
Device family, derived from the first three characters of the serial number.
| SN prefix | `deviceType` |
| --------- | ------------ |
| `881` | `notepro` |
| `882` | `notepins` |
Device serial number, from `BleDevice.getSerialNumber()`.
```kotlin Kotlin icon="android" theme={"system"}
PlaudDeviceAgent.connectBleDevice(bleDevice)
```
The SN signature is stored **in memory only** — there is no Keystore-backed cache and no persistence across process death. Every fresh launch needs a live `sn-sign` call before its first connect, so the device requires network reachability at that moment. Re-sign as a fallback whenever the stored signature is missing.
The RSA key pair itself *is* persisted, wrapped by an AES master key in the Android Keystore, and is what feeds audio E2EE decryption:
```kotlin Kotlin icon="android" theme={"system"}
val privateKeyPem: String = NiceBuildSdk.getSecurePrivateKey()
NiceBuildSdk.clearPartnerData() // drop the stored partner data
```
`NiceBuildSdk.bindDevice(...)` / `unbindDevice(...)` are **not** the cloud bind documented in the [Android SDK reference](/plaud-embedded/android-sdk). They post to a different service (`/api/devices/bind`) with a different credential. For the documented flow, call `developer/api/open/partner/sdk/bind` directly with the user token.
# Low-Level iOS SDK Methods
Source: https://docs.plaud.ai/plaud-embedded/advanced-ios-sdk
Low-level Embedded SDK methods for advanced use cases
## High-level Facades
The Plaud Embedded iOS SDK has a set of high-level interfaces for basic usage. These include:
1. **PlaudDeviceAgent**
2. **PlaudWiFiAgent**
These high-level methods and callbacks in these facades should cover most use cases including:
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
See our [iOS SDK reference](/plaud-embedded/ios-sdk) for examples and documentation on these high-level facades.
```
Your app
│
├── PlaudDeviceAgent ──────┐ (High-level)
├── PlaudWiFiAgent ───┐ │
│ │ │
│ ▼ ▼
│ WiFiAgent BleAgent
│ (low-level) (low-level)
│ │ │
└───────────────────────┴───┴──► device
```
Additionally, using PlaudDeviceAgent & PlaudWiFiAgent abstracts many details that low-level interfaces will not handle.
| Managed Behavior | Purpose |
| --------------------------- | --------------------------------------------------------------------------------------------------------- |
| Analytics event per call | Logged on every gated call |
| Permission gating | A call that fails the gate silently no-ops instead of reaching the device |
| Parameter validation | Guards on `sessionId`, `outputDir`, `startSessionId`, etc. |
| Partner pre-handshake | `sn-sign` / `gen-key` are run and injected before connect (required on protocol V20+ devices) |
| E2EE decryption | `syncFile` transparently decrypts on protocol V20+ devices |
| Audio format conversion | `exportAudio` decodes to `opus` / `mp3` / `wav` / `pcm` |
| Optional delegate callbacks | `PlaudDeviceAgentProtocol` has **1** required member and optional callbacks for you to override as needed |
You can mix high-level and low-level facades. For example if there's a behavior that `PlaudDeviceAgent` does not include, you can use `BleAgent` to cover that functionality.
## Low-level Facades
The iOS SDK has a few lower-level interfaces that our high-level interfaces (PlaudDeviceAgent and PlaudWiFiAgent) wrap over:
1. **BleAgent**: Bluetooth Low Energy (BLE) transport
2. **WiFiAgent**: WiFi agent with low-level methods for WiFi fast transfers
3. **PlaudPartnerApiManager**: Manages the backend endpoints for device signing and RSA keypair generation
**We do not recommend using these low-level facades as main drivers. Use them as coverage for functionality not exposed by PlaudDeviceAgent and PlaudWiFiAgent**.
The `PlaudWorkflowManager` has been deprecated and will be removed in future SDK versions.
***
### BleAgent Reference
Import `BleAgent` through `PlaudDeviceBasicSDK`. You can then access BleAgent through the shared singleton or through the facade's escape hatch:
```swift Swift icon="swift" theme={"system"}
import PlaudDeviceBasicSDK
// either work
let ble = BleAgent.shared
let ble = PlaudDeviceAgent.shared.bleAgent
```
#### BleAgent State
Read-only state is available directly on the agent, which is useful when you need a synchronous answer rather than waiting for a delegate callback:
| Property | Type | Description |
| --------------------- | ------------------- | ------------------------------------------------------ |
| `isPoweredOn` | `Bool` | The phone's Bluetooth radio is on |
| `isConnected` | `Bool` | A device is currently connected |
| `isBinded` | `Bool` | The connected device is bound |
| `isRecording` | `Bool` | The device is recording |
| `isDownloading` | `Bool` | A file sync is in progress |
| `isWiFiOpen` | `Bool` | The device's hotspot is open |
| `needDecode` | `Bool` | Incoming file data still needs decoding |
| `scene` / `sessionId` | `Int` | Current recording scene and session |
| `bleDevice` | `BleDevice?` | The connected device model |
| `delegate` | `BleAgentProtocol?` | Event sink — see [BleAgentProtocol](#bleagentprotocol) |
#### Device Connection
| `PlaudDeviceAgent` | `BleAgent` |
| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `startScan()` | `setFilter(name: nil)` → `startScan()` |
| `stopScan()` | `stopScan()` |
| `isConnected()` | `isDeviceConnect()` |
| `connectBleDevice(bleDevice:deviceToken:)` | partner pre-handshake → `connectBleDevice(bleDevice:_:_:_:)` |
| `recoveryConnectBleDevice(bleDevice:historicalUserId:)` | pin the historical id as the handshake token → `connectBleDevice(bleDevice:_:_:isForceClear: true)` |
| `disconnect()` | `disconnect()` |
| `depair(clear:)` | `depair(clear:)` — pass-through, no added logic |
```swift Swift icon="swift" theme={"system"}
BleAgent.shared.delegate = self
BleAgent.shared.initBluetooth()
BleAgent.shared.setFilter(name: nil)
BleAgent.shared.startScan()
// From your bleScanResult callback:
BleAgent.shared.connectBleDevice(
bleDevice: device,
userId, // devToken
nil, // userName
false // isForceClear
)
```
The device to connect to, as delivered by `bleScanResult(bleDevices:)`.
Per-device token. The facade passes the user identifier here and rejects an empty string by emitting `bleConnectState(state: 2)`; the low-level agent performs no such validation.
Owner name written to the device. The facade passes `"Plaud"`.
`connectBleDevice(bleDevice:deviceToken:)` passes `false`; `recoveryConnectBleDevice(bleDevice:historicalUserId:)` passes `true`. See [Device Recovery at the BLE Layer](#device-recovery-at-the-ble-layer).
Other lifecycle and auth methods on `BleAgent`:
```swift Swift icon="swift" theme={"system"}
func initBluetooth()
func disInitBluetooth()
func setUserIdentifier(_ appKey: String, _ bindToken: String, _ hkServer: Bool = false)
func checkAppKey(_ appKey: String)
func setBinding(_ token: String)
func setFilter(name: String?) // single-name filter, nil to disable
func setFilter(_ names: [String]) // multi-name filter
func startLoopScan() // continuous scan
func isAuthOk() -> Bool
func isSNTempChecked() -> Bool
func reCheckSNIfNeed()
func depair(clear: Bool = false)
func openLog(_ opened: Bool, logBlock: ((String) -> Void)? = nil,
wlogBlock: ((String) -> Void)? = nil)
```
`BleAgent.connectBleDevice(...)` does **not** run the partner pre-handshake, so connecting a protocol V20+ device (NotePro and newer) through the low-level agent fails unless you inject the handshake material yourself. See [Device Security](#device-security).
#### Recording & Device State
```swift Swift icon="swift" theme={"system"}
// Recording
func startRecord(_ scene: Int = 0)
func stopRecord()
func pauseRecord(_ sessionId: Int)
func resumeRecord(_ sessionId: Int)
// State reads — results arrive on the delegate
func getState()
func getStorage()
func getChargingState()
func readPower()
func getDeviceStatus()
func setHeartBeat(status: Int)
func setDeviceActive(status: Int)
func setPrivacy(onOff: Int)
func restoreFactory()
```
`BleAgent` also exposes the full device-settings surface that the facade only partially wraps. Each setting is a `read…` / `set…` pair, with the result delivered on the corresponding `BleAgentProtocol` callback.
```swift Swift icon="swift" theme={"system"}
// Backlight
func readBacklightDuration(); func setBacklight(duration: BacklightDuration)
func readBacklightBright(); func setBacklight(bright: BacklightBright)
// Language, scene & mode
func readLanguage(); func setLanguage(type: LanguageType)
func readRecScene(); func setRecScene(type: RecScene)
func readRecMode(); func setRecMode(type: RecMode)
func openVAD(open: Bool)
func readVadSensitivity(); func setVadSensitivity(sensitivity: VadSensitivity)
// Gains
func readVpuGain(); func setVpuGain(gain: VpuGain)
func readMicGain(); func setMicGain(value: Int)
func readBatteryMode(); func setBatteryMode(value: Int)
// Toggles
func readSwitchHandler(); func setSwitchHandler(id: Int)
func readAutoPowerOff(); func setAutoPowerOff(value: Int)
func readRawWaveEnabled(); func setRawWaveEnabled(value: Int)
func readRecordingAfterDisConnetEnabled(); func setRecordingAfterDisConnetEnabled(value: Int)
func readSyncWhenIdleEnabled(); func setSyncWhenIdleEnabled(value: Int)
func readFindMyState(); func setFindMyState(value: Int)
func readVPUCLK(); func setVPUCLK(value: Int)
func readStopRecordingAfterCharging(); func setStopRecordingAfterCharging(value: Int)
func readAutoClear(); func saveAutoClear(_ open: Bool)
// Naming, LED, alarm recording
func readBleName(); func setBleName(name: String)
func getLedState(); func setLedState(onOff: Int)
func getAlarmRec(); func setAlarmRec(start: Int, duration: Int, repeatMode: Int)
// Generic channel for settings without a dedicated method
func getCommonParams(dataType: Int)
func setCommonParams(dataType: Int, value: String)
// Device logs, find-my, SoundPlus, SD flash, BLE rate test
func getDeviceLogList(logType: Int)
func startSyncDeviceLogFile(logType: Int); func stopSyncDeviceLogFile()
func deleteDeviceLogFile(logType: Int)
func resetFindmy()
func setSoundPlusToken(licenseKey: String)
func getSDFLASHCID()
func startBleRateTest(_ packSize: Int = 80); func stopBleRateTest()
```
Most setters ship in two forms: an `@objc` variant taking a raw `Int` (e.g. `setRecScene(value: Int)`) and a Swift-only variant taking a typed enum (`setRecScene(type: RecScene)`). Prefer the typed variant.
#### File Sync
```swift Swift icon="swift" theme={"system"}
func getFileList(uid: Int, sessionId: Int, onlyOne: Bool = false)
func syncFile(sessionId: Int, start: Int, end: Int, decode: Bool)
func stopSyncFile()
func deleteFile(sessionId: Int)
func clearAllFile()
func getMarking(_ sessionId: Int)
func getRecordMarkingTags(uid: Int, startTimestamp: Int, endTimestamp: Int)
```
Request identifier echoed back on the response. The facade passes a timestamp.
On `getFileList`, the session ID to start listing from. On `syncFile` / `deleteFile`, the recording to act on.
Return a single file rather than the list from `sessionId` onward. This is what `PlaudDeviceAgent.getFile(sessionId:)` sets.
Start byte offset. Use `0` for the whole file, or a `BleFile.offset` to resume.
End byte offset. `0` transfers to the end of the file.
Decode the stream in transit. Pass `false` for E2EE (protocol V20+) devices and decrypt the result yourself; `true` otherwise.
File bytes arrive on `bleData(sessionId:start:data:)` and finish with `bleDataComplete()`. There is no format conversion at this layer — that is what `PlaudDeviceAgent.exportAudio(...)` adds on top.
On a protocol V20+ device, `syncFile(decode: true)` returns bytes that are still E2EE-encrypted. Sync with `decode: false` and pass the file through `AudioFileDecryptor.decryptAudioFile(inputPath:privateKeyPem:)` using the private key from [`gen-key`](#device-security), or use `PlaudDeviceAgent.syncFile(...)`, which does this for you.
#### Firmware Push
```swift Swift icon="swift" theme={"system"}
func pushFotaInfo(_ uid: Int, _ fromVersion: String, _ toVersion: String,
_ thirdVersion: Int = 0, _ fileSize: Int, _ crc: Int)
func pushFotaPack(_ offset: Int, packData: Data, postDelayUs: NSNumber?)
func pushFotaComplete(_ uid: Int, _ status: Int)
func getUpdateInfo(_ callback: @escaping (Int, UpdateInfo?) -> Void)
```
#### Encryption
```swift Swift icon="swift" theme={"system"}
var isSecureChannelEstablished: Bool { get }
var isEncryptionSupported: Bool { get }
func getEncryptionKey() -> String?
func getEncryptionNonce() -> String?
func getEncryptionAD() -> String?
func getEncryptionParameters() -> [String: String]?
// Transport encryption (ChaCha20-Poly1305 / AES-256)
func decryptFileData(_ encryptedData: Data, key: String?, nonce: String?, ad
: String?) throws -> Data
func decryptFile(inputPath: String, outputPath: String, key: String?, nonce:
String?, ad: String?) -> Bool
// End-to-end (RSA-wrapped) audio
func decryptE2EEAudioFile(inputPath: String, outputPath: String?, privateKey
Pem: String) throws -> String
func isE2EEEncryptedFile(path: String) -> Bool
func getE2EEFileHeader(path: String) -> PlaudEncryptHeader?
```
#### BleAgentProtocol
`BleAgentProtocol` is `BleAgent`'s delegate. `PlaudDeviceAgent` conforms to it internally and re-emits a filtered, mostly-optional subset to your `PlaudDeviceAgentProtocol`.
| Protocol | Required members | Optional members |
| -------------------------------------------------- | --------------------- | --------------------- |
| `BleAgentProtocol` (`PlaudBleSDK`) | **96** | 1 (`bleConnectStage`) |
| `PlaudDeviceAgentProtocol` (`PlaudDeviceBasicSDK`) | **1** (`blePenState`) | 49 |
| `PlaudWiFiAgentProtocol` | 0 | all |
Conforming to `BleAgentProtocol` means implementing 96 members. Unless you need an event the facade does not re-emit, keep your delegate on `PlaudDeviceAgentProtocol`.
Where the two layers differ, the facade narrows the payload:
| Callback | `BleAgentProtocol` | `PlaudDeviceAgentProtocol` |
| ----------------- | ------------------------------------------- | --------------------------------- |
| `blePenState` | + `versionType: String, versionCode: Int` | 7 params, no version fields |
| `bleRecordStart` | `(sessionId:start:status:scene:startTime:)` | + `reason: Int` |
| `bleConnectStage` | `(sn:stage:detail:)` | identical — straight pass-through |
##### Connect stages — `bleConnectStage`
```swift Swift icon="swift" theme={"system"}
@objc optional func bleConnectStage(sn: String?, stage: String, detail: String?)
```
`bleConnectStage` is the diagnostics stream for the connect/handshake sequence — the only way to see *which* layer refused a rejected connection. `sn` may be `nil` before the serial number is known.
| Argument | Values |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `stage` | `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` |
| `detail` | Stage detail or failure reason — `ok`, `status_` (`status_1` = token mismatch), `sn_signature_empty`, `sn_signature_invalid`, `user_rsa_public_key_empty`, `bind_token_empty` |
The `stage` strings are the raw values of the `BleAgent.ConnectStage` enum, and the callback delivers them as `String`, not as the enum case names.
| `detail` | What it means |
| --------------------------- | ---------------------------------------------------------- |
| `sn_signature_empty` | `sn-sign` never ran, or the cached signature was lost |
| `sn_signature_invalid` | The cached signature is stale — re-sign the SN |
| `user_rsa_public_key_empty` | `gen-key` has not completed; the key pair is missing |
| `bind_token_empty` | No handshake token was supplied to the connect |
| `status_1` | Token mismatch — the firmware is locked to another account |
A rejection at `pre_handshake` / `send_rsa_public` / `first_handshake`, or a `detail` of `status_1`, is the signature of a device still locked to a previous account — that is the cue for [Device Recovery](#device-recovery-at-the-ble-layer).
Before SDK 1.0.13 all of these collapsed into an undifferentiated `bleConnectState(state: 2)`. The callback itself is not new — it has been `BleAgentProtocol`'s single optional member all along — but 1.0.13 is the release where `PlaudDeviceAgent` started re-emitting it, so you no longer have to drop to the BLE layer to receive it.
***
### WiFiAgent Reference
#### WiFi Fast Transfer
Each `PlaudWiFiAgent` method maps to a single `WiFiAgent` command:
| `PlaudWiFiAgent` | `WiFiAgent` | Command |
| --------------------- | ------------------------------ | ------- |
| `connectWifi(_:_:_:)` | `connectWifi(_:_:_:)` | — |
| `listenPort(_:_:)` | `listenPort(_:_:)` | — |
| `disconnect()` | `disconnect()` | — |
| `isConnectedTo(_:)` | `getCurrentWiFiName() == ssid` | — |
| `getFileList(_:_:_:)` | `appGetFileList(_:_:_:)` | 11 |
| `syncFile(_:_:_:_:)` | `appSyncFile(_:_:_:_:)` | 12 |
| `deleteFile(_:_:)` | `appDeleteFile(_:_:)` | 14 |
| `stopSyncFile(_:_:)` | `appStopSyncFile(_:_:)` | 15 |
The flow is the same as the high-level PlaudWiFiAgent: open the device hotspot, then hand the device to the WiFi layer and wait for `wifiHandshake(0)` before issuing any file command.
```swift Swift icon="swift" theme={"system"}
import PlaudDeviceBasicSDK
WiFiAgent.shared.delegate = self
WiFiAgent.shared.bleDevice = BleAgent.shared.bleDevice // required before connecting
// From bleWiFiOpen(_:_:_:_:) on PlaudDeviceAgentProtocol:
WiFiAgent.shared.connectWifi(wifiName, wifiPass, 60)
extension SyncManager: WiFiAgentProtocol {
func wifiHandshake(_ status: Int) {
guard status == 0 else { return }
WiFiAgent.shared.appGetFileList(Int(Date().timeIntervalSince1970), 0, false)
}
func wifiFileList(_ files: [BleFile]) {
guard let file = files.first else { return }
WiFiAgent.shared.appSyncFile(file.sessionId, 0, 0, file.scenes)
}
func wifiSyncFileData(_ sessionId: Int, _ offset: Int, _ count: Int, _ binData: Data) {
// append binData at offset
}
func wifiDataComplete() { /* all bytes received */ }
}
```
The device hotspot SSID, delivered as `wifiName` on `bleWiFiOpen(_:_:_:_:)`.
The hotspot passphrase, delivered as `wifiPass` on `bleWiFiOpen(_:_:_:_:)`.
Association timeout in seconds. `listenPort(_:_:)` defaults to `30`.
On `appSyncFile` / `appStopSyncFile` / `appDeleteFile`, the file's own `BleFile.scenes` value. A mismatch is rejected by the device with a non-zero `wifiSyncFile` status.
Additional transport controls not surfaced on the facade:
```swift Swift icon="swift" theme={"system"}
func cancelConnectWifi()
func clearAllWiFiConfigurations() // removes SSIDs this app configured
func getCurrentWiFiNameWithRetry(maxRetries: Int = 3, delay: TimeInterval = 1.0) -> String?
func isWebSocketConnected() -> Bool
func appExtendWifiExitTime() // keep the hotspot alive longer
func appWiFiRate(_ onOff: Bool, _ packSize: Int)
func appGetLogs(_ begin: Bool)
func startPushOTA(_ uid: Int, _ fileSize: Int, crc: Int, _ toVersion: Int)
```
#### WiFiAgentProtocol
`WiFiAgentProtocol` is the low-level delegate. Unlike `PlaudWiFiAgentProtocol`, which is entirely optional, most of `WiFiAgentProtocol` are required.
| Callback | Description |
| ----------------------------------------------------------- | ------------------------------------------------------------------------ |
| `wifiHandshake(_ status: Int)` | `0` = handshake complete, ready to issue commands |
| `wifiFileList(_ files: [BleFile])` | Result of `appGetFileList(...)` |
| `wifiFileListFail(_ status: Int)` | File listing failed |
| `wifiSyncFile(_ sessionId: Int, _ status: Int)` | `0` = transfer accepted; non-zero = rejected (commonly a scene mismatch) |
| `wifiSyncFileData(_:_:_:_:)` | A chunk of file data at an offset |
| `wifiDataComplete()` | All bytes received |
| `wifiSyncFileStop(_ status: Int)` | Transfer stopped or aborted |
| `wifiFileDelete(_ sessionId: Int, _ status: Int)` | Result of `appDeleteFile(...)` |
| `wifiCommonErr(_ cmd: Int, _ status: Int)` | A command failed, identified by command number |
| `wifiPower(_ power: Int, _ voltage: Int)` | Device battery over the WiFi session |
| `wifiRate(_:_:_:)` / `wifiRateFail(_:)` | Throughput test results |
| `wifiLogs(_ logData: Data?)` / `wifiLogsFail(_:)` | Device log retrieval |
| `wifiClientFail()` / `wifiClose(_ status: Int)` | Session dropped or closed |
| `wifiTips(_ tips: Int)` | Device-side advisory code |
| `penRequestOTAData(start:end:payloadSize:uid:sendRatePPS:)` | Device requesting an OTA chunk over WiFi |
| `wifiOTAStatus(_ status: Int, _ uid: Int)` | OTA-over-WiFi status |
`wifiCommonErr(cmd: 16, status: 0)` at the end of a transfer is expected and indicates a successful sync.
***
### PlaudPartnerApiManager
`PlaudPartnerApiManager` wraps the partner authentication endpoints. Like `BleAgent`, reach it via the facade or the singleton:
```swift Swift icon="swift" theme={"system"}
let api = PlaudDeviceAgent.shared.getPartnerApiManager()
// or: PlaudPartnerApiManager.shared
```
```swift Swift icon="swift" theme={"system"}
final public class PlaudPartnerApiManager {
public static let shared: PlaudPartnerApiManager
public func setUserAccessToken(_ token: String?)
public func getUserAccessToken() -> String?
// → POST …/open/partner/sdk/sn-sign
public func signDeviceSn(deviceType: String, sn: String,
completion: @escaping (Result) -> Void)
// → POST …/open/partner/sdk/gen-key
public func generateRsaKeyPair(
completion: @escaping (Result) -> Void)
}
```
There is no `sn-verify` method on iOS. Signature **verification** happens on the device during the BLE pre-handshake, not through a client API call.
#### Device Security
Partner endpoints for device authentication:
| Endpoint | Method | Auth header | Driven by |
| --------------- | ------ | ------------------------------------ | ------------------------------------------- |
| `…/sdk/gen-key` | `POST` | `Authorization: Bearer ` | `PlaudPartnerApiManager.generateRsaKeyPair` |
| `…/sdk/sn-sign` | `POST` | `Authorization: Bearer ` | `PlaudPartnerApiManager.signDeviceSn` |
The high-level `PlaudDeviceAgent` drives this entire flow. You only need these APIs if you are connecting through `BleAgent` directly or minting keys on your own backend.
`initSDK(userAccessToken:customDomain:)` and every `setUserAccessToken(_:)` call fetch a fresh RSA key pair, store it, and mirror it into the BLE layer. A new key pair **invalidates every cached `sn-sign` signature**, since signatures are bound to the key pair that produced them.
A response missing `private_key` is a hard failure — the public key alone is not enough to complete the encrypted pre-handshake.
`connectBleDevice(...)` runs the partner handshake before it reaches `BleAgent`:
Methods like `PlaudDeviceAgent.syncFile(...)` will use the symmetric key from the encrypted file header before decoding audio.
# Android SDK
Source: https://docs.plaud.ai/plaud-embedded/android-sdk
Integrate your native Android app via the Embedded SDK for Android.
The Android SDK has many high-level and low-level methods to interact with Plaud devices. We recommend using the high-level methods 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 Android SDK usage](/plaud-embedded/advanced-android-sdk).
## Prerequisites
| Technology | Version |
| -------------------------- | ---------------- |
| Android | 5.0+ (minSdk 21) |
| compileSdk | 34 |
| Java Development Kit (JDK) | 17 |
The SDK ships native libraries for `arm64-v8a` / `armeabi-v7a`. For testing, you must use a physical device, not an emulator.
**Try the Plaud Embedded Skill** to have your coding agent help you with your Android 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.
## Installation
```bash theme={"system"}
git clone https://github.com/Plaud-AI/plaud-sdk-public.git
```
The Android SDK ships as a pre-built `.aar`. Copy `sdk/android/plaud-sdk.aar` into your app module's `libs/` directory and add it as a dependency:
```groovy build.gradle theme={"system"}
dependencies {
implementation files('libs/plaud-sdk.aar')
}
```
***
## Getting Started
Import `PlaudDeviceAgent` from `sdk`. `PlaudDeviceAgent` is a singleton `object`, so call it directly — there is no instance to construct:
```kotlin Kotlin icon="android" theme={"system"}
import sdk.PlaudDeviceAgent
// Initialize once with your app Context, user token, and regional domain
PlaudDeviceAgent.initSDK(
context = applicationContext,
userAccessToken = "user-token",
customDomain = "platform-us.plaud.ai" // domain only, no https://
)
// Assign the global listener to receive device events
PlaudDeviceAgent.listener = object : PlaudDeviceAgentListener {
override fun blePenState(state: Int, privacy: Int, keyState: Int, uDisk: Int) {
// handshake complete — device is ready
}
// override only the callbacks you need
}
```
Use these 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 your app `Context`, a **User Token**, and your **regional** domain.
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 your regional domain, see the [Authentication API reference](/plaud-embedded/auth-api-overview).
```kotlin Kotlin icon="android" theme={"system"}
import sdk.PlaudDeviceAgent
PlaudDeviceAgent.initSDK(
context = applicationContext,
userAccessToken = "user-token",
customDomain = "platform-us.plaud.ai" // domain only, no https://
)
```
Your application `Context` (e.g. `applicationContext`).
User Access Token (JWT), used for device authentication.
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).
### Permission Manager
BLE scanning requires runtime permissions on Android. The SDK's `sdk.permission.PermissionManager` requests the full set it needs and reports the result.
```kotlin Kotlin icon="android" theme={"system"}
import sdk.permission.PermissionManager
class ScanActivity : AppCompatActivity() {
private val perms by lazy { PermissionManager(this) }
private fun startScanning() {
if (perms.hasAllPermissions()) {
PlaudDeviceAgent.startScan()
} else {
perms.requestPermissions(this) { granted -> onPermissionResult(granted) }
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
perms.onRequestPermissionsResult(
requestCode,
arrayOf(*permissions),
grantResults
) { granted -> onPermissionResult(granted) }
}
private fun onPermissionResult(granted: Boolean) {
if (granted) PlaudDeviceAgent.startScan() else { /* ... */ }
}
}
```
Android activity
Receives `true` only when every requested permission was granted.
`PermissionManager` requests **`BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`, `ACCESS_FINE_LOCATION` and `ACCESS_COARSE_LOCATION`** on API 31+, and `BLUETOOTH`, `BLUETOOTH_ADMIN`, `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION` on API ≤ 30. Note that it asks for location on Android 12+ as well.
***
### 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 a device requires an API call to Plaud's cloud services, so you can track device statuses remotely. And a local bind triggered by the Embedded SDK to verify and generate keys on Plaud device.
For the full cloud-side reference, see the [Device Binding APIs](/plaud-embedded/device-binding-api-overview).
`.startScan()` delivers results on the `bleScanResult` callback defined in the [**PlaudDeviceAgentListener**](#plauddeviceagentlistener).
```kotlin Kotlin icon="android" theme={"system"}
PlaudDeviceAgent.listener = object : PlaudDeviceAgentListener {
override fun bleScanResult(devices: List) {
val match = devices.firstOrNull { it.serialNumber == lastSN } ?: return
connect(match) // see the next step
}
override fun bleConnectState(state: Int) { /* 1=connected, 0=disconnected, 2=failed */ }
override fun bleBind(sn: String?, status: Int, protVersion: Int, timezone: Int) { /* status == 0 → bound */ }
override fun blePenState(state: Int, privacy: Int, keyState: Int, uDisk: Int) { /* handshake complete */ }
// ... recording / file-list / battery / storage callbacks
}
PlaudDeviceAgent.startScan()
```
Registers the device/owner association in the Plaud registry. Re-binding a device to the same owner is idempotent, so multiple calls will not have 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.
```kotlin Kotlin icon="android" theme={"system"}
// POST https://platform-us.plaud.ai/developer/api/open/partner/sdk/bind
val body = JSONObject()
.put("type", snType) // e.g. "notepro" / "notepins"
.put("sn", sn)
val request = Request.Builder()
.url("https://platform-us.plaud.ai/developer/api/open/partner/sdk/bind")
.header("Authorization", "Bearer $userAccessToken")
.post(body.toString().toRequestBody("application/json".toMediaType()))
.build()
```
Device type string derived from the SN prefix (`881` → `notepro`, `882` → `notepins`).
Device serial number.
A `403` response means the device is already bound to **another** account.
Connects and generates the key-pair on the device itself. The bind result is delivered on `bleBind(sn, status, protVersion, timezone)`.
**Two things must be in place before `connectBleDevice(...)`, or the secure handshake fails:**
1. The partner RSA key pair must have arrived. `initSDK(...)` fetches it over HTTP.
2. The device serial number must be signed and stored, with `NiceBuildSdk.signAndStoreDeviceSn(deviceType, sn)`. The BLE layer reads that signature during the pre-handshake.
```kotlin Kotlin icon="android" theme={"system"}
import sdk.NiceBuildSdk
private fun connect(bleDevice: BleDevice) = lifecycleScope.launch(Dispatchers.IO) {
val sn = bleDevice.serialNumber ?: return@launch
val deviceType = if (sn.startsWith("881")) "notepro" else "notepins"
// 1. Wait for the RSA key pair fetched by initSDK.
val deadline = System.currentTimeMillis() + 10_000L
while (!NiceBuildSdk.isPartnerDataReady() && System.currentTimeMillis() < deadline) {
delay(200)
}
// 2. Sign the SN — the handshake reads the stored signature.
if (!NiceBuildSdk.signAndStoreDeviceSn(deviceType, sn)) {
// network unreachable or token rejected — the handshake will fail
}
PlaudDeviceAgent.connectBleDevice(bleDevice)
}
```
A scanned device, as delivered by `bleScanResult`.
Optional second argument on the `connectBleDevice(bleDevice, deviceToken)` overload — a unique identifier for that device. Omit it to connect with an empty token, which is what most integrations do.
The serial number is read from the `BleDevice` — you do not pass it here.
#### PlaudDeviceAgentListener Callbacks
| Callback | Description |
| -------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `bleScanResult(devices: List)` | Scan results updated |
| `bleConnectState(state: Int)` | Connection state — `1` = connected, `0` = disconnected, `2` = failed |
| `bleBind(sn: String?, status: Int, protVersion: Int, timezone: Int)` | Device bound successfully |
| `blePenState(state: Int, privacy: Int, keyState: Int, uDisk: Int)` | Secure handshake complete |
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.**
### 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.
Removes the device/owner association in the Plaud registry (visible on the [Plaud developer portal](https://portal.plaud.ai/)). This does not require a BLE connection. Send an authenticated `POST` to the partner unbind endpoint on your regional domain. Unbinding an already-unbound device is an idempotent no-op, so this is safe to call best-effort. 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.
```kotlin Kotlin icon="android" theme={"system"}
// POST https:///developer/api/open/partner/sdk/unbind
val body = JSONObject()
.put("type", snType) // e.g. "notepro" / "notepins"
.put("sn", sn)
val request = Request.Builder()
.url("https://$customDomain/developer/api/open/partner/sdk/unbind")
.header("Authorization", "Bearer $userAccessToken")
.post(body.toString().toRequestBody("application/json".toMediaType()))
.build()
```
Device type string derived from the SN prefix (`881` → `notepro`, `882` → `notepins`).
Device serial number.
Clears the pairing/handshake on the device itself. Requires the device to be connected. On success the SDK disconnects and clears the session. The result is delivered on the `bleDepair(status: Int)` listener callback.
```kotlin Kotlin icon="android" theme={"system"}
PlaudDeviceAgent.listener = object : PlaudDeviceAgentListener {
override fun bleDepair(status: Int) {
if (status == 0) {
// device unpaired and disconnected
}
}
// ...
}
PlaudDeviceAgent.depair(clear = false)
```
Whether to also clear the device's own stored bond list. **Pass `false` for the normal unbind flow** — this is what the [Android Starter App](/plaud-embedded/android-starter-app) does. Omit the argument to call the no-arg `depair()` overload.
Reserve `clear = true` for recovering a device whose on-device pairing state is stale.
`bleDepair` is best-effort — a device that is out of range or unresponsive never answers. Pair the callback with a timeout (a few seconds) and call `PlaudDeviceAgent.disconnect()` either way, so the BLE link is always released.
#### 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 Android this usually surfaces as `bleConnectState(2)`.
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.
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.
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.
```kotlin Kotlin icon="android" expandable theme={"system"}
try {
val url = "https://platform-us.plaud.ai/developer/api/open/partner/sdk/binding" +
"?type=${getDeviceType(sn)}&sn=$sn"
val request = Request.Builder().url(url)
.header("Authorization", "Bearer $userAccessToken")
.get()
.build()
OkHttpClient().newCall(request).execute().use { resp ->
if (!resp.isSuccessful) null
else {
val json = JSONObject(resp.body?.string() ?: "")
CloudBindingInfo(
// JSON null -> null (three-state)
isBind = if (json.isNull("is_bind")) null else json.optBoolean("is_bind"),
bindHistory = json.optJSONArray("bind_history")?.let { arr ->
(0 until arr.length()).mapNotNull {
arr.optString(it).takeIf { id -> id.isNotBlank() }
}
} ?: emptyList()
)
}
}
} catch (e: Exception) {
null
}
```
Device type string derived from the SN prefix (`881` → `notepro`, `882` → `notepins`).
Device serial number.
**Response:**
`true` = bound to another account, `false` = unbound, `null` = signed but never bound. Use Device Recovery only when this is **not** `true`.
Previously bound client IDs, newest first.
`bind_history` records one entry **per bind event**.
For each previous client ID, `recoveryConnectBleDevice` uses that ID as the handshake token and connects with force-clear, wiping the device's stale data.
```kotlin Kotlin icon="android" expandable theme={"system"}
suppressAutoReconnect = true
recoveryInProgress = true
try {
for (historicalId in history) {
val signal = CompletableDeferred()
recoverySignal = signal
PlaudDeviceAgent.recoveryConnectBleDevice(bleDevice, historicalId)
val unlocked = withTimeoutOrNull(25_000L) { signal.await() } ?: false
if (!unlocked) {
// This ID did not match the firmware lock — try the next one.
PlaudDeviceAgent.disconnect()
delay(1_000)
continue
}
// Matched — wipe the stale bond and wait for the device's confirmation.
depairAndAwait()
recoveryInProgress = false
delay(1_500)
// depair changes the MAC, so rescan and match by SN before reconnecting.
val fresh = rescanForDevice(sn)
?: return@launch failRecovery(
"The device was unlocked but did not reappear — please rescan and connect it."
)
// Reconnect as the current user — this rebinds the device.
withContext(Dispatchers.Main) {
suppressAutoReconnect = false
connect(device, currentUserId)
}
return@launch
}
} finally {
recoveryInProgress = false
recoverySignal = null
}
}
```
The scanned device, from `bleScanResult`.
One `bind_history` entry to try as the handshake token.
***
### File Synchronization
The `exportAudio` method exports audio files from the Plaud device to your users' phone, reporting progress through the [**AudioExporter.ExportCallback**](#audioexporterexportcallback).
Plaud devices will record **up to 5 hours**. Recordings longer should be broken up.
The `.getFileList` accesses files on your users' Plaud device.
```kotlin Kotlin icon="android" theme={"system"}
// Get file list from device
PlaudDeviceAgent.getFileList()
PlaudDeviceAgent.exportAudio(
sessionId = sessionId,
outputDir = outputDir,
format = AudioExportFormat.WAV,
channels = 1,
callback = object : AudioExporter.ExportCallback {
override fun onProgress(progress: Int, message: String) { }
override fun onComplete(outputFile: File) {
// decoded file is ready at outputFile
}
}
)
```
Session ID
Output directory
`PCM` | `WAV` | `OPUS` | `MP3`. We recommend `MP3` — it plays everywhere and is accepted directly by the transcription upload API.
Number of audio channels (`1` = mono). Optional — an overload without this argument exists.
`fun onProgress(progress: Int, message: String)`
`fun onComplete(outputFile: File)`
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)
***
### WiFi Fast Transfer
An alternative to a BLE (Bluetooth Low Energy) file transfer, WiFi Fast Transfer is \~10x faster than BLE transfers.
A transfer spans two objects: `PlaudDeviceAgent` opens the hotspot, starts and ends the session, and decodes each recording; `IWifiTransferAgent` (reached with `getWifiAgent()`) lists and deletes files during the session.
`getWifiAgent()` returns `IWifiTransferAgent?` — it is `null` until the SDK has a WiFi agent to hand out. Always reach it with a safe call (`getWifiAgent()?.…`).
The device opens a WiFi hotspot on request over BLE. The credentials arrive on the `bleWiFiOpen` listener callback.
```kotlin Kotlin icon="android" theme={"system"}
PlaudDeviceAgent.setDeviceWiFi(open = true)
```
`true` opens the device hotspot, `false` closes it.
Start the session from the `bleWiFiOpen` callback. The SDK handles joining the hotspot and the secure handshake for you.
```kotlin Kotlin icon="android" theme={"system"}
PlaudDeviceAgent.listener = object : PlaudDeviceAgentListener {
override fun bleWiFiOpen(status: Int, ssid: String, password: String, url: String) {
PlaudDeviceAgent.startWifiTransfer(userId, wifiCallback)
}
// ...
}
```
Identifier for the transfer session.
Drives the rest of the flow. See [**WifiTransferCallback**](#wifitransfercallback) below.
Returns `false` if the session could not be opened.
Connection state advances `NONE` → `CONNECTING` → `CONNECTED` → `HANDSHAKING` → `READY`. **No file command works before `READY`.**
```kotlin Kotlin icon="android" theme={"system"}
val wifiCallback = object : IWifiTransferAgent.WifiTransferCallback {
override fun onConnectionStateChanged(state: WifiConnectionState) {
if (state == WifiConnectionState.READY) {
PlaudDeviceAgent.getWifiAgent()?.getFileList()
}
}
override fun onFileListReceived(files: List) {
// export each session (next step)
}
override fun onError(code: Int, message: String) { }
// ...
}
```
`exportAudioViaWiFi` takes the same arguments as [`exportAudio`](#file-synchronization) and reports on the same `AudioExporter.ExportCallback`, so your existing export handling works unchanged over WiFi.
```kotlin Kotlin icon="android" theme={"system"}
PlaudDeviceAgent.exportAudioViaWiFi(
sessionId = file.sessionId,
outputDir = outputDir,
format = AudioExportFormat.MP3,
channels = 1,
callback = object : AudioExporter.ExportCallback {
override fun onProgress(progress: Int, message: String) { }
override fun onComplete(outputFile: File) {
// decoded file is ready at outputFile
}
}
)
```
Use `exportAudioViaWiFi(...)` rather than the raw `downloadFile()` / `downloadAllFiles()` path on `IWifiTransferAgent`. Those write undecrypted `.opus` bytes straight to disk; `exportAudioViaWiFi` runs the same decode pipeline as BLE.
```kotlin Kotlin icon="android" theme={"system"}
PlaudDeviceAgent.endWiFiTransfer()
```
End the session with `endWiFiTransfer()`, not `getWifiAgent().stopWifiTransfer()`. Only `endWiFiTransfer()` also tells the device to close its hotspot over BLE — otherwise it stays open and drains the device battery.
Use `PlaudDeviceAgent.isWifiTransferActive()` to check whether a session is currently open.
#### Deleting files over WiFi
`IWifiTransferAgent.deleteFiles(...)` removes several recordings in one call — the only batch delete in the SDK. The result arrives on `onFileDeleteCompleted`.
```kotlin Kotlin icon="android" theme={"system"}
PlaudDeviceAgent.getWifiAgent()?.deleteFiles(listOf(sessionId1, sessionId2))
```
Session IDs of the recordings to delete from the device.
#### WifiTransferCallback
The WiFi Fast Transfer flow is driven by the [IWifiTransferAgent.WifiTransferCallback](/plaud-embedded/android-sdk#iwifitransferagent-wifitransfercallback). All members are required — there are no default implementations.
| Callback | Description |
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `onConnectionStateChanged(state: WifiConnectionState)` | WiFi transfer connection state changed |
| `onHandshakeCompleted(info: String)` | Secure handshake completed; transfer is ready |
| `onFileListReceived(files: List)` | Result of the file-list request |
| `onTransferProgress(sessionId: Long, progress: Int, speed: Double)` | Per-file transfer progress (`progress` 0–100, `speed` in KB/s) |
| `onFileTransferCompleted(sessionId: Long, path: String)` | A single file finished downloading to `path` |
| `onBatchDownloadStarted(total: Int)` | Batch download started |
| `onBatchDownloadProgress(current: Int, total: Int, filename: String)` | Progress across a batch download |
| `onBatchDownloadCompleted(success: Int, failed: Int, results: List)` | Batch download finished |
| `onFileDeleteCompleted(success: Boolean, deletedCount: Int, error: String?)` | File delete finished |
| `onWifiTransferStopped()` | WiFi transfer session stopped |
| `onDeviceBatteryUpdate(level: Int, charging: Boolean)` | Device battery / charging status update |
| `onError(code: Int, message: String)` | An error occurred during the transfer |
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.
***
### Firmware Update (OTA)
The Embedded SDK handles the entire OTA flow across three calls: version query → download → MD5 verify → CRC → BLE packet push → device restart → reconnect. Each phase reports through the [**FirmwareUpdateCallback**](#firmwareupdatecallback).
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!
#### Check for Firmware Updates
```kotlin Kotlin icon="android" theme={"system"}
// Check for update
PlaudDeviceAgent.checkFirmwareUpdate(object : SimpleFirmwareUpdateCallback() {
override fun onUpdateCheckResult(result: Result) {
val info = result.getOrNull() ?: return
if (!info.hasUpdate) return
// proceed to download (below)
}
})
```
Callback that reports the check result as a `FirmwareUpdateInfo`.
Whether a firmware update is available
The device's current firmware version
Whether the update is mandatory
The full version-check response (latest version, download URL, MD5, release notes)
#### Download Firmware Update
```kotlin Kotlin icon="android" theme={"system"}
// Download + MD5 verify
PlaudDeviceAgent.downloadFirmware(updateInfo, object : SimpleFirmwareUpdateCallback() {
override fun onDownloadProgress(progress: UpdateProgress) {
// progress.progress: 0 ~ 100
}
override fun onDownloadComplete(result: FirmwareDownloadResult) {
if (result.success) {
result.file?.let { /* install (below) */ }
}
}
})
```
The `FirmwareUpdateInfo` returned by `checkFirmwareUpdate`.
Reports download progress and completion.
Progress from 0 to 100
Human-readable status message
Additional detail for the current step
`TRANSFERRING` / `TRANSFER_COMPLETE_WAITING` / `DEVICE_RESTARTING` / `UPGRADE_COMPLETE` / `TRANSFER_FAILED` / `UPGRADE_FAILED`
Whether the download succeeded
The downloaded firmware file
Whether the MD5 checksum verified
Error description if the download failed
#### Install Firmware Update
```kotlin Kotlin icon="android" theme={"system"}
PlaudDeviceAgent.installFirmware(file, updateInfo, object : SimpleFirmwareUpdateCallback() {
override fun onInstallProgress(progress: UpdateProgress) { }
override fun onInstallComplete(result: FirmwareInstallResult) {
if (result.success) {
// device restarts and reconnects
}
}
})
```
The firmware file from `FirmwareDownloadResult.file`
The `FirmwareUpdateInfo` returned by `checkFirmwareUpdate`
Reports install progress and completion.
Whether the install succeeded
Error description if the install failed
***
## Interfaces and Callbacks
The Embedded SDK is callback-driven. Device events flow through a single global listener you assign to `PlaudDeviceAgent.listener`, while per-operation flows (audio export, WiFi transfer, firmware) take their own callback interface. These four cover most use cases.
Callbacks are delivered on the SDK's internal threads — **not** the main thread. Marshal to the main thread (e.g. `runOnUiThread { }` / a `Handler`) before touching UI or view state.
### PlaudDeviceAgentListener
The primary listener for `PlaudDeviceAgent`. Assign it once to `PlaudDeviceAgent.listener` and it drives the entire BLE lifecycle — scan, connect, bind, device state, recording, and file sync. A single global listener receives every device event; all methods return `Unit`.
```kotlin Kotlin icon="android" theme={"system"}
PlaudDeviceAgent.listener = object : PlaudDeviceAgentListener {
override fun blePenState(state: Int, privacy: Int, keyState: Int, uDisk: Int) {
// handshake complete — device is ready
}
// override only the callbacks you need
}
```
| Group | Callback | Description |
| --------------- | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| Connection | `bleScanResult(devices: List)` | Scan results updated |
| Connection | `bleScanOverTime()` | Scan window elapsed |
| Connection | `bleConnectState(state: Int)` | `1` = connected, `0` = disconnected, `2` = failed |
| Connection | `bleBind(sn: String?, status: Int, protVersion: Int, timezone: Int)` | Binding result — `status == 0` → bound OK |
| Connection | `bleDepair(status: Int)` | Unpair / de-bind result |
| Connection | `blePenState(state: Int, privacy: Int, keyState: Int, uDisk: Int)` | Handshake / device state reported after the handshake completes |
| Connection | `bleDeviceName(name: String)` | Device name read / changed |
| Power & storage | `blePowerChange(power: Int, oldPower: Int)` | Battery % changed |
| Power & storage | `bleChargingState(isCharging: Boolean, level: Int)` | Charging state + battery level |
| Power & storage | `bleStorage(total: Long, free: Long, duration: Long)` | Bytes total/free + recordable seconds |
| Power & storage | `bleMicGain(gain: Int)` | Mic gain read |
| Recording | `bleRecordStart(sessionId: Long, start: Long, status: Int, scene: Int, startTime: Long, reason: Int)` | Recording started |
| Recording | `bleRecordStop(sessionId: Long, reason: Int, fileExist: Boolean, fileSize: Long)` | Recording stopped |
| Recording | `bleRecordPause(sessionId: Long, reason: Int, fileExist: Boolean, fileSize: Long)` | Recording paused |
| Recording | `bleRecordResume(sessionId: Long, start: Long, status: Int, scene: Int, startTime: Long)` | Recording resumed |
| File sync | `bleFileList(files: List)` | Result of `getFileList(...)` |
| File sync | `bleSyncFileHead(sessionId: Long, status: Int)` | Sync started for a file |
| File sync | `bleSyncFileTail(sessionId: Long, status: Int)` | Sync finished for a file |
| File sync | `bleData(sessionId: Long, timestamp: Long, data: ByteArray)` | A chunk of streaming audio/PCM bytes |
| File sync | `bleDataComplete()` | Data stream complete |
| File sync | `bleSyncFileStop()` | Sync stopped |
| File sync | `bleDeleteFile(sessionId: Long, status: Int)` | Result of `deleteFile(...)` |
| WiFi | `bleWiFiOpen(status: Int, ssid: String, password: String, url: String)` | Device opened its hotspot |
| OTA | `bleFotaResult(sessionId: Long, status: Int, message: String)` | Firmware push result |
The listener also delivers the WiFi auto-sync configuration results (`onWifiSyncEnabled`, `onWifiSyncListReceived`, `onWifiSyncConfigReceived`, `onWifiSyncConfigSet`, `onWifiSyncDeleteResult`, `onWifiSyncTestStarted`, `onWifiSyncTestResult`). These configure the device's own scheduled background upload and are distinct from WiFi Fast Transfer.
### AudioExporter.ExportCallback
Reports progress, completion, and errors for `exportAudio(...)` (BLE) and `exportAudioViaWiFi(...)` (WiFi).
```kotlin Kotlin icon="android" theme={"system"}
val callback = object : AudioExporter.ExportCallback {
override fun onProgress(progress: Int, message: String) {
// progress: 0–100 (download bytes)
}
override fun onComplete(outputFile: File) {
// decoded file is ready at outputFile
}
override fun onError(error: String) {
// export failed
}
override fun onStageChanged(stage: ExportStage) {
// optional — DOWNLOADING → TRANSCODING
}
}
```
Export progress, `0`–`100` (download bytes), with a human-readable status message.
Called when decoding finishes; `outputFile` is the written file.
Called if export fails, with a description of the error.
Optional (has a default implementation). Language-neutral phase signal: `DOWNLOADING` (bytes still transferring off the device) → `TRANSCODING` (only local decode/encode remains).
### IWifiTransferAgent.WifiTransferCallback
WiFi Fast Transfer is split across both high-level interfaces, so you will use `IWifiTransferAgent` directly for any fast transfer. `PlaudDeviceAgent` owns the session lifecycle and the audio pipeline; `IWifiTransferAgent` owns the file operations and the callback you implement to drive them.
```kotlin Kotlin icon="android" theme={"system"}
val wifi: IWifiTransferAgent? = PlaudDeviceAgent.getWifiAgent()
```
#### WiFi Fast Transfer
| Functionality | Method |
| ------------------------------- | ------------------------------------------------------------------ |
| Open the device hotspot | `PlaudDeviceAgent.setDeviceWiFi(open = true)` |
| Start the session | `PlaudDeviceAgent.startWifiTransfer(userId, callback)` |
| List files on the device | `IWifiTransferAgent.getFileList()` |
| Download + decode one recording | `PlaudDeviceAgent.exportAudioViaWiFi(...)` |
| Delete recordings in a batch | `IWifiTransferAgent.deleteFiles(sessionIds)` |
| Check session state | `IWifiTransferAgent.getConnectionState()` / `checkPrerequisites()` |
| End the session | `PlaudDeviceAgent.endWiFiTransfer()` |
The typical flow: open the hotspot over BLE, start the session, wait for `READY`, list files, then export each one.
#### WifiTransferCallback
**12 required members** for Wifi Transfers, generally driven by `PlaudDeviceAgent.exportAudioViaWiFi`.
| Callback | Description |
| ----------------------------------------------------- | ---------------------------------------------------- |
| `onConnectionStateChanged(state)` | `READY` = handshake complete, safe to issue commands |
| `onHandshakeCompleted(info)` | Handshake detail string |
| `onFileListReceived(files)` | Result of `getFileList()`, as `WifiFileInfo` |
| `onTransferProgress(sessionId, progress, speed)` | `progress` 0–100, `speed` in KB/s |
| `onFileTransferCompleted(sessionId, path)` | A single file finished |
| `onBatchDownloadStarted(total)` | `downloadAllFiles()` began |
| `onBatchDownloadProgress(current, total, filename)` | Batch position |
| `onBatchDownloadCompleted(success, failed, results)` | Batch finished, with per-file `BatchDownloadResult` |
| `onFileDeleteCompleted(success, deletedCount, error)` | Result of `deleteFiles(...)` |
| `onDeviceBatteryUpdate(level, charging)` | Device battery over the WiFi session |
| `onWifiTransferStopped()` | Session ended |
| `onError(code, message)` | A command failed |
`WifiFileInfo` carries `sessionId`, `fileName`, `fileSize`, `duration` (ms), `timestamp` (epoch seconds), and `scene` — more per-file metadata than `BleFile` exposes over BLE.
### FirmwareUpdateCallback
Reports each phase of the OTA flow across `checkFirmwareUpdate(...)`, `downloadFirmware(...)`, and `installFirmware(...)`. Extend `SimpleFirmwareUpdateCallback()` to override only the methods you need — each has an empty default implementation.
```kotlin Kotlin icon="android" theme={"system"}
val callback = object : SimpleFirmwareUpdateCallback() {
override fun onUpdateCheckResult(result: Result) {
val info = result.getOrNull() ?: return
if (info.hasUpdate) { /* proceed to download */ }
}
override fun onDownloadProgress(progress: UpdateProgress) { /* progress.progress: 0–100 */ }
override fun onDownloadComplete(result: FirmwareDownloadResult) {
if (result.success) { result.file?.let { /* install */ } }
}
override fun onInstallProgress(progress: UpdateProgress) { }
override fun onInstallComplete(result: FirmwareInstallResult) {
if (result.success) { /* device restarts and reconnects */ }
}
}
```
| Callback | Description |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `onUpdateCheckResult(result: Result)` | Version-check result. `FirmwareUpdateInfo` carries `hasUpdate`, `currentVersion`, `isForceUpdate`, and the full `versionResponse` |
| `onDownloadProgress(progress: UpdateProgress)` | Download progress (`progress` 0–100, plus `message`, `detail`, `transferPhase`) |
| `onDownloadComplete(result: FirmwareDownloadResult)` | Download + MD5 verify finished (`success`, `file`, `md5Valid`, `error`) |
| `onInstallProgress(progress: UpdateProgress)` | Install progress (BLE packet push + device restart) |
| `onInstallComplete(result: FirmwareInstallResult)` | Install finished (`success`, `error`) |
# Android Starter App
Source: https://docs.plaud.ai/plaud-embedded/android-starter-app
Build a branded Android app that connects to Plaud devices in minutes using the Android starter app.
This guide walks through the process of building a native Android [Starter App](/plaud-embedded/starter-app-specs) that:
1. Connects to Plaud devices
2. Syncs recordings from Plaud device to mobile phone
3. Uploads recordings from your users' mobile phone to Plaud's file storage
4. Transcribes recordings with the [Transcription API](/plaud-embedded/transcription-api-overview)
## Onboard to the Plaud Developer Platform
Sign in to the [Plaud Developer Portal](https://portal.plaud.ai/) (It's completely free with a generous device connection limit and transcription usage).
Create an **Embedded SDK Application** to receive your **Client ID** and **Secret Key**.
## Set Up the Starter App
**Try the Plaud Embedded Skill** to have your coding agent help you through your Starter App deployment.
```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.
### Prerequisites
* Android Studio (JDK 17)
* minSdk 21 (Android 5.0+), compileSdk 34
* A **physical Android device** — the SDK libraries are `arm64-v8a`/`armeabi-v7a`, Android simulators are not supported
* A Plaud device for end-to-end testing
### Clone the starter app
You can find the GitHub repository for the [starter app here](https://github.com/Plaud-AI/plaud-sdk-public).
```bash theme={"system"}
git clone https://github.com/Plaud-AI/plaud-sdk-public.git
cd plaud-sdk-public/android
```
Install [Android Studio](https://developer.android.com/) to build and test your Android Starter app.
```bash theme={"system"}
brew install --cask android-studio
```
### Retrieve a user token
The `USER_ACCESS_TOKEN` is the per-user JWT your backend mints by calling `POST /open/partner/users/access-token`. See the [Authorization API reference](/plaud-embedded/auth-api-overview) for the full exchange flow.
### Configure credentials
After navigating to the `/android` directory, create a `local.properties` file (gitignored) and add the **user token, client ID, and API key** (if using transcription).
```bash local.properties theme={"system"}
sdk.dir=/path/to/your/Android/sdk
# Required for SDK initialization
PLAUD_USER_ACCESS_TOKEN=your-jwt-token
# Required for Transcription API (optional, not needed for device features)
PLAUD_CLIENT_ID=your-client-id
PLAUD_API_KEY=your-api-key
```
### (Optional) Apply branding
Four places control the entire visual identity:
#### App Name
Navigate to `app/src/main/res/values/strings.xml`. You can modify the app name, and other text on the welcome screen.
```xml strings.xml theme={"system"}
Plaud Template
App name
Get Started
Connect device later
```
#### App icon
Create new image assets directly in Android Studio. Right-click `res`, then go to `New > Image Asset`.
#### Theme colors
In `res/values/colors.xml`, you have full control over the starter app's primary colors and color palette.
```xml colors.xml theme={"system"}
#F9F9F9
#1F1F1F
#A3A3A3
#EBEBEB
```
## Run & test with a real device
Open **Android Studio**, and wait for gradle to finish building the app.
Once indexing and build is finished, **connect your physical Android device** and run the app.
Verify app launches, device pairs, recording syncs, and transcript appears on your Android app.
**Unbind your Plaud device after testing and before uninstalling the Starter App!**
Plaud devices can only be bound to one application at a time (tied to your Partner Token). You will not be able to bind your Plaud device to another app (or the Plaud App) before unbinding from the Starter App.
## Publishing to the Google Play Store
If you don't have a [Google Play Developer Account](https://play.google.com/console/signup), you will need to sign up to publish your app to the Play Store .
There is a one-time \$25 registration fee, and a verification process that may take a few days.
In `app/build.gradle`, replace the template's application ID with your own **unique** reverse-domain identifier and bump the `compileSdk`. **Sdk 35** is the minimum version that Google Play will accept due to security and performance APIs that are only compatible with 35+.
```kotlin app/build.gradle theme={"system"}
android {
namespace 'com.plaud.template'// [!code --]
namespace 'com.yourorg.yourapp'// [!code ++]
compileSdk 34// [!code --]
compileSdk 35// [!code ++]
defaultConfig {
applicationId "com.plaud.template"// [!code --]
applicationId "com.yourorg.yourapp"// [!code ++]
minSdk 21
targetSdk 34// [!code ++]
```
Bump `versionCode` on every upload — Play Console rejects a build that reuses a `versionCode` already in the track.
You can use your editor's find and replace or this bash command to replace all strings in the `app/` directory.
```bash theme={"system"}
rg -l 'com\.plaud\.template' app/ | xargs sed -i '' 's/com\.plaud\.template/com.yourorg.yourapp/g'
```
Play requires every build to be signed with a consistent upload key. In Android Studio, go to `Build > Generate Signed App Bundle / APK`, choose **Android App Bundle**, then `Create new...` to generate a keystore.
If you lose your key, you will have to go through Google's key reset process.
Finish the `Build > Generate Signed App Bundle / APK` flow with the `release` build variant. Android Studio writes the bundle to `app/release/app-release.aab`.
In the [Google Play Console](https://play.google.com/console), select `Create app`, then set your app name, default language, app type, and free/paid status. The application ID from Step 2 is claimed when you upload your first bundle.
Under `Testing > Internal testing`, create a release and upload your `.aab`.
Add your testers by email or Google Group, then share the opt-in link with them.
Under the `Internal app sharing` tab, enable sharing. Allow your email list to access and download your app.
Work through the `Dashboard` checklist. Items to emphasize for a conversation capture app are:
1. **Data safety**
2. **Permissions declaration**: the SDK requires Bluetooth and (on Android 11 and below) location permissions to scan for and connect to Plaud devices.
3. **Privacy policy**
4. **Store listing assets** — app icon (512×512), feature graphic (1024×500), and at least two phone screenshots.
Once the checklist is complete, submit your production release for review! Google's review typically takes a few days for a first submission.
# API Playground
Source: https://docs.plaud.ai/plaud-embedded/api-playground
# Authentication APIs
Source: https://docs.plaud.ai/plaud-embedded/auth-api-overview
Authenticate to Plaud APIs to connect (bind) devices to your mobile app and use the Transcription API
As a Plaud Partner on the developer platform, you will need two types of tokens to authenticate to Plaud Embedded:
1. **Partner Token** - This is an **application-level token** you can use to authenticate all of your users.
2. **User Token** - This is a **user-level token** used to authenticate their device to your mobile app using the [Embedded SDK](/plaud-embedded/ios-sdk).
## Using the Authentication API
### Prerequisites
Use Plaud's Authentication APIs to retrieve your Partner Token and User Token using your app's `client_id` and `client_secret` found in the [developer portal](https://portal.plaud.ai/).
Try the [Plaud Embedded API Playground](https://plaud-embedded-playground.vercel.app/) to see every step of the transcription process with your own client credentials.
End-to-end, from **authentication** to **recording audio** to **uploading** to **transcription**.
### Exchange Credentials for a Partner Token
The Partner Token is an application-level token to issue user-level tokens.
```http theme={"system"}
POST https://platform-us.plaud.ai/developer/api/oauth/partner/access-token
Authorization: Basic base64(client_id:secret_key)
Content-Type: application/x-www-form-urlencoded
```
```json theme={"system"}
{
"access_token": "eyJhbGciOiJSUz...",
"refresh_token": "eyJhbGci...",
"token_type": "bearer",
"expires_in": 3600
}
```
### Mint a User Token with Your Partner Token
The User Token is a user-specific token. This token will be used in the [Embedded SDK](/plaud-embedded/ios-sdk) to bind to your users' devices and upload files.
```http theme={"system"}
POST https://platform-us.plaud.ai/developer/api/open/partner/users/access-token
Authorization: Bearer
Content-Type: application/json
{
"user_id": "",
"expires_in": 86400
}
```
```json theme={"system"}
{
"access_token": "eyJhbGci...",
"token_type": "bearer",
"expires_in": 86400
}
```
Plaud's cloud services are hosted in the U.S. by default. If you're
interested in multi-region hosting in Japan, Europe, and Singapore,
please [reach out to our sales team](https://dev.plaud.ai/contact).
# Billing
Source: https://docs.plaud.ai/plaud-embedded/billing
Prices and billing for Plaud Embedded apps
Plaud Embedded has a PAYG model that scales along **connected devices** and **transcription hours**.
| Usage Type | Definition |
| ------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Connected Devices | Number of Plaud devices your Embedded mobile app has connected to via the [Embedded SDK](/plaud-embedded/ios-sdk) |
| Transcription Hours | Hours of audio transcribed via the [Transcription API](/plaud-embedded/transcription-api-overview) |
Connected Devices are measured as the **maximum number of devices connected to your mobile app in a given month**.
If you had 25 connected devices at the beginning of the month and 20, your connected device count for
the month would be 25.
Join our [Partner Program](https://plaud-embedded-partner-us.bixgrow.com/) to track your customers' device purchases,
qualify for volume discounts, and participate in revenue-based incentives in the future.
## Free Usage
As a new Plaud Embedded developer, you have:
* **50 free Connected Devices**
* **300 hours of free transcription** (after first device connection)
The 50 free Connected Devices should cover all testing and even small rollouts to your end-users. We've had
many customers roll out pilots to their users on the free tier! See examples in our [customer stories](https://dev.plaud.ai/customers).
Once you've connected a device via the [Embedded SDK](/plaud-embedded/ios-sdk), your 300 transcription hours will be
unlocked for your testing and pilots.
## PAYG Pricing
After your free usage, billing will be charged through a PAYG model:
| Usage Type | Definition | Rate |
| ------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------- |
| Connected Devices | Number of Plaud devices your Embedded mobile app has connected to via the [Embedded SDK](/plaud-embedded/ios-sdk) | **\$15/device (per month)** |
| Transcription Hours | Hours of audio transcribed via the [Transcription API](/plaud-embedded/transcription-api-overview) | **\$0.28/hour** |
Charges occur **at the end of the month**. Any payment failures will have a 7 day grace period before services are denied for your Embedded app.
## Enterprise Plans
The PAYG model does not have device or transcription usage limits.
If you're interested in an enterprise plan for HIPAA compliance, volume discounts,
and dedicated support, please reach out to our team by filling out our [contact form](https://dev.plaud.ai/contact/).
# Changelog
Source: https://docs.plaud.ai/plaud-embedded/changelog
Device Recovery and Binding Visibility
Encrypted devices that were left bound to a previous user can now be recovered directly from the Embedded SDK, and connection attempts report where they are in the handshake.
* `recoveryConnectBleDevice` reclaims an encrypted device still holding a prior user's credentials, using the historical user ID from bind history
* Connection stage callbacks surface each step of the BLE handshake, along with the reason a connection failed
* All SDK error messages and progress text returned to your app are now in English on both iOS and Android
[Android Embedded SDK](/plaud-embedded/android-sdk) release with full feature parity with the iOS Embedded
SDK.
* Covers scanning, connection, recording, file listing/sync/delete, settings, WiFi provisioning, WiFi fast transfer, and OTA
MP3 export now supported over iOS WiFi fast transfer
Transfer Insights & Export Progress
The Embedded SDK now surfaces real-time transfer speed and more accurate export progress across both WiFi fast transfer and BLE.
* Live download speed reporting for WiFi fast transfer and BLE transfers
* End-to-end export progress (0–100%) across download and conversion
* Batch download support to export all recordings in a single call
MP3 Export & WiFi Fast Transfer
The Embedded SDK now supports new audio export and transfer options for recordings.
* MP3 export format added alongside existing audio formats
* WiFi fast transfer for quicker recording downloads from your device
Developer Platform Beta Launch
The Plaud Developer Platform is now available in beta. Customers can integrate Plaud recordings, transcripts, and AI summaries into their own applications using the Embedded SDK.
* Embedded SDK for iOS and Android
* Starter app to get started
# Data Retention
Source: https://docs.plaud.ai/plaud-embedded/data-retention
How data is stored with Plaud Embedded
With Plaud Embedded, you maintain control of your users' conversation data.
There are two components of our system your users' data may flow into:
1. **Plaud devices**: Both the Note Pro and Note Pin S have 64GB of local storage
2. **Plaud cloud services**: For transcribing audio files
While Plaud Embedded was designed for builders to integrate with our hardware (Plaud devices) and software (transcription models),
you **do NOT have to opt in to both**.
### Complete Plaud Embedded Integration
## Full Data Custody
For users with strict data retention policies, integrating with **just Plaud devices** keeps your users'
data completely on your users' devices and your application services.
## Multi-Region / Specific Region Requirements
For users using Plaud's Transcription API, services are hosted in U.S. regions by default. If you
have specific requirements (i.e. GDPR or multi-region support), your data can be processed in
these select regions:
| Region | Public host | Status |
| --------- | ------------------------------------ | --------------------------------------------- |
| US | `platform-us.plaud.ai/developer/api` | Default |
| Japan | `platform-jp.plaud.ai/developer/api` | [Contact Sales](https://dev.plaud.ai/contact) |
| Europe | `platform-eu.plaud.ai/developer/api` | [Contact Sales](https://dev.plaud.ai/contact) |
| Singapore | `platform-sg.plaud.ai/developer/api` | [Contact Sales](https://dev.plaud.ai/contact) |
Transcriptions processed with our APIs are **retained for 7 days by default** (this can be configured on custom plans).
# Device Binding APIs
Source: https://docs.plaud.ai/plaud-embedded/device-binding-api-overview
APIs to register and sync Plaud device state
Binding a Plaud device involves two steps:
1. **Cloud bind**: registers the device/owner association in the Plaud registry so you can track Connected Device state remotely
2. **Local device bind**: triggered over BLE by the Embedded SDK, which verifies and generates the key pair on-device
The `/bind`, `/unbind`, and `/binding` are used to update the Plaud registry to keep your local device state and remote device state in-sync at all times.
## Using the Device Binding APIs
### Prerequisites
The Device Binding APIs authenticate with a **User Token**. Use the [Authentication API](/plaud-embedded/auth-api-overview) if you don't have a User Token or need a new one.
### Binding a Plaud Device to a User
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 are always encrypted and can only be decrypted with a valid User Token by your application.
The `POST /sdk/bind` endpoint registers the device/owner association in the Plaud registry.
```http theme={"system"}
POST https://platform-us.plaud.ai/developer/api/open/partner/sdk/bind
Content-Type: application/json
Authorization: Bearer
{
"type": "notepro",
"sn": "8810000000000001"
}
```
```json theme={"system"}
{
"type": "notepro",
"sn": "8810000000000001",
"is_bind": true
}
```
Device type string derived from the SN prefix (`881` → `notepro`, `882` → `notepins`).
Device serial number.
After the cloud bind succeeds, run the local bind over BLE with the Embedded SDK. See [iOS](/plaud-embedded/ios-sdk#connecting-binding-to-a-plaud-device) or [Android](/plaud-embedded/android-sdk#connecting-binding-to-a-plaud-device) for the SDK side of the flow.
Plaud devices can only be bound to one mobile application. If a user is **uninstalling your mobile app, make sure to unbind their Plaud device.**
### Unbinding a Plaud Device to a User
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.
The `POST /sdk/unbind` endpoint removes the device/owner association in the Plaud registry.
```http theme={"system"}
POST https://platform-us.plaud.ai/developer/api/open/partner/sdk/unbind
Content-Type: application/json
Authorization: Bearer
{
"type": "notepro",
"sn": "8810000000000001"
}
```
```json theme={"system"}
{
"type": "notepro",
"sn": "8810000000000001",
"is_bind": false
}
```
Device type string derived from the SN prefix (`881` → `notepro`, `882` → `notepins`).
Device serial number.
Pair this call with the SDK's depair over BLE, which clears the pairing/handshake on the device itself. See [iOS](/plaud-embedded/ios-sdk#standard-unbind) or [Android](/plaud-embedded/android-sdk#standard-unbind) for the SDK side of the flow.
### Recovering a Plaud Device
In certain situations, the cloud and local bind state can go **out-of-sync**, causing the device to lock. Device recovery re-handshakes with each previously bound client ID until one matches the lock on the device, at which point the stale ownership lock is wiped and the device can be bound to the current user.
The `GET /sdk/binding` endpoint returns the remote binding state of a device along with every client ID it has previously been bound to, including bindings made in other apps.
```http theme={"system"}
GET https://platform-us.plaud.ai/developer/api/open/partner/sdk/binding?type=notepro&sn=8810000000000001
Authorization: Bearer
```
```json theme={"system"}
{
"is_bind": false,
"bind_history": [
"b7c1f0a2-4d5e-4a3b-9c8d-1e2f3a4b5c6d",
"3f9e8d7c-6b5a-4938-8271-0a1b2c3d4e5f"
]
}
```
Device type string derived from the SN prefix (`881` → `notepro`, `882` → `notepins`).
Device serial number.
**Response:**
`true` = bound to another account (stop — recovery is not possible), `false` = unbound, `null` = signed but never bound. Run Device Recovery only when this is **not** `true`.
Previously bound client IDs, newest first.
`bind_history` records one entry **per bind event**, so the same client ID can appear more than once — de-duplicate before using it for Device Recovery.
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.
Pass each client ID from `bind_history` to the SDK's recovery connect, which uses that ID as the handshake token and connects with force-clear. See [iOS](/plaud-embedded/ios-sdk#device-recovery) or [Android](/plaud-embedded/android-sdk#device-recovery) for the SDK side of the flow.
# Plaud Devices
Source: https://docs.plaud.ai/plaud-embedded/devices
Devices designed for conversation-based work
Plaud devices are built for executives, sales teams, clinicians, and anyone who needs their conversation data turned into insights and actions.
On Plaud devices, user data is always kept **private and secure**, meaning:
* Encrypted-at-rest
* Full control over what files are shared and synced
* GDPR, SOC2, and HIPAA compliant practices
Plaud currently has two flagship devices:
Designed for phone calls and conversation-heavy workflows.
Designed for hands-free, on-the-go use in field work, healthcare, retail, and beyond.
Plaud Embedded currently only supports the Plaud Note Pro and Plaud NotePin S.
The Plaud Note and Plaud NotePin are NOT supported under Plaud Embedded.
| Specs | Plaud Note Pro | Plaud NotePin S |
| ---------------------------------- | ----------------------------------- | --------------------------- |
| Battery | 30–50 h (endurance mode) | 20 h |
| Microphones | 4 MEMS, 1 VPU | 2 MEMS |
| Connectivity | Dual-band Wi-Fi + Bluetooth | Dual-band Wi-Fi + Bluetooth |
| Storage | 64G | 64G |
| Mode | Smart dual-mode (calls + in-person) | In-person conversations |
| Max single-file recording duration | 5h | 5h |
Join our [Partner Program](https://plaud-embedded-partner-us.bixgrow.com/) to track your customers' device purchases,
qualify for volume discounts, and participate in revenue-based incentives in the future.
# File Upload API
Source: https://docs.plaud.ai/plaud-embedded/file-api-overview
Upload your user's conversation audio to Plaud storage and get a download URL to pass to the Transcription API.
The [Transcription API](/plaud-embedded/transcription-api-overview) accepts any publicly accessible audio URL.
**This File Upload API is optional**. You can host your audio files on your own storage if you prefer.
The File Upload API uploads audio files from your users' mobile apps to Plaud storage. Once files are uploaded, these files can be passed to the [Transcription API](/plaud-embedded/transcription-api-overview) to take advantage of Plaud's transcription and ASR models.
Using the `POST /generate-presigned-urls` endpoint, Plaud will send an array of pre-signed S3 upload endpoints for you to upload your users' audio files
You must send **\<`ChunkSize`** of data to each `PresignedUrl`.
`PUT` the raw bytes of each chunk directly to its presigned S3 URL (no auth needed), then read the `ETag` (Entity Tag) response header for each part.
Send your list of `ETag`s to the `POST /complete-upload` endpoint to receive your file's `DownloadUrl`
## Using the File Upload API
## Prerequisites
The File Upload API authenticates with a **User Token**. Use the [Authentication API](/plaud-embedded/auth-api-overview) if you don't have a User Token or need a new one.
Try the [Plaud Embedded API Playground](https://plaud-embedded-playground.vercel.app/) to see every step of the transcription process with your own client credentials.
End-to-end, from **authentication** to **recording audio** to **uploading** to **transcription**.
### Generate Presigned Upload URLs
Request presigned S3 URLs for a multipart upload from Plaud's API. The number of presigned URLs will depend on your file size.
```http theme={"system"}
POST https://platform-us.plaud.ai/developer/api/open/partner/files/upload/generate-presigned-urls
Content-Type: application/json
Authorization: Bearer
{
"filesize": 10485760,
"filetype": "mp3"
}
```
```json theme={"system"}
{
"FileId": "file_xxx",
"UploadId": "upload_xxx",
"ChunkSize": 5242880,
"Parts": [
{ "PartNumber": 1, "PresignedUrl": "https://plaud-bucket.s3.amazonaws.com/..." },
{ "PartNumber": 2, "PresignedUrl": "https://plaud-bucket.s3.amazonaws.com/..." }
]
}
```
### Upload Each Chunk to S3
For each part, `PUT` up to the `ChunkSize` of raw bytes (5MB) to its `PresignedUrl`. These AWS S3 calls are presigned thus no authentication is needed.
Keep the `ETag` (Entity Tags) found in the response headers from each `PUT` call. These are needed in `POST /complete-upload`.
```http theme={"system"}
PUT [PresignedUrl]
[raw bytes of chunk]
```
The S3 response includes the `ETag` header for that part:
```http theme={"system"}
ETag: "abc123..."
```
### Complete the Multipart Upload
Submit the:
1. `file_id` from the first step
2. `upload_id` from the first step
3. Array of `ETag` with its corresponding `PartNumber` from the second step
```http theme={"system"}
POST https://platform-us.plaud.ai/developer/api/open/partner/files/upload/complete-upload
Content-Type: application/json
Authorization: Bearer
{
"file_id": "file_xxx",
"upload_id": "upload_xxx",
"part_list": [
{ "PartNumber": 1, "ETag": "\"abc123...\"" },
{ "PartNumber": 2, "ETag": "\"def456...\"" }
],
"filetype": "mp3",
"file_md5": "9e107d9d372bb6826bd81d3542a419d6"
}
```
```json theme={"system"}
{
"FileId": "file_xxx",
"FileType": "mp3",
"DownloadUrl": "https://plaud-bucket.s3.amazonaws.com/...",
"FileMd5": "9e107d9d372bb6826bd81d3542a419d6"
}
```
The returned `DownloadUrl` is valid for **24 hours**. Pass it as `file_url` to the [Transcription API](/plaud-embedded/transcription-api-overview) to transcribe your file!
Plaud's cloud services are hosted in the U.S. by default. If you're
interested in multi-region hosting in Japan, Europe, and Singapore,
please [reach out to our sales team](https://dev.plaud.ai/contact).
# Using Flutter
Source: https://docs.plaud.ai/plaud-embedded/flutter
You can use Plaud's iOS and Android SDK in Flutter through a [Flutter plugin](https://docs.flutter.dev/packages-and-plugins/developing-packages#plugin). Use our `plaud_sdk` plugin as a starter template for:
1. Connecting to Plaud Devices
2. Syncing audio files
3. Transcription
For advanced usage of the Embedded SDK methods for use cases like WiFi fast transfers, we recommend adding more methods to this plugin or using the native [Embedded iOS SDK](/plaud-embedded/ios-sdk) directly
## How it works
Flutter uses [platform channels](https://docs.flutter.dev/platform-integration/platform-channels) to let your Flutter app call native platform code. A [plugin](https://docs.flutter.dev/packages-and-plugins/developing-packages#plugin) packages that native code so it can be dropped into any Flutter app.
Plaud Embedded's plugin can be added as a path dependency, imported as a typed Dart library, and run as native code via the [Embedded iOS SDK](/plaud-embedded/ios-sdk) and [Embedded Android SDK](/plaud-embedded/android-sdk).
```
your Flutter code
│ import 'package:plaud_sdk/plaud_sdk.dart'
▼
┌─────────────────────────────┐
│ Dart layer (lib/*.dart) │ static PlaudSdk API, fully typed,
│ │ guard with isPlaudSdkAvailable off-iOS
└─────────────────────────────┘
│ MethodChannel (calls) / EventChannel (typed streams)
┌─────────────────────────────┐
│ Plaud native SDK │ three precompiled .xcframeworks
│ (ios/Frameworks/*) │ BLE / Device / WiFi
└─────────────────────────────┘
```
Plaud's Flutter plugin uses a `MethodChannel` for method calls and an `EventChannel` for event listening, passing data between the Dart layer and the native SDK.
## Running the Demo App
The demo app is included in the Plaud Embedded Flutter repo as reference for implementing the plugin in your own app and seeing how it works.
```bash theme={"system"}
git clone https://github.com/Plaud-AI/embedded-flutter.git
```
```bash theme={"system"}
cd embedded-flutter
flutter pub get
cp .env.example .env
```
You can retrieve your environment credentials from the [developer portal](https://portal.plaud.ai/) and generate a token from our [API playground](https://plaud-embedded-playground.vercel.app)
Open `ios/Runner.xcworkspace` once to set your Apple developer team (Runner → Signing & Capabilities).
Then **run on a physical device** to test out the demo app with your Plaud devices. Credentials are compile-time defines, so `--dart-define-from-file=.env` is required on every run.
```bash theme={"system"}
flutter run --dart-define-from-file=.env
```
## How to Integrate with your Flutter App
Try the Embedded Flutter Skill to upload this doc and the codebase context for your agent.
```bash theme={"system"}
npx skills add Plaud-AI/embedded_flutter
```
### Prerequisites
| Tool | Notes |
| --------------------- | ------------------------------------------ |
| Flutter | Dart SDK 3.12+ |
| Xcode | 15.x+, with a physical iPhone + Apple ID |
| CocoaPods | `brew install cocoapods` |
| iOS deployment target | **15.1** (the Plaud frameworks require it) |
### Step 1: Clone the Plaud Embedded Flutter Repo
```bash theme={"system"}
git clone https://github.com/Plaud-AI/embedded_flutter.git
```
This repo includes:
1. Flutter plugin (`plugins/plaud_sdk`) for the Plaud iOS and Android SDK for basic functionality
2. Example Flutter app
3. Skill for implementing the Plaud Embedded plugin within projects
### Step 2: Copy the Plaud Embedded Plugin into your App
At the root of your Flutter project, copy `plugins/plaud_sdk` into your project's `plugins` directory.
```bash theme={"system"}
cp -R plugins/plaud_sdk your-app/plugins/plaud_sdk
```
Then depend on the plugin via a path reference in your `pubspec.yaml`:
```yaml pubspec.yaml theme={"system"}
dependencies:
plaud_sdk:
path: plugins/plaud_sdk
```
```bash theme={"system"}
flutter pub get
```
Flutter's plugin autolinking handles importing the plaud\_sdk plugin; no extra configuration is needed.
### Step 3: Configure Permissions
**For iOS**, set the deployment target to **15.1** in `ios/Podfile` (`platform :ios, '15.1'`) and the Runner Xcode target — the Plaud frameworks require it, don't go lower.
Then add BLE permissions to `ios/Runner/Info.plist` for the Plaud SDK to leverage iOS's native bluetooth functionality (without the first key, the app hard-crashes the moment it touches Bluetooth).
```xml Info.plist theme={"system"}
NSBluetoothAlwaysUsageDescription
Plaud uses Bluetooth to connect to your recorder and sync recordings.
UIBackgroundModes
bluetooth-central
```
**For Android**, the permissions are in your manifest from the plugin. Permissions requests are done at runtime.
### Step 4: Use the Plaud SDK from Flutter
You can now import the `plaud_sdk` library and use the [Embedded iOS SDK](/plaud-embedded/ios-sdk) and [Embedded Android SDK](/plaud-embedded/android-sdk) straight from your Flutter project in Dart.
```dart theme={"system"}
import 'package:plaud_sdk/plaud_sdk.dart';
if (!isPlaudSdkAvailable) { /* off-device: show a "device required" state */ }
await PlaudSdk.initSDK(
userAccessToken: token, // per-user Bearer JWT
customDomain: 'platform-us.plaud.ai', // domain only, no https://
userId: 'your-app-user-id', // default connect deviceToken
);
final subs = [
PlaudSdk.onScanResult.listen((devices) {/* show devices */}),
PlaudSdk.onConnectState.listen((s) {
if (s.connected) PlaudSdk.getFileList(); // load recordings on connect
}),
PlaudSdk.onFileList.listen((files) {/* show recordings */}),
PlaudSdk.onExportProgress.listen((p) {/* p.progress (0–100), p.message */}),
];
await PlaudSdk.startScan();
await PlaudSdk.connectBleDevice(uuid: device.uuid); // from an onScanResult device
final export = await PlaudSdk.exportAudio(
sessionId: file.sessionId,
format: PlaudAudioFormat.mp3,
);
// export.outputPath → the decoded mp3 under Documents/PlaudExports
for (final s in subs) { s.cancel(); }
```
### Step 5: Run Your App
```bash theme={"system"}
flutter pub get
flutter run --dart-define-from-file=.env
```
For the full list of relevant SDK methods for interacting with Plaud devices, see our [iOS SDK reference](/plaud-embedded/ios-sdk) and [Android SDK reference](/plaud-embedded/android-sdk).
# How Plaud Embedded Works
Source: https://docs.plaud.ai/plaud-embedded/how-plaud-embedded-works
We'll walk through a complete Plaud Embedded implementation to understand how the SDKs and APIs fit together.
Multiple SDKs and APIs can be a lot to juggle. **Don't worry!** We'll walk through the steps you need to know to implement Plaud Embedded end-to-end.
## The End-to-End Flow
In general, integrating with Plaud Embedded end-to-end is done through the following steps:
Try the [Plaud Embedded API Playground](https://plaud-embedded-playground.vercel.app/) to see every step of the transcription process with your own client credentials.
End-to-end, from **authentication** to **recording audio** to **uploading** to **transcription**.
Let's go through each step.
### Retrieving Tokens and Authenticating
Plaud Embedded's SDK and APIs have two different authentication methods to be aware of:
1. User Token - user-level tokens with your **user's unique ID** and minted with a **Partner Token**
2. Client ID & API Key - used for the Transcription API
A Partner Token is an **application-level** token. A single Partner Token can mint multiple **User Tokens**.
Use the [Authentication APIs](/plaud-embedded/auth-api-overview) to retrieve your Partner Tokens and mint User Tokens.
```http Exchanging client credentials for Partner Token expandable theme={"system"}
POST https://platform-us.plaud.ai/developer/api/oauth/partner/access-token
Authorization: Basic base64(client_id:secret_key)
Content-Type: application/x-www-form-urlencoded
//Returns
{
"access_token": "eyJhbGciOiJSUz...",
"refresh_token": "eyJhbGci...",
"token_type": "bearer",
"expires_in": 3600
}
```
```http Minting User Token expandable theme={"system"}
POST https://platform-us.plaud.ai/developer/api/open/partner/users/access-token
Authorization: Bearer
Content-Type: application/json
{
"user_id": "",
"expires_in": 86400
}
//Returns
{
"access_token": "eyJhbGci...",
"token_type": "bearer",
"expires_in": 86400
}
```
### Binding Plaud Devices to Your Mobile App
**Binding Plaud Devices** is the process of linking your users' Plaud device to your mobile application. The reason we use the term **"bind"** is because devices can only be linked to one application at a time.
When your application **binds to your users' Plaud devices**, your partner/user credentials are used for encrypting, decrypting, and syncing your users' data with your app. This is how your users' data is kept secure and only syncs data with your application (not just any Plaud Embedded application).
```swift Binding to Plaud devices using the Embedded SDK expandable theme={"system"}
// Configure Embedded SDK
private let customDomain = "platform-us.plaud.ai"
func configure(userId: String) {
RecordingStore.shared.userId = userId
PlaudDeviceAgent.shared.initSDK(
userAccessToken: userAccessToken,
customDomain: customDomain
)
}
// Scan for Plaud devices
func startScan() {
cachedBleDevices.removeAll()
scannedDevicesSubject.send([])
connectionStateSubject.send(.scanning)
PlaudDeviceAgent.shared.startScan()
}
// Connect to Plaud devices via Bluetooth
func connect(_ device: ScannedDevice, userId: String) {
guard let bleDevice = cachedBleDevices[device.serialNumber] else { return }
connectionStateSubject.send(.connecting(device))
PlaudDeviceAgent.shared.connectBleDevice(bleDevice: bleDevice, deviceToken: userId)
}
```
### Syncing Files from Plaud Device to Your Mobile App
Plaud devices have their own storage and audio is stored on-device while recording. After a recording is finished, you must sync audio files from your users' Plaud device to their mobile app.
This can be done via Bluetooth Low Energy (BLE) or WiFi Fast Transfer.
WiFi Fast Transfer is **\~10x** faster. It works by opening a hotspot connection between your Plaud device and your users' phone. WiFi Fast Transfer requires the **HotSpot Entitlement** when configuring your iOS app
```swift Syncing files with BLE and WiFi Fast Transfer expandable theme={"system"}
// BLE Sync
PlaudDeviceAgent.shared.exportAudio(
sessionId: file.sessionId,
outputDir: outputDir,
format: .wav,
channels: 1,
callback: callback
)
// WiFi Fast Transfer
PlaudDeviceAgent.shared.setDeviceWiFi(open: true)
// In bleWiFiOpen callback:
PlaudWiFiAgent.shared.bleDevice = BleAgent.shared.bleDevice
PlaudWiFiAgent.shared.connectWifi(ssid, password, 60)
// After wifiHandshake(status: 0):
PlaudWiFiAgent.shared.exportAudioViaWiFi(...)
```
### (Optional) Uploading Files Using the File Upload API
The [Transcription API](/plaud-embedded/transcription-api-overview) accepts any publicly accessible audio URL. You can host your files on your own storage, or use Plaud's File Upload API. If you choose to use Plaud's managed S3 storage, the upload is done in 3 steps:
1. Generating presigned upload URLs to Plaud's S3 storage
2. Uploading your audio files to the presigned upload URLs in chunks
3. Sending the chunk information to complete the multipart upload
```http Generating presigned upload URLs expandable theme={"system"}
POST https://platform-us.plaud.ai/developer/api/open/partner/files/upload/generate-presigned-urls
Content-Type: application/json
Authorization: Bearer
{
"filesize": 10485760,
"filetype": "mp3"
}
// Returns
{
"FileId": "file_xxx",
"UploadId": "upload_xxx",
"ChunkSize": 5242880,
"Parts": [
{ "PartNumber": 1, "PresignedUrl": "https://plaud-bucket.s3.amazonaws.com/..." },
{ "PartNumber": 2, "PresignedUrl": "https://plaud-bucket.s3.amazonaws.com/..." }
]
}
```
```http Uploading chunks to presigned storage URLs theme={"system"}
PUT [PresignedUrl]
[raw bytes of chunk]
// Returns
ETag: "abc123..."
```
```http Completing upload with the part mappings expandable theme={"system"}
POST https://platform-us.plaud.ai/developer/api/open/partner/files/upload/complete-upload
Content-Type: application/json
Authorization: Bearer
{
"file_id": "file_xxx",
"upload_id": "upload_xxx",
"part_list": [
{ "PartNumber": 1, "ETag": "\"abc123...\"" },
{ "PartNumber": 2, "ETag": "\"def456...\"" }
],
"filetype": "mp3",
"file_md5": "9e107d9d372bb6826bd81d3542a419d6"
}
// Returns
{
"FileId": "file_xxx",
"FileType": "mp3",
"DownloadUrl": "https://plaud-bucket.s3.amazonaws.com/...",
"FileMd5": "9e107d9d372bb6826bd81d3542a419d6"
}
```
### Triggering and Pulling Transcriptions from the Transcriptions API
The Transcription API allows you to transcribe any file uploaded to Plaud's storage in an asynchronous way.
* **Trigger a transcription task** - behind the scenes, Plaud is running your uploaded audio file through noise reduction, speaker detection, language recognition, and other speech-to-text pipeline steps
* **Poll transcription status** - use the Transcription API to check the status of a task, and on the `SUCCESS` status, the transcription data will be available.
```http Submit a transcription task expandable theme={"system"}
POST https://platform-us.plaud.ai/developer/api/open/partner/ai/transcriptions/
Content-Type: application/json
X-Client-Api-Key: [API_KEY]
X-Client-Id: [CLIENT_ID]
{
"file_url": "",
"params": {
"transcribe": { "language": "auto", "model": "plaud-fast-whisper" },
"vad": { "decode_silence": false },
"diarization": { "enabled": false, "return_embedding": false }
}
}
// Returns
{
"transcription_id": "task_exec_xxx",
"status": "PENDING",
"data": {}
}
```
```http Check and retrieve transcription expandable theme={"system"}
GET https://platform-us.plaud.ai/developer/api/open/partner/ai/transcriptions/[TRANSCRIPTION_ID]
X-Client-Api-Key: [API_KEY]
X-Client-Id: [CLIENT_ID]
// Returns
{
"transcription_id": "task_exec_xxx",
"status": "SUCCESS",
"data": {
"text": "Meeting started at 10am...",
"language": "en",
"duration": 1843,
"segments": [
{
"start": 0,
"end": 4.2,
"text": "Meeting started at 10am.",
"speaker": "Speaker 1"
}
]
}
}
```
That's the general flow for Plaud Embedded apps! You're now ready to get started implementing the Embedded SDK.
For new builders to build on top of our iOS template with the Embedded SDK pre-implemented (Also a great reference)
For developers ready to jump in and implement the Embedded SDK in their existing mobile app
# iOS SDK
Source: https://docs.plaud.ai/plaud-embedded/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 |
```bash theme={"system"}
git clone https://github.com/Plaud-AI/plaud-sdk-public.git
```
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
```
SDK frameworks are compiled for `arm64` (physical devices only). Simulator is not supported.
**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.
***
## 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.
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).
```swift Swift icon="swift" theme={"system"}
import PlaudDeviceBasicSDK
PlaudDeviceAgent.shared.initSDK(
userAccessToken: "user-token",
customDomain: "platform-us.plaud.ai" // domain only, no https://
)
```
User Access Token (JWT), used for device authentication. The handshake token is automatically parsed from the JWT `sub` field.
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).
#### 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)
```
Refreshed User Token (JWT)
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 a device requires an API call to Plaud's cloud services, so you can track device statuses remotely. And a local bind triggered by the Embedded SDK to verify and generate keys on Plaud device.
For the full cloud-side reference, see the [Device Binding APIs](/plaud-embedded/device-binding-api-overview).
Registers the device/owner association in the Plaud registry. Re-binding a device to the same owner is idempotent, so multiple calls will not have 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.
```swift Swift icon="swift" theme={"system"}
// POST https://platform-us.plaud.ai/developer/api/open/partner/sdk/bind
let body: [String: String] = [
"type": snType, // e.g. "notepro" / "notepins"
"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).resume()
```
Device type string derived from the SN prefix (`881` → `notepro`, `882` → `notepins`).
Device serial number.
A `403` response means the device is already bound to **another** account.
Scans for the device, connects, and generates the key-pair on the device itself. The `.startScan()` will call the `bleScanResult` callback defined in the [**PlaudDeviceAgentProtocol**](#plauddeviceagentprotocol), and the bind result is delivered on `bleBind(sn:status:protVersion:timezone:)`.
```swift Swift icon="swift" theme={"system"}
extension DeviceManager: PlaudDeviceAgentProtocol {
func bleScanResult(bleDevices: [BleDevice]) {
guard let match = bleDevices.first(where: { $0.serialNumber == lastSN }) else { return }
PlaudDeviceAgent.shared.connectBleDevice(bleDevice: match, deviceToken: userId)
}
func bleConnectState(state: Int) {
switch state {
case 1: // connected
case 0: // disconnected
case 2, -1, -2: // connection failed
default: break
}
}
func bleBind(sn: String?, status: Int, protVersion: Int, timezone: Int) { /* status == 0 → bound */ }
func blePenState(state: Int, privacy: Int, keyState: Int, uDisk: Int,
findMyToken: Int, hasSndpKey: Int, deviceAccessToken: Int) { /* handshake complete */ }
// ... recording / file-list / battery / storage callbacks
}
PlaudDeviceAgent.shared.delegate = self
PlaudDeviceAgent.shared.startScan()
```
A scanned/connected device
If needed, a unique identifier for that device. There is also a `connectBleDevice(bleDevice:)` overload that omits it.
#### 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, `2` / `-1` / `-2` = connection failed |
| `bleBind(sn:status:protVersion:timezone:)` | Device bound successfully (`status == 0`) |
| `blePenState(state:privacy:keyState:uDisk:findMyToken:hasSndpKey:deviceAccessToken:)` | Secure handshake complete |
| `bleConnectStage(sn: String?, stage: String, detail: String?)` | Observability to the device handshake process |
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.**
***
### 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.
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, // e.g. "notepro" / "notepins"
"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()
```
Device type string derived from the SN prefix (`881` → `notepro`, `882` → `notepins`).
Device serial number.
Clears the pairing/handshake on the device itself. Requires the device to be connected. On success the SDK disconnects and clears the session. The result is delivered on the `bleDepair(_ status: Int)` delegate callback.
```swift Swift icon="swift" theme={"system"}
PlaudDeviceAgent.shared.delegate = self
extension DeviceManager: PlaudDeviceAgentProtocol {
func bleDepair(_ status: Int) {
if status == 0 {
// device unpaired and disconnected
}
}
// ...
}
PlaudDeviceAgent.shared.depair(clear: true)
```
**Pass `true`** to clear all connections. The argument defaults to `false`, so pass it explicitly for the unbind flow.
#### Device Recovery
In certain situations, the cloud and local bind state can go **out-of-sync**, causing the device to lock.
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.
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 cannot override an active binding.
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" expandable theme={"system"}
let url = URL(string: "https://platform-us.plaud.ai/developer/api/open/partner/sdk/binding?type=\(type)&sn=\(sn)")!
var request = URLRequest(url: url)
request.setValue("Bearer \(userAccessToken)", forHTTPHeaderField: "Authorization")
session.dataTask(with: request) { data, response, error in
if error != nil {
completion(nil)
return
}
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
guard status == 200, let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
completion(nil) // an unknown device returns a bare 404 with no error body
return
}
let isBind = json["is_bind"] as? Bool // JSON null → nil (three-state)
let history = (json["bind_history"] as? [String]) ?? []
completion((isBind: isBind, bindHistory: history))
}.resume()
```
Device type string derived from the SN prefix (`881` → `notepro`, `882` → `notepins`).
Device serial number.
**Response:**
`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`.
Previously bound client IDs, newest first.
`bind_history` records one entry **per bind event**
For each previous client ID, `recoveryConnectBleDevice` uses that ID as the handshake token and connects with force-clear. When a client ID matches the one on the device, the handshake is accepted, the stale lock is cleared, and a rescan is initiated.
```swift Swift icon="swift" expandable theme={"system"}
var seen = Set()
let history = Array(bindHistory.filter { seen.insert($0).inserted }.prefix(5))
suppressAutoReconnect = true
recoveryInProgress = true
defer { recoveryInProgress = false; recoveryAttemptResult = nil }
for historicalId in history {
let semaphore = DispatchSemaphore(value: 0)
var unlocked = false
recoveryAttemptResult = { ok in unlocked = ok; semaphore.signal() }
DispatchQueue.main.async {
PlaudDeviceAgent.shared.recoveryConnectBleDevice(bleDevice: bleDevice, historicalUserId: historicalId)
}
_ = semaphore.wait(timeout: .now() + 25) // handshake timeout
recoveryAttemptResult = nil
if !unlocked {
// This ID did not match the firmware lock — try the next one.
DispatchQueue.main.async { PlaudDeviceAgent.shared.disconnect() }
Thread.sleep(forTimeInterval: 1.0)
continue
}
// Matched — wipe the stale bond and wait for the device's confirmation.
let depairSemaphore = DispatchSemaphore(value: 0)
recoveryDepairDone = { depairSemaphore.signal() }
DispatchQueue.main.async { PlaudDeviceAgent.shared.depair(clear: false) }
_ = depairSemaphore.wait(timeout: .now() + 5)
recoveryDepairDone = nil
recoveryInProgress = false
DispatchQueue.main.async { PlaudDeviceAgent.shared.disconnect() }
Thread.sleep(forTimeInterval: 1.5)
// depair changes the MAC, so rescan and match by SN before reconnecting.
let sn = device.serialNumber
let rescanSemaphore = DispatchSemaphore(value: 0)
var freshDevice: BleDevice?
recoveryRescanSN = sn
recoveryRescanHook = { dev in freshDevice = dev; rescanSemaphore.signal() }
DispatchQueue.main.async { PlaudDeviceAgent.shared.startScan() }
let rescanned = rescanSemaphore.wait(timeout: .now() + 20)
recoveryRescanSN = nil
recoveryRescanHook = nil
DispatchQueue.main.async { PlaudDeviceAgent.shared.stopScan() }
guard rescanned == .success, let fresh = freshDevice else {
failRecovery("The device was unlocked but did not reappear — please rescan and connect it.")
return
}
// Reconnect as the current user — this rebinds the device.
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.suppressAutoReconnect = false
let scanned = ScannedDevice(name: fresh.name, serialNumber: sn, rssi: fresh.rssi)
self.connect(scanned, userId: currentUserId)
}
return
}
```
The scanned device, from `bleScanResult`.
One `bind_history` entry to try as the handshake token.
***
### 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.
Plaud devices will record **up to 5 hours**. Recordings longer should be broken up.
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) { }
}
```
`AudioExportCallback` is an `@objc` protocol, so your conformer must be an `NSObject` subclass.
Session ID
Output Directory
`.pcm` (0) | `.mp3` (1) | `.wav` (2) | `.opus` (3). We recommend `.mp3` — it plays everywhere and is accepted directly by the transcription upload API.
Number of audio channels in the exported file (`1` = mono).
See [**AudioExportCallback**](#audioexportcallback) for details.
func onProgress(\_ progress: Int, message: String)
func onComplete(outputPath: String)
func onError(\_ error: String)
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)
#### 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)
```
Session ID
***
### WiFi Fast Transfer
An alternative to a BLE (Bluetooth Low Energy) file transfer, WiFi Fast Transfer is \~10x faster than BLE transfers.
Requires the `Hotspot Configuration` entitlement in your iOS app settings.
`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!
)
}
}
```
The `wholeName` value from `bleWiFiOpen`.
The `wifiPass` value from `bleWiFiOpen`.
Join timeout in seconds. The SDK re-applies the hotspot configuration and retries internally until handshake or timeout.
#### 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()
```
`endWiFiTransfer()` only reaches the device while BLE is up.
Use `PlaudDeviceAgent.shared.isWiFiTransferActive` to check whether a session is currently open.
For lower-level control, you can use `PlaudWiFiAgent.shared.syncFile`.
```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:))
}
}
```
Session ID of the recording
Start offset in bytes. Use `0` to transfer from the beginning, or a `BleFile.offset` to resume.
End offset in bytes. `0` transfers the whole file.
The file's scene, from `BleFile.scenes`. Must match the file's own value or the transfer is rejected; defaults to `1`.
If you encounter the error message: `wifiCommonErr(cmd: 16, status: 0)`, this is an expected behavior and indicates a successful sync.
#### 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 |
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.
***
### Firmware Update (OTA)
The Embedded SDK handles the entire OTA flow: version query → download → MD5 verify → CRC → BLE packet push → device restart → reconnect.
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!
#### 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)")
}
```
Callback function with type PlaudFirmwareCheckResult
Whether a firmware update is available
The device's current firmware version
The latest available firmware version
Numeric version code
Release notes for the update
URL to download the firmware
MD5 checksum for verification
Whether the update is mandatory
#### 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 ?? "")")
}
}
)
```
Callback that reports firmware update progress.
| 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 |
* `phase`: Current phase of the update (`PlaudFirmwarePhase`)
* `percentage`: Progress from 0.0 to 1.0
Callback when the update completes.
Whether the update succeeded
The firmware version after update
Error description if update failed
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 }
)
```
Full path to the locally downloaded firmware file
Target firmware version to update to (e.g., "V1.2.8")
Callback that reports firmware update progress.
* `phase`: Current phase (`downloading` / `installing` / `restarting` / `complete`)
* `percentage`: Progress from 0.0 to 1.0
Callback when the update completes with `success`, `version`, and `errorMessage` fields.
***
## 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.
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.
### 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) {
// ...
}
}
```
`blePenState` is the **only required** member. Every other callback is `@objc optional` — implement only the ones you need.
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, `2` / `-1` / `-2` = connection failed |
| 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:)` | 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 |
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.
### 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) {
// ...
}
}
```
Export progress, `0`–`100`, with a human-readable status message.
Called when decoding finishes; `outputPath` is the full path to the written file.
Called if export fails, with a description of the error.
### 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 |
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.
# iOS Starter App
Source: https://docs.plaud.ai/plaud-embedded/ios-starter-app
Build a branded iOS app that connects to Plaud devices in minutes using the iOS starter app.
This guide walks through the process of building an iOS [Starter App](/plaud-embedded/starter-app-specs) that:
1. Connects to Plaud devices
2. Syncs recordings from Plaud device to mobile phone
3. Uploads recordings from your users' mobile phone to Plaud's file storage
4. Transcribes recordings with the [Transcription API](/plaud-embedded/transcription-api-overview)
## Video Tutorial
## Onboard to the Plaud Developer Platform
Sign in to the [Plaud Developer Portal](https://portal.plaud.ai/) (It's completely free with a generous [Connected Device limit and transcription usage](/plaud-embedded/billing)).
Create an **Embedded SDK Application** to receive your **Client ID** and **Secret Key**.
## Set Up the Starter App
**Try the Plaud Embedded Skill** to have your coding agent help you through your Starter App deployment.
```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.
### Prerequisites
* A Mac running macOS with **Xcode 16.0+** (the SDK is built with Swift 6.0.3)
* **iOS 14.0+** as deployment target
* An Apple ID for code signing onto a physical device. A free Apple ID works for local testing; TestFlight or App Store release (Step 6) requires the paid **Apple Developer Program**
* A **physical iOS device** — the SDK frameworks are `arm64`-only, so iOS Simulator is not supported
* A Plaud device for end-to-end testing
### Clone the starter app and generate the Xcode project
```bash theme={"system"}
git clone https://github.com/plaud-ai/plaud-sdk-public.git
cd plaud-template-app/plaud-template-app/ios
xcodegen generate
```
Install [XcodeGen](https://github.com/yonaskolb/XcodeGen) (brew install xcodegen) to generate an Xcode project from the `project.yml` file
The `xcodegen generate` step is required — `PlaudTemplateApp.xcodeproj` does not exist in source control and is built from `project.yml`.
You can find the GitHub repository for the [starter app here](https://github.com/plaud-ai/plaud-template-app).
### Retrieve a user token
The `USER_ACCESS_TOKEN` is the per-user JWT your backend mints by calling `POST /open/partner/users/access-token`. See the [Authorization API reference](/plaud-embedded/auth-api-overview) for the full exchange flow.
### Configure credentials
Open `PartnerConfig.xcconfig` and set three values:
```bash xcconfig theme={"system"}
# Required for SDK initialization
USER_ACCESS_TOKEN = your-user-access-token
# Required for the Transcription API
PLAUD_CLIENT_ID = your-client-id
PLAUD_API_KEY = your-api-key
```
**For local development**, create `PartnerConfig.local.xcconfig` alongside it with your real values — it's gitignored and overrides the placeholders.
Then open `project.yml` and change `bundleIdPrefix: com.plaud` to your own reverse-DNS prefix (e.g., `com.acme`); Xcode auto-assigns your Development Team on first build.
### (Optional) Apply branding
Four places control the entire visual identity:
#### App Name
There are two changes to make to change your app name:
1. In `project.yml`, add `CFBundleDisplayName` to properties
```yaml project.yml changes theme={"system"}
#...
info:
path: PlaudTemplateApp/Info.plist
properties:
CFBundleDisplayName: YourAppName # [!code ++]
UILaunchScreen:
UIColorName: "systemBackground"
UserAccessToken: $(USER_ACCESS_TOKEN)
```
Re-run `xcodegen generate` after editing
2. Verify that `Info.plist` includes `CFBundleDisplayName`
```html Info.plist changes theme={"system"}
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
YourAppName
CFBundleExecutable
$(EXECUTABLE_NAME)
```
#### App icon
Add your PNG icon to `PlaudTemplateApp/Resources/Assets.xcassets/AppIcon.appiconset/`. Then in the `.../Assets.xcassets/AppIcon.appiconset/Contents.json` file add your icon filename.
```yaml Contents.json theme={"system"}
{
"images" : [
{
"filename" : "your-icon-1024x1024.png", # [!code ++]
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
```
Your icon image must be a `.png` file without a transparent background or alpha.
#### Theme colors
In `PlaudTemplateApp/Common/PlaudTheme.swift`, edit the `UIColor(hex:)` constants. The template ships intentionally monochrome (`#1f1f1f` labels, `#f9f9f9` background)
```swift PlaudTheme.swift changes theme={"system"}
static let backgroundPrimary = UIColor(hex: "#f9f9f9") // [!code --]
static let labelPrimary = UIColor(hex: "#1f1f1f") // [!code --]
static let backgroundPrimary = UIColor(hex: "#E4DDC8") // [!code ++]
static let labelPrimary = UIColor(hex: "#1e293b") // [!code ++]
```
#### Welcome Screen
In `PlaudTemplateApp/UI/Onboarding/WelcomeViewController.swift`, replace `App Name` and the `UIImage(systemName: "square.grid.2x2")` with your app name and icon.
```swift WelcomeViewController.swift changes theme={"system"}
private let logoIcon: UIImageView = {
let iv = UIImageView(image: UIImage(systemName: "square.grid.2x2")) // [!code --]
iv.tintColor = .label//[!code --]
let iv = UIImageView(image: UIImage(named: "your-icon"))// [!code ++]
iv.contentMode = .scaleAspectFit
iv.translatesAutoresizingMaskIntoConstraints = false
return iv
}()
/// App name label (B2B customers replace with their own brand name)
private let appNameLabel: UILabel = {
let l = UILabel()
l.text = "App name"//[!code --]
l.text = "Your App Name!"//[!code ++]
l.font = PlaudTheme.largeTitle()
```
You'll need to create an imageset for your icon asset
## Run & test with a real device
```bash theme={"system"}
open PlaudTemplateApp.xcodeproj # then ⌘R in Xcode
```
Connect a physical iPhone over USB and select it as the run destination. Simulator is not supported — the SDK frameworks are `arm64`-only.
Verify app launches, device pairs, recording syncs, and transcript appears on your iPhone.
**Unbind your Plaud device after testing and before uninstalling the Starter App!**
Plaud devices can only be bound to one application at a time (tied to your Partner Token). You will not be able to bind your Plaud device to another app (or the Plaud App) before unbinding from the Starter App.
Device binding is also affected by the unique app installation. If you are uninstalling the Starter App from your phone, **be sure to unbind your Plaud device before uninstalling.**
## Publishing to Testflight and the App Store
You will need an [Apple Developer Account](https://developer.apple.com/account) to sign your app and publish to the app store (current pricing is \$99/year)
In the `Signing and Capabilities` tab, click on `Automatically Manage Signing`, set your developer team account, and input a **unique** bundle ID
In [Apple App Connect](https://appstoreconnect.apple.com/apps), create an iOS app and tag your bundle ID from Step 2
In the XCode topbar, navigate to `Product > Archive`. After clicking `Distribute` you should see options to publish to App Connect and Testflight
Navigating back to your [Apple App Connect Portal](https://appstoreconnect.apple.com/apps), you can now either:
1. Fill out the necessary information for App Connect
2. Choose your users for Testflight invites
Either distribution method, you should be able to select your archived bundle from Step 4.
For App Connect, the Apple approval process will take some time.
If you're using Testflight, your beta users should download the **Testflight** app in the App Store, accept their invite, and they can start using your app!
# Plaud Embedded Overview
Source: https://docs.plaud.ai/plaud-embedded/overview
Plaud Embedded is part of Plaud's developer platform **for builders who want to integrate their user-facing products with Plaud's first-class recording devices and speech-to-text pipeline**.
Examples of use cases for Plaud Embedded include:
1. A healthtech platform that automates documentation from physician-patient conversations captured with Plaud devices
2. An AI coaching product that uses Plaud devices to capture conversations for agents to use as context
3. A sales platform that captures offline sales meetings with Plaud devices
***
## Why Plaud Embedded?
### 1. Purpose-built Devices for Capturing Audio
Plaud devices were built to capture conversation-based work. Rather than forcing users to record conversation using your phone directly, Plaud devices have:
* **Long-lasting batteries** so your users don't need to drain their phones
* **Higher-quality microphones** that can pick up on conversations feet away
* **64 GB storage with encryption** so audio data can be held locally and shared only when users want
* Light and convenient designs that **don't take away from your users' in-person experiences**
Designed for phone calls and conversation-heavy workflows.
Designed for hands-free, on-the-go use in field work, healthcare, and retail.
Read more about [Plaud devices](/plaud-embedded/devices).
### 2. State-of-the-art Speech-to-Text Pipeline
Plaud's Automatic Speech Recognition (ASR) models were trained to be able to accurately attribute speakers and transcribe audio even with background noises, overlapping speakers, different languages (100+), and distance from mics.
Your app can leverage our ASR models via the [Transcription API](/plaud-embedded/transcription-api-overview)
***
## How it Works
Plaud Embedded's architecture has 4 main components:
1. **Your backend services**
2. **Your mobile app**
3. **Plaud devices**
4. **Plaud's cloud services**
At a high level, the system interactions goes:
**Your backend services** exchanges `client_id` and `client_secret` for a valid user token from **Plaud's cloud services**.
Pass the **user token** to **your mobile app** to connect and bind your users' **Plaud device**
**Each of your users' unique Plaud devices can only be bound to one app**. This is necessary for offline encryption so even if a device is stolen, the data remains inaccessible.
Sync audio files from **Plaud devices** to **your backend** either through BLE (Bluetooth Low Energy) or Wifi connection. Pass the `file_url` to **Plaud's cloud services** to receive the full transcription via Plaud's Transcription API.
If you want a walkthrough of how the SDKs and APIs fit together, read the [How Plaud Embedded Works Guide](/plaud-embedded/how-plaud-embedded-works).
If you're ready to get started, go to the [Plaud Embedded Quickstart](/plaud-embedded/quickstart)!
## Key Definitions
| Term | Definition | More Information |
| :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------- |
| **Binding a device** | **Linking your users' Plaud device** with your mobile application to enable encryption, decryption, and syncing | [Binding with the SDK](/plaud-embedded/how-plaud-embedded-works#binding-plaud-devices-to-your-mobile-app) |
| **Partner Application** | **That's your application!** We think of Plaud Embedded users as our partners as this platform would be nowhere without you. Your application can bind to your users' Plaud devices and use our transcription models to power your product's use cases. | [Onboard to the Plaud Developer Platform](https://portal.plaud.ai/) |
| **Partner Token** | An **application-level** token used to mint user-specific User Tokens. | [Authentication API](/plaud-embedded/auth-api-overview) |
| **User Token** | A **user-specific** token used to bind your users' Plaud devices to your mobile app | [Authentication API](/plaud-embedded/auth-api-overview) |
| **BLE Connection** | Plaud devices pair to your mobile app via Bluetooth Low Energy (BLE). **BLE is also the default way audio files are synced** from your users' Plaud device to your mobile app. | [Connecting to a Plaud device](/plaud-embedded/ios-sdk#connecting-binding-to-a-plaud-device) |
| **WiFi Fast Transfer** | Although BLE is Plaud devices' default way of syncing files from device to your mobile app, WiFi Fast Transfer is an alternative way of syncing files that's **\~10x faster**. | [Embedded SDK method](/plaud-embedded/ios-sdk#wifi-fast-transfer) |
## Frequently asked questions
No. With the SDK, Plaud acts as infrastructure for your product — you pay for transcription usage and the number of connected devices, and you own the product experience and pricing for your app.
Phones are not designed for capturing in-person conversations, nor can they capture phone calls without significant effort. They also drain the phone battery, create an awkward end-user experience, and lack built-in state-of-the-art transcription. Plaud devices and the SDK address all of this.
Yes. The Plaud Embedded SDK integrates directly into your iOS or Android application. Your app handles device pairing, recording controls, and user experience — Plaud provides the SDK, transcription API, and a starter app to build from.
112 languages, with published accuracy benchmarks by language, scenario, and environment.
Free tier accounts have **300 free transcription hours** and **50 free connected devices per client**. See our [billing section](/plaud-embedded/billing) for more information.
# Quickstart
Source: https://docs.plaud.ai/plaud-embedded/quickstart
Plaud Embedded has **two core components** for integrating with Plaud:
iOS & Android SDKs for linking your users' Plaud devices to your mobile app
APIs to upload audio files and transcribe them with Plaud models
To start using these components, you must first onboard to the Plaud Developer Portal to access your client credentials and API keys.
**Try the Plaud Embedded Skill** to have your coding agent guide you through the Plaud Embedded SDK and APIs.
```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.
## Onboard to the Plaud Developer Platform
Sign in to the [Plaud Developer Portal](https://portal.plaud.ai/) (It's completely free with a generous [Connected Device limit and transcription usage](/plaud-embedded/billing)).
Create an **Embedded SDK Application** to receive your **Client ID** and **Client Secret**.
## Create Your API Key
Your **Client ID** and **Client Secret** are used to generate User Tokens and link user devices. Your **API Key** will be used to interact with the [Transcription API](/plaud-embedded/transcription-api-overview) to transcribe audio files, using Plaud's transcription pipeline to provide speech-to-text with noise reduction, language detection, speaker detection, and more.
You can grab your **API Key** from your developer portal under App Settings > API Keys
## Ready to Build
If you want a walkthrough of how the SDKs and APIs fit together, read the [How Plaud Embedded Works Guide](/plaud-embedded/how-plaud-embedded-works).
For new builders to build on top of our iOS template with the Embedded SDK pre-implemented (Also a great reference)
For developers ready to jump in and implement the Embedded SDK in their existing mobile app
# Using React Native
Source: https://docs.plaud.ai/plaud-embedded/react-native
You can use Plaud's iOS and Android SDK in React Native through [Expo Native Modules](https://docs.expo.dev/modules/overview/). Use our PlaudPlugin as a starter template for:
1. Connecting to Plaud Devices
2. Syncing audio files
3. Transcription
For advanced usage of the Embedded SDK methods for use cases like WiFi fast transfers, we recommend adding more methods to this plugin or using the native [Embedded iOS SDK](/plaud-embedded/ios-sdk) or [Embedded Android SDK](/plaud-embedded/android-sdk) directly.
## How it works
Expo is a React Native framework with [Native Modules](https://docs.expo.dev/modules/overview/) for adding more native functionality to your React Native project.
Examples include iOS/Android local storage and bluetooth functionality.
Plaud Embedded's Native module can be easily imported as a typescript interface, brought into your app as an Expo dependency, and runs native code via the [Embedded iOS SDK](/plaud-embedded/ios-sdk) and [Embedded Android SDK](/plaud-embedded/android-sdk).
```
your React Native code
│ import { PlaudSdk, isAvailable } from 'plaud-sdk'
▼
┌─────────────────────────────┐
│ JS layer (src/*.ts) │ requireNativeModule('PlaudSdk'), fully typed,
│ │
└─────────────────────────────┘
│ Expo Modules bridge (AsyncFunction / Events)
┌─────────────────────────────┐
│ Plaud native SDK │
│ (ios/Frameworks/*) │ BLE / Device / WiFi
└─────────────────────────────┘
```
Plaud's React Native Module uses [Expo's bridge](https://docs.expo.dev/modules/overview/) to pass data between the Javascript layer in your React Native project and the native SDK.
## Running the Demo App
The demo app is included in the Plaud Embedded Module as reference for implementing the module in your own app and seeing how the module works.
```bash theme={"system"}
git clone https://github.com/Plaud-AI/embedded-react-native.git
```
```bash theme={"system"}
cd react-native-demo
npm i
brew install cocoapods #if not already installed
cp .env.example .env
```
You can retrieve your environment credentials from the [developer portal](https://portal.plaud.ai/) and generate a token from our [API playground](https://plaud-embedded-playground.vercel.app)
### For iOS
```bash theme={"system"}
npx expo prebuild -p ios
open ios/reactnativedemo.xcworkspace
```
In XCode, make sure to include your Apple developer credentials and certificate.
### For Android
```bash theme={"system"}
npx expo prebuild -p android
npx expo run:android
```
Then **run on a physical device** to test out the demo app with your Plaud devices.
## How to Integrate with your React Native App
Try the Embedded React Native Skill to upload this doc and the codebase context for your agent.
```bash theme={"system"}
npx skills add Plaud-AI/embedded-react-native
```
### Prerequisites
| Tool | Notes |
| -------------------- | ---------------------------------------------------------------------------------- |
| Node.js | v20+ (v24 used here) |
| Xcode | 15.x+, with a physical iPhone + Apple ID |
| CocoaPods | `brew install cocoapods` |
| An Expo-based RN app | Expo SDK 52+ recommended (this repo uses SDK 57) `npx install-expo-modules@latest` |
### Step 1: Clone the Plaud Embedded React Native Repo
```bash theme={"system"}
git clone https://github.com/Plaud-AI/embedded-react-native.git
```
This repo includes:
1. Expo module for the Plaud SDK for basic functionality
2. Example React Native app
3. Skill for implementing the Plaud Embedded module within projects
### Step 2: Copy the Plaud Embedded Module into your App
At the root of your React Native project, copy the `modules/plaud-sdk` into your project modules.
```bash theme={"system"}
cp -R modules/plaud-sdk your-app/modules/plaud-sdk
```
> Expo will automatically pick up the module and import it
If you are using typescript, add the `plaud-sdk` module to your `tsconfig.json`
```json tsconfig.json theme={"system"}
{
"compilerOptions": {
"paths": {
"plaud-sdk": ["./modules/plaud-sdk"]
}
}
}
```
### Step 3: Add BLE Permissions
**For iOS apps**, in your `app.json` file in your project root, add BLE permissions for the Plaud SDK to leverage iOS's native bluetooth functionality.
```jsonc theme={"system"}
{
"expo": {
"ios": {
"infoPlist": {
"NSBluetoothAlwaysUsageDescription": "Plaud uses Bluetooth to connect to your recorder and sync recordings.",
"UIBackgroundModes": ["bluetooth-central"]
}
}
}
}
```
**For Android apps**, the packaged manifest includes the necessary permissions for Android versions \<12. For Android 12+, permissions are requested at runtime.
Then run `expo prebuild` to build your project.
```bash theme={"system"}
npx expo prebuild -p ios
npx expo prebuild -p android
```
### Step 4: Use the Plaud SDK from React Native
You can now import the PlaudSdk typescript interface from the expo module and use the [Embedded iOS SDK](/plaud-embedded/ios-sdk) and [Embedded Android SDK](/plaud-embedded/android-sdk) straight from your React Native project in javascript.
```ts theme={"system"}
import { PlaudSdk } from 'plaud-sdk';
await PlaudSdk.initSDK({
userAccessToken,
customDomain: 'platform-us.plaud.ai',
userId: 'your-app-user-id',
});
const subs = [
PlaudSdk.addListener('scanResult', ({ devices }) => {/* show devices */}),
PlaudSdk.addListener('connectState', ({ connected, failed }) => {
if (connected) PlaudSdk.getFileList(); // ask for recordings once connected
}),
PlaudSdk.addListener('fileList', ({ files }) => {/* show recordings */}),
PlaudSdk.addListener('exportProgress', ({ progress, message }) => {/* progress UI */}),
];
await PlaudSdk.startScan();
await PlaudSdk.connectBleDevice({ uuid: device.uuid });
const { outputPath } = await PlaudSdk.exportAudio({ sessionId, format: 'mp3' });
subs.forEach((s) => s.remove());
```
### Step 5: Run Your App
```bash theme={"system"}
npx expo prebuild -p ios
npx expo run:ios --device
npx expo prebuild -p android
npx expo run:android --device
```
For the full list of relevant SDK methods for interacting with Plaud devices, see our [iOS SDK reference](/plaud-embedded/ios-sdk) and [Android SDK reference](/plaud-embedded/android-sdk).
# Transcription API
Source: https://docs.plaud.ai/plaud-embedded/transcription-api-overview
Transcribe your user's conversation audio with Plaud's ASR and transcription models.
The Transcription API follows a polling model where you:
After uploading your audio to a publicly accessible URL — either via Plaud's [File Upload API](/plaud-embedded/file-api-overview) or your own storage — submit the URL to the Transcription API to kick off a transcription task.
Use the `GET /transcription` endpoint to poll the Transcription API for the task status.
On completion, the payload will include the transcribed data.
## Using the Transcription API
## Prerequisites
1. If you haven't done so, retrieve your `client_id` and `api_key` from the [developer portal](https://portal.plaud.ai/).
The `api_key` is NOT your `client_secret`. Navigate to App Settings > API Keys.
2. Have a publicly available download URL with the audio file you'd like to transcribe.
This `file_url` could be your cloud storage OR served through Plaud's [File Upload API](/plaud-embedded/file-api-overview) if you prefer Plaud to host your files.
Try the [Plaud Embedded API Playground](https://plaud-embedded-playground.vercel.app/) to see every step of the transcription process with your own client credentials.
End-to-end, from **authentication** to **recording audio** to **uploading** to **transcription**.
### Submit File for Transcription
Submit a transcription task for Plaud Embedded's transcription and ASR models to process.
```http theme={"system"}
POST https://platform-us.plaud.ai/developer/api/open/partner/ai/transcriptions/
Content-Type: application/json
X-Client-Api-Key: [API_KEY]
X-Client-Id: [CLIENT_ID]
{
"file_url": "",
"params": {
"transcribe": { "language": "auto", "model": "plaud-fast-whisper" },
"vad": { "decode_silence": false },
"diarization": { "enabled": false, "return_embedding": false }
}
}
```
```json theme={"system"}
{
"transcription_id": "task_exec_xxx",
"status": "PENDING",
"data": {}
}
```
Recordings **exceeding 5 hours** should be broken into chunks and transcribed in parts.
### Poll Transcription Task Until Completion
Check the status of your transcription task. On the `SUCCESS` status code, the transcript data will be included.
```http theme={"system"}
GET https://platform-us.plaud.ai/developer/api/open/partner/ai/transcriptions/[TRANSCRIPTION_ID]
X-Client-Api-Key: [API_KEY]
X-Client-Id: [CLIENT_ID]
```
```json expandable theme={"system"}
{
"transcription_id": "task_exec_xxx",
"status": "SUCCESS",
"data": {
"text": "Meeting started at 10am...",
"language": "en",
"duration": 1843,
"segments": [
{
"start": 0,
"end": 4.2,
"text": "Meeting started at 10am.",
"speaker": "Speaker 1"
}
]
}
}
```
Plaud's cloud services are hosted in the U.S. by default. If you're
interested in multi-region hosting in Japan, Europe, and Singapore,
please [reach out to our sales team](https://dev.plaud.ai/contact).
# Web App to Native App
Source: https://docs.plaud.ai/plaud-embedded/web-app-wrapper
Plaud's iOS and Android SDK can be used through a Capacitor plugin to turn any web app into a native mobile app!
The PlaudPlugin for Capacitor implements the basic methods for:
1. Connecting to Plaud Devices
2. Syncing audio files
3. Transcription
For advanced usage of the Embedded SDK methods for use cases like WiFi fast transfers, we recommend adding more methods to this plugin or using the native [Embedded iOS SDK](/plaud-embedded/ios-sdk) or [Embedded Android SDK](/plaud-embedded/android-sdk) directly
## Video Walkthrough
## How it works
[Capacitor](https://capacitorjs.com/) is a runtime to run web apps on native platforms. It works by wrapping your web app in a native shell, while Capacitor's bridge sends data from your web app to native features like iOS APIs and BLE.
```
┌──────────────────────────────────────────┐
│ Web App (JavaScript) │
│ PlaudSdk │
└──────────────────▲───────────────────────┘
│
Capacitor Bridge
(JavaScript ↔ IPC)
│
┌──────────────────▼───────────────────────┐
│ PlaudPlugin (Swift/Native) │
│ BLE, Files, iOS APIs, Events │
└──────────────────────────────────────────┘
```
Capacitor plugins like the PlaudPlugin still use native swift code (built on top of the [Embedded iOS SDK](/plaud-embedded/ios-sdk)), but these plugins
can be called and listened to using the Capacitor bridge.
## Running the Demo App
The demo app is included in the Plaud Embedded plugin as reference for implementing the plugin in your own app and seeing how everything works.
```bash theme={"system"}
git clone https://github.com/Plaud-AI/embedded-capacitor.git
```
```bash theme={"system"}
cd nextjs-demo
npm i
cp .env.example .env
```
You can retrieve your environment credentials from the [developer portal](https://portal.plaud.ai/).
```bash theme={"system"}
# for ios
npx cap sync ios
npx cap open ios
# for android
npx cap sync android
npx cap open android
```
**For iOS**, make sure to include your Apple developer credentials and certificate in XCode.
Then **run on a physical device** to test out the demo app with your Plaud devices.
## Setting Up Plaud Embedded's Capacitor Plugin
The Plaud Embedded Capacitor Skill has all of the context in these docs so your agent can immediately start wrapping your web app into a native iOS app with Plaud Embedded.
```bash theme={"system"}
npx skills add Plaud-AI/embedded-capacitor
```
### Step 1: Clone our Embedded Capacitor Repo
```bash theme={"system"}
git clone https://github.com/Plaud-AI/embedded-capacitor.git
```
This repo includes:
1. PlaudPlugin for Capacitor runtime
2. Typescript interfaces and utility functions for the PlaudPlugin
3. A sample app with a NextJS application using the Capacitor wrapper to work as a native iOS app
4. Plaud Capacitor Wrapper Skill for agents to implement the Plaud Plugin and the Capacitor runtime wrapper
### Step 2: Setup Capacitor
```bash theme={"system"}
npm i @capacitor/core @capacitor/ios @capacitor-community/bluetooth-le @capacitor/android
npm i -D @capacitor/cli
```
Then initialize Capacitor to setup your Capacitor configs
```bash theme={"system"}
npx cap init
```
Lastly, add ios to your capacitor project and sync your web app
```bash theme={"system"}
//For ios
npx cap add ios
npx cap sync ios
//For android
npx cap add android
npx cap sync android
```
### Step 3: Setup the PlaudPlugin
#### For iOS
Copy the `ios/PlaudPlugin/` framework and paste into the `ios/` directory.
#### For Android
Copy `android/app/libs/plaud-sdk.aar` into your `android/app/libs/` directory.
Then, copy `android/app/src/main/java/ai/plaud/pwademo/PlaudSdkPlugin.java` into your app's
package directory, and **change its `package` declaration** to match your `applicationId`.
#### For iOS
Copy the `ios/App/App/MainViewController.swift` into your `ios/App/App` directory to register the PlaudPlugin.
Your `ios/` directory should look like this:
```
ios/
├── App/
│ ├── App/
│ │ └── MainViewController.swift
└── PlaudPlugin/
├── Package.swift
├── Frameworks/
│ ├── PlaudBleSDK.xcframework
│ ├── PlaudDeviceBasicSDK.xcframework
│ └── PlaudWiFiSDK.xcframework
└── Sources/
└── PlaudPlugin/
└── PlaudSdkPlugin.swift
```
#### For Android
In `MainActivity.java` that Capacitor generated, register the PlaudSdkPlugin class.
```java theme={"system"}
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
registerPlugin(PlaudSdkPlugin.class);
super.onCreate(savedInstanceState);
}
}
```
Your `android/` directory should look like this:
```
android/
├── app/
│ ├── libs/
│ │ └── plaud-sdk.aar (copied)
│ ├── build.gradle (edited)
│ └── src/main/java//
│ ├── MainActivity.java (edited)
│ └── PlaudSdkPlugin.java (copied)
└── variables.gradle (edited)
```
#### For iOS
Link `PlaudPlugin` into the App target in Xcode.
Open the project (`npx cap open ios`), then **File -> Add Package Dependencies -> Add Local**,
Select `ios/PlaudPlugin`, and add the `PlaudPlugin` library product to the **App** target (the same way `CapApp-SPM` is already linked).
Then, add the Bluetooth entitlement in `ios/App/App/Info.plist`
```xml theme={"system"}
CFBundleDevelopmentRegion
en
...
NSBluetoothAlwaysUsageDescription
Uses Bluetooth to connect and interact with peripheral BLE devices.
UIBackgroundModes
bluetooth-central
```
#### For Android
Declare all dependencies as the SDK does not come with a `pom.xml` file.
```gradle theme={"system"}
dependencies {
// Stock template says ['*.jar'] — '*.aar' is what picks up libs/plaud-sdk.aar
implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
implementation "com.google.code.gson:gson:$gsonVersion"
implementation "com.google.guava:guava:$guavaVersion"
implementation "org.bouncycastle:bcprov-jdk18on:$bouncyCastleVersion"
implementation "org.java-websocket:Java-WebSocket:$javaWebSocketVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation "com.github.tony19:logback-android:$logbackAndroidVersion"
implementation "com.jakewharton.timber:timber:$timberVersion"
// ...leave the rest of the generated block (capacitor-android, androidx, tests) as-is
}
```
And add the matching versions to `android/variables.gradle`:
```gradle theme={"system"}
ext {
// ...the generated Capacitor/AndroidX versions stay as they are
// Transitive dependencies of libs/plaud-sdk.aar
kotlinVersion = '1.9.25'
coroutinesVersion = '1.8.1'
okhttpVersion = '4.12.0'
retrofitVersion = '2.11.0'
gsonVersion = '2.11.0'
guavaVersion = '33.2.1-android'
bouncyCastleVersion = '1.78.1'
javaWebSocketVersion = '1.5.7'
slf4jVersion = '2.0.13'
logbackAndroidVersion = '3.0.0'
timberVersion = '5.0.1'
}
```
Lastly, point the native shell at your web app's URL. Set this in the root
`capacitor.config.ts` — that's the source of truth.
```typescript theme={"system"}
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'ai.plaud.capacitordemo',
appName: 'Plaud Capacitor Demo',
// Required by Capacitor even when loading a remote URL; its contents are
// ignored at runtime because `server.url` is set below.
webDir: 'public',
server: {
// The native shell loads your deployed site and Capacitor injects the
// native bridge, so the plugin can reach iOS CoreBluetooth.
url: 'https://plaud-capacitor-demo.vercel.app',
cleartext: false,
},
};
export default config;
```
## Start Using Plaud Embedded in Your "Web App"
With Capacitor, your web app can stay a web app **AND be a native iOS app!**
The key is to have mobile specific logic execute when your users are using a native mobile platform:
```typescript theme={"system"}
if (Capacitor.isNativePlatform()){
//Plaud SDK Logic
}
```
Use `plaud-sdk.ts` as a convenient typescript interface for interacting with the native iOS Plaud SDK, and write logic for connecting to devices, exporting audio, and triggering transcriptions directly from your web app.
```typescript theme={"system"}
import { Capacitor, type PluginListenerHandle } from "@capacitor/core";
import {
PlaudSdk,
readExportedFile,
type PlaudScanDevice,
type PlaudFile,
} from "@/lib/plaud-sdk";
const handleConnect = async (d: PlaudScanDevice) => {
setError(null);
if (!ensureNative()) return;
try {
setStatus(`connecting to ${d.name || d.serialNumber}…`);
await PlaudSdk.stopScan();
setScanning(false);
await PlaudSdk.connectBleDevice({ uuid: d.uuid, serialNumber: d.serialNumber });
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
}
};
```
For the full list of relevant SDK methods for interacting with Plaud devices, see our [iOS SDK reference](/plaud-embedded/ios-sdk) and [Android SDK reference](/plaud-embedded/android-sdk).
# Changelog
Source: https://docs.plaud.ai/plaud-mcp-cli/changelog
MCP & CLI — General Availability
Plaud MCP (`@plaud-ai/mcp`) and Plaud CLI (`@plaud-ai/cli`) are now generally available. Individuals can connect their Plaud account to any MCP-compatible AI client or access recordings directly from the terminal.
**MCP** — works with Claude, Codex, Cursor, ChatGPT, VS Code, Windsurf, Zed, and other MCP-compatible clients:
```bash theme={"system"}
npx -y @plaud-ai/mcp@latest install
```
**CLI** — terminal access to recordings, transcripts, and AI summaries:
```bash theme={"system"}
npm install -g @plaud-ai/cli
```
# Plaud CLI
Source: https://docs.plaud.ai/plaud-mcp-cli/cli
Access your Plaud recordings from the terminal — browse, search, read transcripts, download audio, and view AI summaries without opening the app.
## Prerequisites
* Node.js ≥ 20 — [download](https://nodejs.org/)
* A Plaud account
***
## Install
```bash theme={"system"}
npm install -g @plaud-ai/cli
```
***
## Quick start
```bash theme={"system"}
plaud login # sign in via browser
plaud files # browse your recordings
plaud search "Q2" # find a recording by keyword
plaud summary # read the AI summary
plaud transcript # read the full transcript
```
***
## Command reference
### Authentication
```bash theme={"system"}
plaud login # sign in via browser — tokens saved automatically
plaud logout # sign out and revoke authorization
plaud me # show your current account details
```
### Browse
```bash theme={"system"}
plaud files # latest page of recordings
plaud files --page 2 --page-size 50
plaud recent # recordings from the last 7 days
plaud recent --days 30 # recordings from the last 30 days
plaud today # recordings from today only
```
**`plaud files` options**
| Option | Description | Default |
| ----------------- | ----------------------- | ------- |
| `-p, --page` | Page number (1–1000) | `1` |
| `-s, --page-size` | Items per page (10–100) | `20` |
### Search
```bash theme={"system"}
plaud search
plaud search "weekly" --from 2026-04-01 --to 2026-04-30
plaud search "onboarding" --max 10
```
Client-side keyword search (case-insensitive) against recording names. Scans up to 500 most recent recordings.
| Option | Description |
| --------------------- | ------------------------------------------ |
| `--from ` | Start of date range (inclusive) |
| `--to ` | End of date range (inclusive) |
| `--max ` | Maximum results to display (default: `50`) |
### Read a recording
```bash theme={"system"}
plaud file # full metadata and availability
plaud audio # 24-hour download URL for the audio
plaud transcript # timestamped transcript
plaud transcript -o transcript.txt # save to file
plaud summary # AI summary (Markdown)
plaud summary -o summary.md # save to file
```
### Utility
```bash theme={"system"}
plaud version # show installed version and build info
plaud update # check for a newer version and print the upgrade command
```
***
## Data reference
### Fields in `plaud files` / `plaud file`
| Field | Description |
| ------------ | ------------------------------------------------ |
| `id` | Unique recording ID — use this in other commands |
| `name` | Recording name |
| `created_at` | Creation time (ISO 8601) |
| `duration` | Length of the recording |
Additional fields in `plaud file ` only:
| Field | Description |
| --------------- | --------------------------------------- |
| `start_at` | Recording start time (ISO 8601) |
| `serial_number` | Device serial number |
| `audio` | Whether audio is available for download |
| `transcript` | Whether a transcript is available |
| `summary` | Whether an AI summary is available |
### Exit codes
| Code | Meaning |
| ---- | ----------------------------------------- |
| `0` | Success |
| `1` | Invalid arguments or unknown error |
| `2` | Authentication failed — run `plaud login` |
| `3` | Network error — check your connection |
| `4` | Request timed out |
All errors are written to **stderr**, keeping stdout clean for piping and scripting:
```
✖ [AUTH_FAILED] Token invalid or expired. Run `plaud login`.
```
***
## Configuration
Tokens are stored at `~/.plaud/tokens.json` and managed automatically — no manual edits needed.
For advanced use, create an optional config file at `~/.plaud/cli.yaml`:
```yaml theme={"system"}
api_base: "https://platform.plaud.ai/developer/api"
timeout: 30000 # milliseconds
```
Environment variables override both the config file and built-in defaults:
| Variable | Purpose |
| ----------------------------------------- | ------------------------- |
| `PLAUD_API_BASE` | Override the API base URL |
| `PLAUD_CLIENT_ID` / `PLAUD_CLI_CLIENT_ID` | OAuth client ID |
| `PLAUD_CLIENT_SECRET` | OAuth client secret |
| `PLAUD_AUTH_URL` | Authorization endpoint |
| `PLAUD_TOKEN_URL` | Token exchange endpoint |
| `PLAUD_REFRESH_URL` | Token refresh endpoint |
***
## Upgrade
```bash theme={"system"}
npm install -g @plaud-ai/cli@latest
```
Or check first:
```bash theme={"system"}
plaud update
```
***
## Uninstall
```bash theme={"system"}
npm uninstall -g @plaud-ai/cli
rm -rf ~/.plaud
```
***
## Troubleshooting
| Symptom | Fix |
| ---------------------------------------- | ------------------------------------------------------------------------------------- |
| `401` / "Not authenticated" | Run `plaud login` |
| `plaud: command not found` | Reopen your terminal; confirm `npm install -g @plaud-ai/cli` completed without errors |
| Token refresh errors | Delete `~/.plaud/tokens.json` and run `plaud login` again |
| Browser doesn't open during sign-in | Copy the URL printed in the terminal and open it manually |
| `npx` returns `E404` for `@plaud-ai/cli` | Run `npm cache clean --force` and retry |
# Contact Us
Source: https://docs.plaud.ai/plaud-mcp-cli/contact
Get help with Plaud MCP and app integrations.
## Support
For questions about Plaud MCP, CLI, or any Plaud app integrations, email us at [support@plaud.ai](mailto:support@plaud.ai).
# Plaud MCP
Source: https://docs.plaud.ai/plaud-mcp-cli/mcp
Connect your Plaud recordings to any MCP-compatible AI client — Claude, Cursor, ChatGPT, Codex, and more. Search recordings, read transcripts, and generate documents without leaving your AI assistant.
## Supported clients
| Client | Auto-configured | Restart after install |
| --------------------------------- | ----------------- | --------------------------- |
| Claude Desktop | ✓ | ⌘Q + reopen |
| Claude Code | ✓ | Exit + new `claude` session |
| Codex Desktop | ✓ | Quit + reopen |
| Cursor / Windsurf / VS Code / Zed | ✓ | Reload per client UI |
| Claude Web / ChatGPT Web | Interactive guide | No restart needed |
| [Kiro](#other-clients) | ✓ | No restart needed |
***
## Prerequisites
* Node.js ≥ 20 — [download](https://nodejs.org/)
* A Plaud account
***
## Install
Run this once. The installer detects your AI clients, writes the MCP configuration, and opens your browser for sign-in:
```bash theme={"system"}
npx -y @plaud-ai/mcp@latest install
```
When your browser opens, click **Authorize**, then restart the clients listed in the installer output. Plaud tools will be available immediately in your next session.
**Options**
| Flag | What it does |
| ------------ | --------------------------------------------------------------------- |
| `--yes` | Auto-configure all detected local clients without prompts |
| `--no-login` | Skip the browser sign-in step (useful on remote or headless machines) |
**Other MCP clients**
If your client isn't auto-detected, add Plaud manually by pasting this into your client's MCP configuration:
```json theme={"system"}
{
"mcpServers": {
"plaud": {
"command": "npx",
"args": ["-y", "@plaud-ai/mcp@latest"]
}
}
}
```
**HTTP-based clients (Claude Web, ChatGPT Web)**
When connecting via HTTP, your recording data passes through Plaud's MCP server (hosted in the US). Plaud does not store this data after the request completes — it is processed in transit only. Data handling is governed by [Plaud's privacy policy](https://plaud.ai/privacy).
### Claude Web
Go to [claude.ai](https://claude.ai), navigate to the left sidebar and click on Customize -> Connectors.
Click Connectors, search for Plaud Web MCP, and connect
Claude will open the Plaud authorization page in your browser. Sign in to your Plaud account and click **Authorize**. Return to Claude — Plaud tools are now available in your conversation.
### ChatGPT Web
[chatgpt.com](https://chatgpt.com)
On the left sidebar, click on Plugins and search for Plaud
Click Connect and sign in with Plaud
***
### Other Clients
[Kiro Server Directory](https://kiro.dev/docs/mcp/servers/)
## Quick start
Once installed, sign in from your AI client:
> Log me into Plaud
Your browser will open the Plaud authorization page. Click **Authorize** and return to your client — you're signed in.
Then try:
> List my recent recordings
> Summarize Tuesday's standup
> Draft a follow-up email from this meeting's action items
***
## Tools
These tools are available to your AI client once Plaud MCP is connected:
| Tool | Description |
| ------------------ | -------------------------------------------------------------- |
| `login` | Opens your browser for OAuth sign-in |
| `logout` | Signs out and revokes your authorization |
| `get_current_user` | Shows your current account details |
| `list_files` | Lists your recordings, with optional filters |
| `get_file` | Returns full details for a single recording |
| `get_note` | Returns the AI-generated summary, action items, and key topics |
| `get_transcript` | Returns the full transcript with timestamps and speaker labels |
### Filtering recordings with `list_files`
| Parameter | Description |
| -------------------- | ------------------------------------------------ |
| `query` | Case-insensitive keyword match on recording name |
| `date_from` | Start date, `YYYY-MM-DD` |
| `date_to` | End date, `YYYY-MM-DD` |
| `page` / `page_size` | Pagination (ignored when filters are set) |
***
## Data reference
### Fields returned by `list_files` and `get_file`
| Field | Type | Description |
| --------------- | ------ | ------------------------------- |
| `id` | string | Unique recording ID |
| `name` | string | Recording name |
| `created_at` | string | Creation time (ISO 8601) |
| `start_at` | string | Recording start time (ISO 8601) |
| `duration` | number | Duration in milliseconds |
| `serial_number` | string | Device serial number |
### Additional fields returned only by `get_file`
| Field | Type | Description |
| --------------- | ------ | ------------------------------------------------------ |
| `presigned_url` | string | Temporary audio download URL (valid 24 hours) |
| `source_list` | array | Transcript segments with timestamps and speaker labels |
| `note_list` | array | AI-generated notes in Markdown |
***
## Skills
Skills are pre-built instructions that help your AI client handle common Plaud workflows automatically. They load when the installer runs — no extra steps needed.
| Skill | Triggered when you ask things like… |
| ---------------- | ----------------------------------------------------- |
| `plaud-browse` | "list my recordings", "show recent files" |
| `plaud-find` | "find the Weekly Sync", "the meeting from Monday" |
| `plaud-read` | "show the transcript", "summarize this recording" |
| `plaud-digest` | "weekly report", "what meetings did I have this week" |
| `plaud-followup` | "draft a follow-up email", "list the action items" |
| `plaud-export` | "save to Notion", "post to Slack" |
***
## Upgrade
```bash theme={"system"}
npm install -g @plaud-ai/mcp@latest
```
Restart your AI client after upgrading.
For Claude Code specifically:
```bash theme={"system"}
npx -y @plaud-ai/mcp clean-plugin
```
Then reinstall inside Claude Code:
```
/plugin install plaud
```
***
## Uninstall
| Client | Cleanup command |
| -------------- | --------------------------------------------------------------------------- |
| Claude Desktop | `plaud-mcp unsetup` |
| Claude Code | `claude mcp remove plaud --scope user && npx -y @plaud-ai/mcp clean-plugin` |
| Codex Desktop | `plaud-mcp unsetup codex` |
Then remove the package and local data:
```bash theme={"system"}
npm uninstall -g @plaud-ai/mcp
rm -rf ~/.plaud
```
***
## Troubleshooting
| Symptom | Fix |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Plaud tools don't appear after install | Make sure you did a full client restart — closing and reopening the window is not enough. For Claude Code, exit and start a new `claude` session. |
| `401` / "Not authenticated" errors | Ask your AI client: *"log me into Plaud"*. |
| No local clients configured in `--yes` mode | Install a supported local client first, or run without `--yes` to get interactive guidance for web clients. |
| Token refresh errors | Delete `~/.plaud/tokens-mcp.json` and sign in again. |
| Browser doesn't open during sign-in | Copy the URL the installer prints and open it manually. On remote machines, forward port 8199 first: `ssh -L 8199:localhost:8199 user@host`. |
| "Server disconnected" in Claude Desktop | Re-run setup: `plaud-mcp unsetup && plaud-mcp setup`, then restart Claude Desktop. |
| Version didn't update after upgrade | Run `npx -y @plaud-ai/mcp clean-plugin`, reinstall with `/plugin install plaud` in Claude Code, then restart. |
# Plaud Devices
Source: https://docs.plaud.ai/plaud-embedded/devices
Devices designed for conversation-based work
Plaud devices are built for executives, sales teams, clinicians, and anyone who needs their conversation data turned into insights and actions.
On Plaud devices, user data is always kept **private and secure**, meaning:
* Encrypted-at-rest
* Full control over what files are shared and synced
* GDPR, SOC2, and HIPAA compliant practices
Plaud currently has two flagship devices:
Designed for phone calls and conversation-heavy workflows.
Designed for hands-free, on-the-go use in field work, healthcare, retail, and beyond.
Plaud Embedded currently only supports the Plaud Note Pro and Plaud NotePin S.
The Plaud Note and Plaud NotePin are NOT supported under Plaud Embedded.
| Specs | Plaud Note Pro | Plaud NotePin S |
| ---------------------------------- | ----------------------------------- | --------------------------- |
| Battery | 30–50 h (endurance mode) | 20 h |
| Microphones | 4 MEMS, 1 VPU | 2 MEMS |
| Connectivity | Dual-band Wi-Fi + Bluetooth | Dual-band Wi-Fi + Bluetooth |
| Storage | 64G | 64G |
| Mode | Smart dual-mode (calls + in-person) | In-person conversations |
| Max single-file recording duration | 5h | 5h |
Join our [Partner Program](https://plaud-embedded-partner-us.bixgrow.com/) to track your customers' device purchases,
qualify for volume discounts, and participate in revenue-based incentives in the future.
# Plaud Embedded Overview
Source: https://docs.plaud.ai/plaud-embedded/overview
Plaud Embedded is part of Plaud's developer platform **for builders who want to integrate their user-facing products with Plaud's first-class recording devices and speech-to-text pipeline**.
Examples of use cases for Plaud Embedded include:
1. A healthtech platform that automates documentation from physician-patient conversations captured with Plaud devices
2. An AI coaching product that uses Plaud devices to capture conversations for agents to use as context
3. A sales platform that captures offline sales meetings with Plaud devices
***
## Why Plaud Embedded?
### 1. Purpose-built Devices for Capturing Audio
Plaud devices were built to capture conversation-based work. Rather than forcing users to record conversation using your phone directly, Plaud devices have:
* **Long-lasting batteries** so your users don't need to drain their phones
* **Higher-quality microphones** that can pick up on conversations feet away
* **64 GB storage with encryption** so audio data can be held locally and shared only when users want
* Light and convenient designs that **don't take away from your users' in-person experiences**
Designed for phone calls and conversation-heavy workflows.
Designed for hands-free, on-the-go use in field work, healthcare, and retail.
Read more about [Plaud devices](/plaud-embedded/devices).
### 2. State-of-the-art Speech-to-Text Pipeline
Plaud's Automatic Speech Recognition (ASR) models were trained to be able to accurately attribute speakers and transcribe audio even with background noises, overlapping speakers, different languages (100+), and distance from mics.
Your app can leverage our ASR models via the [Transcription API](/plaud-embedded/transcription-api-overview)
***
## How it Works
Plaud Embedded's architecture has 4 main components:
1. **Your backend services**
2. **Your mobile app**
3. **Plaud devices**
4. **Plaud's cloud services**
At a high level, the system interactions goes:
**Your backend services** exchanges `client_id` and `client_secret` for a valid user token from **Plaud's cloud services**.
Pass the **user token** to **your mobile app** to connect and bind your users' **Plaud device**
**Each of your users' unique Plaud devices can only be bound to one app**. This is necessary for offline encryption so even if a device is stolen, the data remains inaccessible.
Sync audio files from **Plaud devices** to **your backend** either through BLE (Bluetooth Low Energy) or Wifi connection. Pass the `file_url` to **Plaud's cloud services** to receive the full transcription via Plaud's Transcription API.
If you want a walkthrough of how the SDKs and APIs fit together, read the [How Plaud Embedded Works Guide](/plaud-embedded/how-plaud-embedded-works).
If you're ready to get started, go to the [Plaud Embedded Quickstart](/plaud-embedded/quickstart)!
## Key Definitions
| Term | Definition | More Information |
| :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------- |
| **Binding a device** | **Linking your users' Plaud device** with your mobile application to enable encryption, decryption, and syncing | [Binding with the SDK](/plaud-embedded/how-plaud-embedded-works#binding-plaud-devices-to-your-mobile-app) |
| **Partner Application** | **That's your application!** We think of Plaud Embedded users as our partners as this platform would be nowhere without you. Your application can bind to your users' Plaud devices and use our transcription models to power your product's use cases. | [Onboard to the Plaud Developer Platform](https://portal.plaud.ai/) |
| **Partner Token** | An **application-level** token used to mint user-specific User Tokens. | [Authentication API](/plaud-embedded/auth-api-overview) |
| **User Token** | A **user-specific** token used to bind your users' Plaud devices to your mobile app | [Authentication API](/plaud-embedded/auth-api-overview) |
| **BLE Connection** | Plaud devices pair to your mobile app via Bluetooth Low Energy (BLE). **BLE is also the default way audio files are synced** from your users' Plaud device to your mobile app. | [Connecting to a Plaud device](/plaud-embedded/ios-sdk#connecting-binding-to-a-plaud-device) |
| **WiFi Fast Transfer** | Although BLE is Plaud devices' default way of syncing files from device to your mobile app, WiFi Fast Transfer is an alternative way of syncing files that's **\~10x faster**. | [Embedded SDK method](/plaud-embedded/ios-sdk#wifi-fast-transfer) |
## Frequently asked questions
No. With the SDK, Plaud acts as infrastructure for your product — you pay for transcription usage and the number of connected devices, and you own the product experience and pricing for your app.
Phones are not designed for capturing in-person conversations, nor can they capture phone calls without significant effort. They also drain the phone battery, create an awkward end-user experience, and lack built-in state-of-the-art transcription. Plaud devices and the SDK address all of this.
Yes. The Plaud Embedded SDK integrates directly into your iOS or Android application. Your app handles device pairing, recording controls, and user experience — Plaud provides the SDK, transcription API, and a starter app to build from.
112 languages, with published accuracy benchmarks by language, scenario, and environment.
Free tier accounts have **300 free transcription hours** and **50 free connected devices per client**. See our [billing section](/plaud-embedded/billing) for more information.
# Quickstart
Source: https://docs.plaud.ai/plaud-embedded/quickstart
Plaud Embedded has **two core components** for integrating with Plaud:
iOS & Android SDKs for linking your users' Plaud devices to your mobile app
APIs to upload audio files and transcribe them with Plaud models
To start using these components, you must first onboard to the Plaud Developer Portal to access your client credentials and API keys.
**Try the Plaud Embedded Skill** to have your coding agent guide you through the Plaud Embedded SDK and APIs.
```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.
## Onboard to the Plaud Developer Platform
Sign in to the [Plaud Developer Portal](https://portal.plaud.ai/) (It's completely free with a generous [Connected Device limit and transcription usage](/plaud-embedded/billing)).
Create an **Embedded SDK Application** to receive your **Client ID** and **Client Secret**.
## Create Your API Key
Your **Client ID** and **Client Secret** are used to generate User Tokens and link user devices. Your **API Key** will be used to interact with the [Transcription API](/plaud-embedded/transcription-api-overview) to transcribe audio files, using Plaud's transcription pipeline to provide speech-to-text with noise reduction, language detection, speaker detection, and more.
You can grab your **API Key** from your developer portal under App Settings > API Keys
## Ready to Build
If you want a walkthrough of how the SDKs and APIs fit together, read the [How Plaud Embedded Works Guide](/plaud-embedded/how-plaud-embedded-works).
For new builders to build on top of our iOS template with the Embedded SDK pre-implemented (Also a great reference)
For developers ready to jump in and implement the Embedded SDK in their existing mobile app