{
  "openapi": "3.0.3",
  "info": {
    "title": "VibeVoice Public API",
    "description": "Public REST and WebSocket API for VibeVoice streaming speech-to-text, batch audio file transcription, and OAuth device code authentication.",
    "termsOfService": "https://vibevoice.net/terms",
    "contact": {
      "name": "VibeVoice Support",
      "url": "https://vibevoice.net"
    },
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://vibevoice.net",
      "description": "Production Server (HTTPS)"
    },
    {
      "url": "wss://vibevoice.net",
      "description": "Production Server (WebSocket)"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    },
    {
      "BearerAuth": []
    }
  ],
  "paths": {
    "/api/jobs/submit": {
      "post": {
        "summary": "Submit a file for asynchronous transcription",
        "description": "POST /api/transcribe opens the upload as a WAV and nothing transcodes first, so every other format goes here. The response returns immediately with a job_id; poll GET /api/jobs/{job_id} until status is \"completed\" or \"failed\". Rate limited to 100 submissions per hour.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["file"],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Audio or video. Accepted extensions: .mp3, .m4a, .wav, .flac, .ogg, .opus, .aac, .wma, .amr, .3gp, .mp4, .mov, .mkv, .avi, .webm, .mpeg, .mpg, .wmv"
                  },
                  "include_timestamps": {
                    "type": "string",
                    "enum": ["true", "false"],
                    "default": "false"
                  },
                  "enable_diarization": {
                    "type": "string",
                    "enum": ["true", "false"],
                    "default": "false"
                  },
                  "word_level_timestamps": {
                    "type": "string",
                    "enum": ["true", "false"],
                    "default": "false"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Accepted and queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "job_id": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "string",
                      "enum": ["pending"]
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No file, empty filename, or an extension that is not accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "supported_formats": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Email address not verified",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "Larger than the plan allows",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "max_size_mb": {
                      "type": "integer"
                    },
                    "plan": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Quota exceeded. The body names how far short the account is, so a client can say what is missing rather than only that something is.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "quota_exceeded"
                    },
                    "message": {
                      "type": "string",
                      "example": "This file is 12.4 minutes long. You have 8 minutes left this month."
                    },
                    "used": {
                      "type": "number",
                      "description": "Minutes already used this month."
                    },
                    "limit": {
                      "type": "number",
                      "description": "Minutes the plan grants per month."
                    },
                    "required": {
                      "type": "number",
                      "description": "Minutes this file would need."
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "The job system is unavailable, or the upload could not be stored."
          },
          "503": {
            "description": "Speaker diarization was requested and is unavailable on this deployment. Resubmitting without it succeeds."
          }
        }
      }
    },
    "/api/jobs/{job_id}": {
      "get": {
        "summary": "Read the state and result of a job",
        "description": "Poll this until status is \"completed\", at which point result_text holds the transcript, or \"failed\", at which point error_message says why.",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The job",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "job": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "original_filename": {
                          "type": "string"
                        },
                        "file_format": {
                          "type": "string"
                        },
                        "file_size_bytes": {
                          "type": "integer"
                        },
                        "audio_duration_seconds": {
                          "type": "number",
                          "nullable": true
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "processing",
                            "completed",
                            "failed"
                          ]
                        },
                        "result_text": {
                          "type": "string",
                          "nullable": true
                        },
                        "error_message": {
                          "type": "string",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string"
                        },
                        "started_at": {
                          "type": "string",
                          "nullable": true
                        },
                        "completed_at": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such job for this account",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Job lookup failed."
          }
        }
      }
    },
    "/api/request-demo-session": {
      "post": {
        "summary": "Request Live Demo Session Token",
        "description": "Generates a temporary session token for in-browser live demo transcription.",
        "operationId": "requestDemoSession",
        "security": [],
        "responses": {
          "200": {
            "description": "Demo Session Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "type": "string",
                      "example": "demo_sess_12345"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. The demo session endpoint is capped per caller."
          }
        }
      }
    },
    "/api/stream": {
      "get": {
        "summary": "Streaming WebSocket Endpoint",
        "description": "Opens a WebSocket for streaming speech-to-text. Audio is sent as raw int16 PCM binary frames at 16 kHz mono, and punctuated text is returned in pieces while the speaker is still talking. Authentication is in-band rather than in a header: the first text frame must be {\"api_key\": \"<key>\"}, and the server answers {\"status\": \"authenticated\"} before it accepts audio. A key in the query string is not read. End the stream with the text frame END_STREAM. Pieces arrive as {\"text\": \"...\", \"idx\": n, \"ack_ms\": t}: idx counts pieces from 1, ack_ms is the offset in your audio (ms) before which every word has been delivered. The stream ends with {\"text\": \"\", \"is_final\": true, \"idx\": count}. Optional resume: add \"resumable\": true to the first frame to receive a \"resume\" token; after a dropped connection, reconnect with {\"api_key\", \"resumable\": true, \"resume\": token, \"last_idx\": n} to continue the same stream. The reply carries \"resumed\": true and \"received_ms\" (resend audio from there), or \"replay_only\": true if the stream had already ended (send nothing more), or \"resumed\": false if the session is gone (resend from the last ack_ms). Sessions are held for 30 seconds. Usage is counted by the audio received.",
        "operationId": "streamWebSocket",
        "responses": {
          "101": {
            "description": "Switching Protocols to WebSocket"
          },
          "401": {
            "description": "Unauthorized"
          }
        },
        "security": []
      }
    },
    "/api/transcribe": {
      "post": {
        "summary": "Batch Audio File Transcription",
        "description": "Synchronous transcription of a single file. The upload is opened as a WAV and nothing transcodes first, so this endpoint accepts WAV only — send any other format to POST /api/jobs/submit. The response body is the transcript as plain text, not JSON.",
        "operationId": "transcribeAudio",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["file"],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Audio payload. WAV only (PCM, 1-2 channels)."
                  },
                  "prompt": {
                    "type": "string",
                    "description": "Optional context to bias recognition towards names, jargon or spellings you expect. A hint, not a dictionary: it makes the listed forms more likely and guarantees none of them."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transcript, as plain text.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "Hello world, this is a transcribed speech segment."
                }
              }
            }
          },
          "400": {
            "description": "No file part in the request, or a file whose extension is not one this endpoint accepts."
          },
          "403": {
            "description": "The account exists but its email address has not been verified yet."
          },
          "401": {
            "description": "Unauthorized - Invalid or missing API key."
          },
          "429": {
            "description": "Quota Exceeded or Rate Limited."
          },
          "413": {
            "description": "File exceeds the plan's maximum upload size."
          },
          "411": {
            "description": "Content-Length header missing — it is required."
          },
          "500": {
            "description": "Transcription failed."
          },
          "503": {
            "description": "Temporarily unavailable — the service is restarting, or transcription is disabled on this deployment. Safe to retry; the other error codes are not."
          }
        }
      }
    },
    "/oauth/device/code": {
      "post": {
        "summary": "Issue OAuth Device Code",
        "description": "Initiates the OAuth 2.0 Device Code Flow for desktop clients and CLI tools.",
        "operationId": "issueDeviceCode",
        "security": [],
        "responses": {
          "200": {
            "description": "Device Code Issued Successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "device_code": {
                      "type": "string",
                      "example": "dev_code_abc123"
                    },
                    "user_code": {
                      "type": "string",
                      "example": "ABCD-1234"
                    },
                    "verification_uri": {
                      "type": "string",
                      "example": "https://vibevoice.net/link"
                    },
                    "verification_uri_complete": {
                      "type": "string",
                      "description": "The verification URI with the user code already filled in.",
                      "example": "https://vibevoice.net/link?code=ABCD-1234"
                    },
                    "expires_in": {
                      "type": "integer",
                      "example": 900
                    },
                    "interval": {
                      "type": "integer",
                      "example": 5
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/oauth/device/token": {
      "post": {
        "summary": "Exchange OAuth Device Code for Token",
        "description": "Polls device authorization status and retrieves an API key token upon user approval.",
        "operationId": "exchangeDeviceToken",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["device_code"],
                "properties": {
                  "device_code": {
                    "type": "string",
                    "example": "dev_code_abc123"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Approved. Returns the API key for this device.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "api_key": {
                      "type": "string",
                      "example": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
                    },
                    "user": {
                      "type": "object",
                      "description": "The account the device is now linked to."
                    },
                    "device": {
                      "type": "object",
                      "description": "The device record created by the approval."
                    }
                  }
                }
              }
            }
          },
          "428": {
            "description": "Authorization pending. Keep polling at the interval from /oauth/device/code."
          },
          "400": {
            "description": "The device code has expired or is not known."
          },
          "500": {
            "description": "Token exchange failed."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Your API key. `Authorization: Bearer <key>` is accepted on the same endpoints."
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "The same API key, sent as `Authorization: Bearer <key>`. Accepted on every endpoint below that takes a key; X-API-Key is read as well, so either works. Not used by /api/stream, which authenticates in its first text frame instead."
      }
    }
  }
}
