Audio recording, transcription, and speaker identification
All endpoints (except OTP login and shared transcripts) require a Bearer token:
Authorization: Bearer po_...
Get an API key by signing in with your email via the OTP flow below.
{
"email": string, // required
"device": string // optional, default "api" — "ios" | "mac" | "api"
}
202{
"message": "Code sent"
}
429 — Rate limited (1 request per 2 min per email/device)
{
"email": string, // required
"code": string, // required — 6-digit code
"device": string // optional, default "api" — must match the request call
}
200{
"api_key": string, // "po_..." — save this
"created": boolean // true if a new account was created
}
401 — Invalid code or too many attempts (max 5)
404 — No pending code — request a new one
200{
"api_key": string // new key
}
200{
"revoked": integer // number of keys revoked
}
200{
"id": string,
"email": string,
"settings": {
"email_transcripts": boolean | null
},
"created_at": string, // ISO 8601
"active_keys": [
{
"id": string,
"name": string, // e.g. "ios-26-04-02-15-30-00"
"created_at": string
}
]
}
{
"email_transcripts": boolean // optional
}
200{
"email_transcripts": boolean | null
}
multipart/form-dataaudio: file // required — M4A, WAV, MP3, FLAC, OGG
source: string // required — "mac_system" | "mac_mic" | "ios_mic"
recorded_at: string // required — ISO 8601 datetime
timezone: string // required — IANA timezone
idempotency_key: string // required — UUID, unique per user
duration_seconds: float // optional
202{
"recording_id": string,
"status": "queued",
"source": string,
"duration_seconds": float | null,
"timezone": string,
"recorded_at": string,
"created_at": string,
"title": null,
"description": null
}
200 — Idempotent hit (same idempotency_key returns cached result)
413 — File too large (max 500MB)
422 — Invalid source or audio format
application/x-www-form-urlencodedsource: string // required — "mac_system" | "mac_mic" | "ios_mic"
recorded_at: string // required — ISO 8601
timezone: string // required — IANA timezone
idempotency_key: string // required — UUID
201{
"recording_id": string,
"status": "uploading",
... // same shape as upload response
}
multipart/form-dataindex: integer // required — chunk sequence number
file: file // required — audio/mp4
200{}
409 — Recording not in "uploading" status
413 — Chunk too large (max 50MB)
application/x-www-form-urlencodedconfirmed_chunks: integer // required — number of chunks uploaded
duration_seconds: float // required — total duration
200{
"recording_id": string,
"status": "queued",
...
}
cursor: string // optional — pagination cursor from previous response
limit: integer // optional, default 20, max 100
status: string // optional — filter by status
speaker_id: string // optional — filter recordings containing speaker
200{
"recordings": [
{
"id": string,
"status": string,
"source": string,
"duration_seconds": float | null,
"timezone": string,
"recorded_at": string,
"created_at": string,
"title": string | null,
"description": string | null
}
],
"next_cursor": string | null
}
200{
"id": string,
"status": string,
"source": string,
"duration_seconds": float | null,
"timezone": string,
"recorded_at": string,
"created_at": string,
"title": string | null,
"description": string | null,
"transcript": [
{
"speaker_id": string | null,
"speaker_name": string | null,
"start": float, // seconds
"end": float, // seconds
"text": string
}
]
}
format: string // optional, default "json" — "json" | "text"
200 (json){
"transcript": [ ... ] // same shape as above
}
200 (text)[0] Speaker 1: First segment text
[1] Speaker 2: Second segment text
uploading status. Deletes chunks and record.204 — No content409 — Recording not in "uploading" status
204 — No content200[
{
"id": string,
"name": string | null,
"name_is_user_set": boolean,
"sample_count": integer,
"created_at": string
}
]
200{
"id": string,
"name": string | null,
"name_is_user_set": boolean,
"sample_count": integer,
"created_at": string,
"recording_count": integer,
"recording_ids": [string]
}
{
"name": string // required
}
200 — Updated speaker object204 — No content200{
"share_url": string // public URL
}
200{
"share_url": string
}
404 — No active share link
204 — No content200{
"recorded_at": string, // local timezone
"duration_seconds": float | null,
"timezone": string,
"transcript": [
{
"speaker_id": string | null,
"speaker_name": string | null,
"start": float,
"end": float,
"text": string
}
]
}
Receive HTTP callbacks when recordings are processed. One webhook per account. Events: recording.complete, recording.empty, recording.failed.
Payloads are signed with X-PlayNotes-Signature: sha256=<hex> (HMAC-SHA256 of the JSON body using your webhook secret).
{
"url": string // required — https:// URL, no private/loopback
}
201{
"id": string,
"url": string,
"secret": string // only returned on creation — save this
}
200{
"id": string,
"url": string,
"created_at": string
}
// or null if no webhook configured
204 — No content200[
{
"id": string,
"recording_id": string | null,
"event": string,
"status": string, // "pending" | "delivered" | "failed"
"attempts": integer,
"last_attempted_at": string | null
}
]
event: recording.complete
data: {"recording_id": "rec_..."}
event: recording.empty
data: {"recording_id": "rec_..."}
event: recording.failed
data: {"recording_id": "rec_..."}
200{
"balance_cents": integer,
"auto_topup_enabled": boolean,
"auto_topup_threshold_cents": integer | null,
"auto_topup_amount_cents": integer | null
}
{
"amount_cents": integer // required — min 399
}
200{
"checkout_url": string // Stripe Checkout URL — redirect user here
}
{
"enabled": boolean, // required
"threshold_cents": integer, // required if enabled — min 100
"amount_cents": integer // required if enabled — min 399
}
200{
"auto_topup_enabled": boolean,
"auto_topup_threshold_cents": integer | null,
"auto_topup_amount_cents": integer | null
}
cursor: string // optional
limit: integer // optional, default 20, max 100
200{
"transactions": [
{
"id": string,
"amount_cents": integer,
"balance_after_cents": integer,
"type": string, // "purchase" | "usage" | "admin_grant" | "auto_topup"
"recording_id": string | null,
"description": string | null,
"created_at": string
}
],
"next_cursor": string | null
}
uploading — chunked upload in progressqueued — waiting for processingprocessing — transcription and speaker identification in progresscompleted — transcript readyempty — no spoken audio detectedfailed — processing errormac_system — macOS system audio capturemac_mic — macOS microphoneios_mic — iOS microphoneM4A, WAV, MP3, FLAC, OGG. Max 500MB per file, 50MB per chunk.