{
  "openapi": "3.1.0",
  "info": {
    "title": "Loquela Remote API",
    "version": "1",
    "summary": "Private speech-to-text in OpenAI's transcription format.",
    "description": "Send an audio or video file and get its text back, with word timings and speaker labels when you ask. English plus 24 European languages. The API follows OpenAI's /v1/audio/transcriptions format, so OpenAI's SDKs work once the base URL is https://api.getloquela.com/v1. Audio is processed in memory on hardware Praxis Dynamics owns and is never stored or used for training. Guides and error codes: https://docs.getloquela.com/remote",
    "termsOfService": "https://api.getloquela.com/legal/terms",
    "contact": { "name": "Loquela Remote", "url": "https://api.getloquela.com", "email": "info@getloquela.com" }
  },
  "externalDocs": { "description": "Loquela Remote documentation", "url": "https://docs.getloquela.com/remote" },
  "servers": [{ "url": "https://api.getloquela.com/v1" }],
  "security": [{ "apiKey": [] }],
  "paths": {
    "/audio/transcriptions": {
      "post": {
        "operationId": "createTranscription",
        "summary": "Transcribe an audio or video file",
        "description": "Uploads are limited to 100 MB and 3 hours of audio. If a JSON response is not ready within 80 seconds, the status line is sent early, followed by spaces until the result arrives. Guide: https://docs.getloquela.com/remote/transcribe",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": { "$ref": "#/components/schemas/TranscriptionRequest" },
              "encoding": { "timestamp_granularities[]": { "style": "form", "explode": true } }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The transcript in the requested format. With encrypt_to, the JSON formats return an EncryptedTranscript and the text formats return the armored PGP message.",
            "headers": {
              "x-request-id": { "$ref": "#/components/headers/RequestId" },
              "x-loquela-encrypted": { "description": "Present on encrypted results: the fingerprint of the key the result is encrypted to.", "schema": { "type": "string" } }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/Transcription" },
                    { "$ref": "#/components/schemas/TranscriptionVerbose" },
                    { "$ref": "#/components/schemas/TranscriptionDiarized" },
                    { "$ref": "#/components/schemas/EncryptedTranscript" }
                  ]
                }
              },
              "text/plain": { "schema": { "type": "string", "description": "response_format text or srt, or an armored PGP message when encrypt_to is set." } },
              "text/vtt": { "schema": { "type": "string", "description": "response_format vtt." } }
            }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "413": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" },
          "500": { "$ref": "#/components/responses/Error" },
          "503": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/models": {
      "get": {
        "operationId": "listModels",
        "summary": "List models",
        "description": "Lists loquela-remote-beta for a valid key. Useful to check a key without sending audio.",
        "responses": {
          "200": {
            "description": "The model list, in OpenAI's format.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ModelList" } } }
          },
          "401": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/usage": {
      "get": {
        "operationId": "getUsage",
        "summary": "Minutes left",
        "description": "Your plan and the minutes you have left. Costs no minutes. Minutes are rounded to two decimals and dates are in UTC. Guide: https://docs.getloquela.com/remote/usage",
        "responses": {
          "200": {
            "description": "Plan and minutes.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountUsage" } } }
          },
          "401": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/pgp/key": {
      "get": {
        "operationId": "getPgpKeys",
        "summary": "Your account's PGP key and our signing key",
        "description": "One armored block for gpg --import: your account's key (audio encrypted to it can be transcribed only in your account's requests) and Loquela Remote's signing key. Included on Pro, Business and Enterprise. Guide: https://docs.getloquela.com/remote/pgp",
        "responses": {
          "200": {
            "description": "The two public keys, armored.",
            "headers": {
              "x-loquela-fingerprint": { "description": "Your account key's fingerprint.", "schema": { "type": "string" } },
              "x-loquela-signing-fingerprint": { "description": "Our signing key's fingerprint.", "schema": { "type": "string" } }
            },
            "content": { "application/pgp-keys": { "schema": { "type": "string" } } }
          },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key from your account page (https://api.getloquela.com/account), sent as Authorization: Bearer <key>."
      }
    },
    "headers": {
      "RequestId": { "description": "Quote it if you contact us; it is the only thing we can look up, since nothing about the audio is kept.", "schema": { "type": "string" } }
    },
    "responses": {
      "Error": {
        "description": "An error in OpenAI's format, plus doc_url. Program against code. Retry-After is set when we know how long to wait. Every code: https://docs.getloquela.com/remote/errors",
        "headers": {
          "x-request-id": { "$ref": "#/components/headers/RequestId" },
          "Retry-After": { "description": "Seconds to wait before trying again.", "schema": { "type": "integer" } }
        },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "TranscriptionRequest": {
        "type": "object",
        "required": ["file"],
        "properties": {
          "file": {
            "type": "string",
            "format": "binary",
            "description": "WAV, MP3, M4A, AAC, OGG/Opus, WebM, FLAC, CAF or AIFF, or a video file (MP4, MOV, 3GP, MKV, WebM). The type is read from the contents. On Pro and Business the file may be PGP-encrypted to your account's key."
          },
          "model": {
            "type": "string",
            "enum": ["loquela-remote-beta", "default", "whisper-1", "gpt-4o-transcribe", "gpt-4o-mini-transcribe", "gpt-transcribe", "gpt-4o-transcribe-diarize"],
            "description": "Leave it out or use loquela-remote-beta. OpenAI's names are accepted and use the same model; gpt-4o-transcribe-diarize also turns on speaker labels."
          },
          "response_format": {
            "type": "string",
            "enum": ["json", "text", "verbose_json", "srt", "vtt", "diarized_json"],
            "default": "json"
          },
          "language": {
            "type": "string",
            "description": "en (default) or, when other languages are on for your account, a two-letter code (a region such as es-MX is fine) or auto. One language per request.",
            "examples": ["en", "es", "de", "auto"]
          },
          "diarize": { "type": "boolean", "default": false, "description": "Adds speaker labels." },
          "timestamp_granularities[]": {
            "type": "array",
            "items": { "type": "string", "enum": ["word", "segment"] },
            "description": "With verbose_json: word and/or segment."
          },
          "written_numbers": { "type": "boolean", "default": true, "description": "false keeps numbers as the model wrote them, often in words. Applies to English." },
          "encrypt_to": {
            "type": "string",
            "description": "Return the result signed by us and PGP-encrypted to a public key on your account: its fingerprint, its last 16 characters, or default. Not the key itself. Pro, Business and Enterprise."
          },
          "prompt": { "type": "string", "description": "Accepted for OpenAI compatibility and ignored." },
          "temperature": { "type": "number", "description": "Accepted for OpenAI compatibility and ignored." },
          "stream": { "type": "boolean", "description": "Accepted and ignored: the whole result comes back as one response." }
        }
      },
      "TranscriptionUsage": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "const": "duration" },
          "seconds": { "type": "integer", "description": "Audio length charged, in whole seconds." }
        }
      },
      "Transcription": {
        "type": "object",
        "description": "response_format json (the default).",
        "required": ["text"],
        "properties": {
          "text": { "type": "string" },
          "partial": { "type": "boolean", "description": "Present and true when part of the file could not be transcribed; only transcribed audio is charged." },
          "speaker_labels": { "type": "string", "const": "unavailable", "description": "Present when speaker labels were asked for but could not be made; the text is complete." },
          "usage": { "$ref": "#/components/schemas/TranscriptionUsage" }
        }
      },
      "Word": {
        "type": "object",
        "required": ["word", "start", "end"],
        "properties": {
          "word": { "type": "string" },
          "start": { "type": "number", "description": "Seconds." },
          "end": { "type": "number", "description": "Seconds." },
          "speaker": { "type": "string", "description": "With speaker labels on." }
        }
      },
      "Segment": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "seek": { "type": "integer" },
          "start": { "type": "number" },
          "end": { "type": "number" },
          "text": { "type": "string" },
          "speaker": { "type": "string" },
          "tokens": { "type": "array", "items": { "type": "integer" } },
          "temperature": { "type": "number" },
          "avg_logprob": { "type": "number" },
          "compression_ratio": { "type": "number" },
          "no_speech_prob": { "type": "number" }
        }
      },
      "TranscriptionVerbose": {
        "type": "object",
        "description": "response_format verbose_json.",
        "required": ["task", "language", "duration", "text"],
        "properties": {
          "task": { "type": "string", "const": "transcribe" },
          "language": { "type": "string", "description": "The language's name, for example english." },
          "duration": { "type": "number", "description": "Seconds." },
          "text": { "type": "string" },
          "partial": { "type": "boolean" },
          "speaker_labels": { "type": "string", "const": "unavailable" },
          "words": { "type": "array", "items": { "$ref": "#/components/schemas/Word" }, "description": "When timestamp_granularities[] includes word." },
          "segments": { "type": "array", "items": { "$ref": "#/components/schemas/Segment" } },
          "usage": { "$ref": "#/components/schemas/TranscriptionUsage" }
        }
      },
      "DiarizedSegment": {
        "type": "object",
        "required": ["type", "id", "start", "end", "text", "speaker"],
        "properties": {
          "type": { "type": "string", "const": "transcript.text.segment" },
          "id": { "type": "string", "examples": ["seg_0"] },
          "start": { "type": "number" },
          "end": { "type": "number" },
          "text": { "type": "string" },
          "speaker": { "type": "string", "description": "A speaker label, or unknown for speech no speaker turn covers." }
        }
      },
      "TranscriptionDiarized": {
        "type": "object",
        "description": "response_format diarized_json.",
        "required": ["task", "duration", "text", "segments"],
        "properties": {
          "task": { "type": "string", "const": "transcribe" },
          "duration": { "type": "number" },
          "text": { "type": "string" },
          "partial": { "type": "boolean" },
          "speaker_labels": { "type": "string", "const": "unavailable" },
          "segments": { "type": "array", "items": { "$ref": "#/components/schemas/DiarizedSegment" } },
          "usage": { "$ref": "#/components/schemas/TranscriptionUsage" }
        }
      },
      "EncryptedTranscript": {
        "type": "object",
        "description": "A JSON format with encrypt_to: the armored PGP message, signed by us, holds exactly what the request would otherwise have returned.",
        "required": ["text", "encrypted", "format", "recipient", "signed_by"],
        "properties": {
          "text": { "type": "string", "description": "-----BEGIN PGP MESSAGE----- ..." },
          "encrypted": { "type": "boolean", "const": true },
          "format": { "type": "string", "enum": ["json", "verbose_json", "diarized_json"] },
          "recipient": { "type": "string", "description": "Fingerprint of the key it is encrypted to." },
          "signed_by": { "type": "string", "description": "Fingerprint of our signing key." },
          "partial": { "type": "boolean" }
        }
      },
      "ModelList": {
        "type": "object",
        "properties": {
          "object": { "type": "string", "const": "list" },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string", "examples": ["loquela-remote-beta"] },
                "object": { "type": "string", "const": "model" },
                "created": { "type": "integer" },
                "owned_by": { "type": "string", "const": "loquela" }
              }
            }
          }
        }
      },
      "AccountUsage": {
        "type": "object",
        "properties": {
          "plan": { "type": ["string", "null"], "enum": ["starter", "pro", "business", null] },
          "period_end": { "type": ["string", "null"], "description": "When plan minutes next reset (UTC)." },
          "plan_minutes": { "type": "number" },
          "plan_minutes_used": { "type": "number" },
          "prepaid_minutes": { "type": "number", "description": "Bought minutes plus any free minutes." },
          "prepaid_next_expiry": { "type": ["string", "null"], "description": "When the next prepaid minutes expire (UTC)." },
          "available_minutes": { "type": ["number", "null"], "description": "Minutes usable now; null when unlimited is true." },
          "unlimited": { "type": "boolean" }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["message", "type", "code"],
            "properties": {
              "message": { "type": "string", "description": "Written for people: what happened and what to do." },
              "type": { "type": "string", "examples": ["invalid_request_error", "server_error"] },
              "param": { "type": ["string", "null"] },
              "code": { "type": "string", "examples": ["invalid_api_key", "insufficient_quota", "file_too_large"] },
              "doc_url": { "type": "string", "format": "uri" }
            }
          }
        }
      }
    }
  }
}
