OpenPlaud API reference for all endpoints.
http://localhost:3000/api
Browser endpoints require a valid session cookie set by Better Auth.
Automation endpoints under /api/v1/ also accept API keys:
Authorization: Bearer op_...Keys are created from Settings -> API Keys. The raw key is shown once,
stored as an HMAC-SHA256 hash, and can be revoked at any time. Hashing uses
API_TOKEN_HASH_SECRET when set, otherwise BETTER_AUTH_SECRET; the dedicated
secret is optional and lets operators rotate auth/session secrets independently
from API key hashes. API_TOKEN_HASH_SECRET must be at least as strong as
BETTER_AUTH_SECRET.
Health check endpoint.
Response:
{
"status": "ok",
"timestamp": "2025-01-22T12:00:00.000Z"
}Create a new user account.
Body:
{
"email": "user@example.com",
"password": "securepassword",
"name": "John Doe"
}Sign in to existing account.
Body:
{
"email": "user@example.com",
"password": "securepassword"
}Sign out current user.
Send a one-time verification code to the user's Plaud email. Handles regional redirects automatically — if the account lives on a different regional server, the correct apiBase is returned.
Body:
{
"email": "user@example.com"
}Response:
{
"success": true,
"otpToken": "eyJhbGc...",
"apiBase": "https://api-euc1.plaud.ai"
}Verify the OTP code, obtain a long-lived access token from Plaud, and store the encrypted connection.
Body:
{
"code": "123456",
"otpToken": "eyJhbGc...",
"apiBase": "https://api-euc1.plaud.ai",
"email": "user@example.com"
}Response:
{
"success": true,
"devices": [...]
}Get current Plaud connection status.
Response:
{
"connected": true,
"server": "eu",
"plaudEmail": "user@example.com",
"createdAt": "2025-01-22T12:00:00.000Z",
"updatedAt": "2025-01-22T12:00:00.000Z"
}Disconnect the current Plaud account. Deletes the stored connection and device records; synced recordings are preserved in OpenPlaud storage.
Response:
{
"success": true
}Manually trigger sync of recordings from Plaud device.
Response:
{
"success": true,
"newRecordings": 5,
"updatedRecordings": 2,
"errors": []
}List all recordings for current user.
Query Parameters:
limit(optional): Number of results (default: 50)offset(optional): Pagination offset (default: 0)
Response:
{
"recordings": [
{
"id": "abc123",
"filename": "Meeting Notes",
"duration": 3600000,
"startTime": "2025-01-22T10:00:00.000Z",
"filesize": 15728640,
"deviceSn": "888317426694681884"
}
],
"total": 100
}Get single recording by ID.
Response:
{
"id": "abc123",
"filename": "Meeting Notes",
"duration": 3600000,
"startTime": "2025-01-22T10:00:00.000Z",
"transcription": {...},
"aiEnhancements": {...}
}Stream audio file.
Headers:
Range: Optional byte range (e.g.,bytes=0-1023)
Response:
- Content-Type: audio/mpeg, audio/opus, etc.
- Supports HTTP range requests (206 Partial Content)
Transcribe a recording.
Body:
{
"provider": "openai",
"model": "whisper-1"
}Response:
{
"success": true,
"transcriptionId": "xyz789",
"text": "Transcribed text...",
"detectedLanguage": "en"
}List API keys for the signed-in user. Requires a session cookie; API keys cannot manage API keys.
Create a read-only API key. The raw key field is returned once.
Body:
{
"name": "Hermes Agent",
"expiresAt": "2026-12-31T23:59:59.000Z",
"scopes": ["read"]
}Scopes use string identifiers. v1 supports only "read" today; future scopes
may be added without invalidating existing read keys.
Revoke an API key.
Get user settings.
Response:
{
"autoTranscribe": false,
"emailNotifications": true,
"notificationEmail": "user@example.com",
"syncInterval": 300000,
"defaultPlaybackSpeed": 1.0
}Update user settings.
Body:
{
"autoTranscribe": true,
"emailNotifications": true
}Configure storage provider.
Body:
{
"storageType": "s3",
"s3Config": {
"endpoint": "https://...",
"bucket": "openplaud",
"region": "us-east-1",
"accessKeyId": "...",
"secretAccessKey": "..."
}
}List AI providers.
Response:
{
"providers": [
{
"id": "xyz",
"provider": "openai",
"baseUrl": null,
"defaultModel": "whisper-1",
"isDefaultTranscription": true
}
]
}Add new AI provider.
Body:
{
"provider": "groq",
"apiKey": "gsk_...",
"baseUrl": "https://api.groq.com/openai/v1",
"defaultModel": "whisper-large-v3",
"isDefaultTranscription": true
}Update AI provider.
Delete AI provider.
Send test email to verify SMTP configuration.
Body:
{
"email": "user@example.com"
}All v1 endpoints accept either a browser session cookie or
Authorization: Bearer op_....
List recordings with cursor pagination and incremental filters.
Query Parameters:
cursor: base64url cursor fromnext_cursorlimit: 1-100, default 50created_since: ISO timestampupdated_since: ISO timestamp; includes recording metadata, transcript, summary, and generated-title changeshas_transcription:trueorfalse
Response:
{
"data": [
{
"id": "abc123",
"title": "Meeting Notes",
"created_at": "2026-05-06T12:00:00.000Z",
"updated_at": "2026-05-06T12:05:00.000Z",
"recorded_at": "2026-05-06T11:30:00.000Z",
"duration_ms": 3600000,
"filesize_bytes": 15728640,
"device": {
"serial_number": "888317426694681884",
"name": "Plaud Note",
"model": "Note"
},
"has_transcription": true,
"has_summary": false,
"links": {
"self": "/api/v1/recordings/abc123",
"transcript": "/api/v1/recordings/abc123/transcript",
"audio": "/api/v1/recordings/abc123/audio"
}
}
],
"next_cursor": null,
"has_more": false
}updated_at is the recording resource timestamp for v1 clients. It changes
when the recording metadata changes and when transcript, summary, or generated
title state changes.
Return the stable recording shape plus inline transcript and summary
objects when present.
Return transcript text and provider metadata, or 404 when the recording has
not been transcribed.
Return a 302 redirect to a presigned S3 URL for S3 storage, or stream local
audio with byte-range support for local storage.
Export recordings in various formats.
Query Parameters:
format: json | txt | srt | vtt
Response:
- File download
Create backup of all user data.
Response:
{
"success": true,
"backupUrl": "/backups/user_20250122_120000.zip"
}All errors follow this format:
{
"error": "Error message",
"code": "ERROR_CODE"
}UNAUTHORIZED: Not authenticatedFORBIDDEN: Insufficient permissionsNOT_FOUND: Resource not foundINVALID_INPUT: Validation failedPLAUD_API_ERROR: Plaud API failureTRANSCRIPTION_FAILED: Transcription errorSTORAGE_ERROR: Storage operation failedEMAIL_SEND_FAILED: Email notification failedINTERNAL_ERROR: Server error
Automation endpoints under /api/v1/* are rate limited with shared server-side
fixed windows:
- 1,200 requests per minute per client IP.
- 600 requests per minute per authenticated identity (
op_...key, or user session for browser-authenticated calls).
Rate-limited requests return 429 with Retry-After,
X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset
headers.
Forwarding headers such as X-Forwarded-For are ignored unless
RATE_LIMIT_TRUST_PROXY_HEADERS=true; only enable it behind a trusted reverse
proxy that strips or overwrites client-supplied forwarding headers.
When RATE_LIMIT_TRUST_PROXY_HEADERS=false or unset, OpenPlaud cannot derive a
trusted client IP from the request. The unauthenticated IP limiter therefore
uses one shared "unknown" bucket of 1,200 requests per minute for the whole
instance. Authenticated identity limits still apply per API key/session at 600
requests per minute.
Webhooks are configured from Settings -> Webhooks. Target validation is
controlled by WEBHOOKS_REQUIRE_PUBLIC_TARGETS ?? IS_HOSTED.
When strict public targets are required, endpoint URLs must use HTTPS, must not
include credentials, must use public hostnames/IPs, and are delivered with DNS
pinning to the resolved public addresses. When strict public targets are not
required, self-host instances may use HTTP, private IPs, loopback addresses,
.local names, and Docker service hostnames such as
http://n8n:5678/webhook.
Supported events:
recording.syncedrecording.updatedrecording.deletedtranscription.completedtranscription.failed
OpenPlaud signs each request with HMAC-SHA256:
X-OpenPlaud-Event: transcription.completed
X-OpenPlaud-Delivery: <delivery-id>
X-OpenPlaud-Timestamp: 1778078610
X-OpenPlaud-Signature: t=1778078610,v1=<hex hmac>The signature input is:
<unix_timestamp>.<raw_json_body>
Verify with the endpoint secret returned on creation. Reject old timestamps (five minutes is a reasonable default) and compare signatures in constant time.
Delivery uses an in-process worker started by Next.js instrumentation.ts.
This matches the Docker deployment model. Stateless serverless deployments need
an external process or cron to run deliveries reliably.
Delivery history stores only minimal event metadata. Recording, transcript, and
summary data are hydrated at delivery time. Retries and manual redelivery also
hydrate data at retry/redelivery time, not at original event time. Consumers
should order events by delivered_at and treat data as the latest observed
state at delivery time.
Webhook recording links are absolute API URLs built from APP_URL. Normal v1
API responses keep their documented relative links.
Webhook transcript payloads include a bounded preview instead of the full text:
{
"transcript": {
"preview": "first 500 characters...",
"truncated": true,
"length": 24837,
"language": "en",
"provider": "openai",
"model": "whisper-1",
"created_at": "2026-05-06T12:05:00.000Z"
},
"links": {
"transcript": "https://openplaud.example/api/v1/recordings/abc123/transcript"
}
}recording.deleted is the exception to normal latest-state hydration because
normal v1 recording reads exclude tombstoned rows. Deleted recording payloads
use tombstoned metadata that is still available, include deleted_at, set
transcript and summary to null, and keep absolute resource/API URLs for
identity and follow-up handling.
Retries use exponential backoff: 30 seconds, 2 minutes, 10 minutes, 1 hour,
then 6 hours. After six failed attempts, the delivery is marked dead.
Currently, no official SDK is available. The API is RESTful and can be consumed by any HTTP client.
Example with JavaScript:
// Fetch recordings
const response = await fetch('/api/recordings', {
credentials: 'include' // Include session cookie
});
const data = await response.json();For more details, see the source code in src/app/api/.