# SPDX-License-Identifier: CC-BY-SA-4.0 # OpenScribe open API - single source of truth for the device and server REST interfaces. # The device implements the "device" paths on the LAN; the server implements the "server" # paths. Both share the Recording schema. Keep firmware and server in sync with this file. openapi: 3.1.0 info: title: OpenScribe API version: 0.1.0 description: > Open API for the OpenScribe self-hosted AI voice recorder. Two surfaces share this document: the on-device REST API (served by the ESP32-S3 on the LAN) and the self-hosted server API (ingest, transcription, summaries, exports). Everything the owner needs to list, download, export and delete their recordings is here. license: name: CC-BY-SA-4.0 identifier: CC-BY-SA-4.0 servers: - url: http://openscribe.local/api/v1 description: Device on the LAN (mDNS name; also reachable by IP) - url: http://your-server.lan:8000/api/v1 description: Self-hosted server tags: - name: device description: Implemented by the firmware on the device (LAN). - name: server description: Implemented by the self-hosted server. - name: recordings description: Recording listing, download, export, delete (both surfaces where noted). security: - bearerAuth: [] paths: # ---------------- Device surface ---------------- /device: get: tags: [device] summary: Device status responses: "200": description: Current device status content: application/json: schema: { $ref: "#/components/schemas/DeviceStatus" } /device/config: get: tags: [device] summary: Read device config (secrets redacted) responses: "200": description: Config content: application/json: schema: { $ref: "#/components/schemas/DeviceConfig" } put: tags: [device] summary: Update device config (WiFi, upload target, codec) requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/DeviceConfigUpdate" } responses: "200": { description: Updated } "401": { $ref: "#/components/responses/Unauthorized" } /record/start: post: tags: [device] summary: Start a recording responses: "200": description: Recording started content: application/json: schema: { $ref: "#/components/schemas/Recording" } "409": { description: Already recording } /record/stop: post: tags: [device] summary: Stop the current recording responses: "200": description: Recording stopped content: application/json: schema: { $ref: "#/components/schemas/Recording" } "409": { description: Not recording } /ota: post: tags: [device] summary: Upload a firmware image to update the device (OTA) requestBody: required: true content: application/octet-stream: schema: { type: string, format: binary } responses: "202": { description: Update accepted; device will apply and reboot } "401": { $ref: "#/components/responses/Unauthorized" } # ---------------- Shared: recordings ---------------- /recordings: get: tags: [recordings] summary: List recordings description: Implemented by both device (local files) and server (ingested). parameters: - { in: query, name: limit, schema: { type: integer, default: 50 } } - { in: query, name: cursor, schema: { type: string } } - in: query name: sync_state schema: { $ref: "#/components/schemas/SyncState" } responses: "200": description: A page of recordings content: application/json: schema: { $ref: "#/components/schemas/RecordingPage" } /recordings/{id}: parameters: - { in: path, name: id, required: true, schema: { type: string } } get: tags: [recordings] summary: Get recording metadata responses: "200": description: Recording metadata content: application/json: schema: { $ref: "#/components/schemas/Recording" } "404": { $ref: "#/components/responses/NotFound" } delete: tags: [recordings] summary: Delete a recording (and its artefacts) responses: "204": { description: Deleted } "404": { $ref: "#/components/responses/NotFound" } /recordings/{id}/audio: parameters: - { in: path, name: id, required: true, schema: { type: string } } get: tags: [recordings] summary: Download the audio (supports HTTP Range for resumable/partial fetch) responses: "200": description: Full audio content: audio/wav: { schema: { type: string, format: binary } } audio/ogg: { schema: { type: string, format: binary } } "206": { description: Partial content (Range) } "404": { $ref: "#/components/responses/NotFound" } # ---------------- Server surface: AI + exports ---------------- /recordings/{id}/transcript: parameters: - { in: path, name: id, required: true, schema: { type: string } } get: tags: [server] summary: Get the transcript responses: "200": description: Transcript with word/segment timings content: application/json: schema: { $ref: "#/components/schemas/Transcript" } "404": { $ref: "#/components/responses/NotFound" } "409": { description: Not transcribed yet } /recordings/{id}/summary: parameters: - { in: path, name: id, required: true, schema: { type: string } } get: tags: [server] summary: Get the LLM summary (overview, key points, action items) responses: "200": description: Summary content: application/json: schema: { $ref: "#/components/schemas/Summary" } "404": { $ref: "#/components/responses/NotFound" } "409": { description: Not summarised yet } /recordings/{id}/export: parameters: - { in: path, name: id, required: true, schema: { type: string } } - in: query name: format required: true schema: type: string enum: [wav, ogg, txt, srt, vtt, md, json] description: > Export format. audio (wav/ogg), plain text (txt), subtitles (srt/vtt), Markdown summary (md), or the full record as json. get: tags: [server] summary: Export a recording in the requested format responses: "200": description: The exported artefact content: application/octet-stream: { schema: { type: string, format: binary } } "404": { $ref: "#/components/responses/NotFound" } /ingest: post: tags: [server] summary: Ingest a recording (direct upload; also used by the device uploader) description: > Accepts audio + metadata and queues transcription and summarisation. The device normally uploads to object storage and notifies here; direct multipart upload is also supported for the app or manual import. requestBody: required: true content: multipart/form-data: schema: type: object properties: metadata: { $ref: "#/components/schemas/Recording" } audio: { type: string, format: binary } responses: "202": description: Accepted for processing content: application/json: schema: { $ref: "#/components/schemas/Recording" } "401": { $ref: "#/components/responses/Unauthorized" } components: securitySchemes: bearerAuth: type: http scheme: bearer description: > Bearer token. On the device the token is set in config and stored in NVS; on the server it is an API key. Read-only listing may be unauthenticated on a trusted LAN if the owner enables that in config. responses: Unauthorized: description: Missing or invalid token content: application/json: schema: { $ref: "#/components/schemas/Error" } NotFound: description: No such recording content: application/json: schema: { $ref: "#/components/schemas/Error" } schemas: SyncState: type: string enum: [local, uploaded, ingested, transcribed, summarised] Recording: type: object required: [id, started_at, duration_s, sample_rate, channels, codec] properties: id: { type: string, example: "rec_20260703T101500Z_ab12" } device_id: { type: string, example: "openscribe-abc123" } started_at: { type: string, format: date-time } duration_s: { type: number, example: 372.5 } sample_rate: { type: integer, example: 16000 } channels: { type: integer, example: 1 } codec: { type: string, example: "wav_pcm_s16le" } size_bytes: { type: integer, example: 11920000 } sha256: { type: string } source: { type: string, enum: [device, upload], default: device } sync_state: { $ref: "#/components/schemas/SyncState" } transcript_ref: { type: [string, "null"] } summary_ref: { type: [string, "null"] } RecordingPage: type: object properties: items: type: array items: { $ref: "#/components/schemas/Recording" } next_cursor: { type: [string, "null"] } TranscriptSegment: type: object properties: start: { type: number } end: { type: number } text: { type: string } speaker: { type: [string, "null"], description: "Reserved for future diarisation" } Transcript: type: object properties: recording_id: { type: string } language: { type: string, example: "en" } model: { type: string, example: "faster-whisper:base" } text: { type: string } segments: type: array items: { $ref: "#/components/schemas/TranscriptSegment" } Summary: type: object properties: recording_id: { type: string } model: { type: string, example: "ollama:llama3.1" } overview: { type: string } key_points: type: array items: { type: string } action_items: type: array items: { type: string } DeviceStatus: type: object properties: device_id: { type: string } firmware_version: { type: string } state: { type: string, enum: [idle, recording, uploading] } power: { type: string, enum: [battery, charging, powered] } battery_pct: { type: [integer, "null"] } storage_free_bytes: { type: integer } storage_total_bytes: { type: integer } recordings_count: { type: integer } wifi_connected: { type: boolean } ip: { type: [string, "null"] } DeviceConfig: type: object properties: device_id: { type: string } wifi_ssid: { type: [string, "null"] } upload_target: { type: string, enum: [none, s3, webdav], default: none } upload_endpoint: { type: [string, "null"] } upload_bucket_or_path: { type: [string, "null"] } codec: { type: string, enum: [wav_pcm_s16le, adpcm, opus], default: wav_pcm_s16le } sample_rate: { type: integer, default: 16000 } # secrets (wifi_psk, s3 keys) are write-only via DeviceConfigUpdate and never returned DeviceConfigUpdate: allOf: - $ref: "#/components/schemas/DeviceConfig" - type: object properties: wifi_psk: { type: string, writeOnly: true } upload_access_key: { type: string, writeOnly: true } upload_secret_key: { type: string, writeOnly: true } api_token: { type: string, writeOnly: true } Error: type: object properties: error: { type: string } detail: { type: [string, "null"] }