{
  "openapi": "3.1.0",
  "info": {
    "title": "PlainScribe API",
    "version": "1.0.0",
    "summary": "Asynchronous transcription for uploaded audio/video and YouTube URLs.",
    "description": "Create uploads, start transcription jobs, poll their status, and retrieve JSON, TXT, Markdown, CSV, SRT, or VTT results. Raw uploaded media is deleted after processing and transcript artifacts are retained for up to 30 days.",
    "termsOfService": "https://www.plainscribe.com/terms",
    "license": {
      "name": "PlainScribe Terms of Service",
      "url": "https://www.plainscribe.com/terms"
    },
    "contact": {
      "name": "PlainScribe",
      "url": "https://www.plainscribe.com/contact"
    }
  },
  "servers": [
    {
      "url": "https://www.plainscribe.com/api/v1",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "PlainScribe developer documentation",
    "url": "https://www.plainscribe.com/developers"
  },
  "security": [
    {
      "userApiKey": []
    }
  ],
  "tags": [
    { "name": "Uploads", "description": "Durable direct-upload reservations." },
    { "name": "Transcriptions", "description": "Asynchronous file and YouTube transcription jobs." },
    { "name": "Credits", "description": "Current account credit balance." }
  ],
  "paths": {
    "/uploads": {
      "post": {
        "operationId": "createUpload",
        "summary": "Create a media upload",
        "description": "Returns a signed two-hour PUT URL and signed TUS resumable-upload configuration. The unconsumed upload expires after 24 hours. TUS is recommended above 6MB; clients must use the returned endpoint and headers verbatim.",
        "tags": ["Uploads"],
        "parameters": [
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateUploadRequest" },
              "examples": {
                "mp3": {
                  "value": {
                    "file_name": "interview.mp3",
                    "size_bytes": 18432000,
                    "content_type": "audio/mpeg"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Upload reservation created.",
            "headers": {
              "X-Request-Id": { "$ref": "#/components/headers/RequestId" },
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Upload" },
                "examples": {
                  "created": {
                    "value": {
                      "id": "3e07842c-39d8-4baf-a678-f4bca8678165",
                      "object": "upload",
                      "file_name": "interview.mp3",
                      "size_bytes": 18432000,
                      "content_type": "audio/mpeg",
                      "status": "pending",
                      "expires_at": "2026-08-20T18:00:00.000Z",
                      "upload": {
                        "method": "PUT",
                        "url": "https://project.supabase.co/storage/v1/object/upload/sign/plainscribe/transcriptions/user/api-3e07842c-39d8-4baf-a678-f4bca8678165.mp3?token=SIGNED_TOKEN",
                        "headers": { "Content-Type": "audio/mpeg" },
                        "expires_at": "2026-08-19T20:00:00.000Z"
                      },
                      "resumable_upload": {
                        "recommended": true,
                        "protocol": "tus-1.0",
                        "endpoint": "https://project.storage.supabase.co/storage/v1/upload/resumable/sign",
                        "chunk_size_bytes": 6291456,
                        "headers": {
                          "x-signature": "SIGNED_TOKEN",
                          "Upload-Metadata": "bucketName cGxhaW5zY3JpYmU=,objectName dHJhbnNjcmlwdGlvbnMvdXNlci9hcGktM2UwNzg0MmMtMzlkOC00YmFmLWE2NzgtZjRiY2E4Njc4MTY1Lm1wMw==,contentType YXVkaW8vbXBlZw==,cacheControl MzYwMA=="
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "413": { "$ref": "#/components/responses/PayloadTooLarge" },
          "422": { "$ref": "#/components/responses/Unprocessable" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/transcriptions": {
      "post": {
        "operationId": "createTranscription",
        "summary": "Start a transcription",
        "description": "Starts an asynchronous job from a completed upload or supported YouTube URL. For uploads, PlainScribe verifies object size and reads media duration before reserving credits.",
        "tags": ["Transcriptions"],
        "parameters": [
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateTranscriptionRequest" },
              "examples": {
                "upload": {
                  "value": {
                    "source": { "type": "upload", "upload_id": "3e07842c-39d8-4baf-a678-f4bca8678165" },
                    "mode": "transcription"
                  }
                },
                "youtube": {
                  "value": {
                    "source": { "type": "youtube", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "language": "auto" },
                    "mode": "transcription"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Transcription accepted.",
            "headers": {
              "X-Request-Id": { "$ref": "#/components/headers/RequestId" },
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Transcription" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientCredits" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/Unprocessable" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "$ref": "#/components/responses/ProviderFailure" }
        }
      },
      "get": {
        "operationId": "listTranscriptions",
        "summary": "List transcriptions",
        "tags": ["Transcriptions"],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Number of jobs to return.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
          },
          {
            "name": "before",
            "in": "query",
            "description": "Return jobs created before this ISO 8601 timestamp.",
            "schema": { "type": "string", "format": "date-time" }
          }
        ],
        "responses": {
          "200": {
            "description": "A reverse-chronological page of jobs.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TranscriptionList" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/transcriptions/{id}": {
      "parameters": [
        { "$ref": "#/components/parameters/TranscriptionId" }
      ],
      "get": {
        "operationId": "getTranscription",
        "summary": "Get transcription status",
        "tags": ["Transcriptions"],
        "responses": {
          "200": {
            "description": "Current job status.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Transcription" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "410": { "$ref": "#/components/responses/Gone" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "operationId": "deleteTranscription",
        "summary": "Delete a terminal transcription",
        "description": "Deletes a completed or failed job and its retained artifacts. Processing jobs cannot be deleted.",
        "tags": ["Transcriptions"],
        "responses": {
          "204": { "description": "Deleted." },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/transcriptions/{id}/result": {
      "get": {
        "operationId": "getTranscriptionResult",
        "summary": "Get a transcription result",
        "description": "Returns canonical JSON by default or a raw text/subtitle export when format is supplied. A processing job returns 409 with Retry-After: 5.",
        "tags": ["Transcriptions"],
        "parameters": [
          { "$ref": "#/components/parameters/TranscriptionId" },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["json", "txt", "md", "csv", "srt", "vtt"],
              "default": "json"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Transcript artifact.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/TranscriptResult" } },
              "text/plain": { "schema": { "type": "string" } },
              "text/markdown": { "schema": { "type": "string" } },
              "text/csv": { "schema": { "type": "string" } },
              "application/x-subrip": { "schema": { "type": "string" } },
              "text/vtt": { "schema": { "type": "string" } }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "410": { "$ref": "#/components/responses/Gone" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/credits": {
      "get": {
        "operationId": "getCreditBalance",
        "summary": "Get credit balance",
        "tags": ["Credits"],
        "responses": {
          "200": {
            "description": "Current pay-as-you-go balance.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CreditBalance" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "userApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "PlainScribe user API key",
        "description": "Create a user API key from the PlainScribe profile page. Treat it as a server-side secret and use it only from a trusted server or local agent process; never embed it in browser or mobile code. A valid user key can access every endpoint exposed under API v1; per-endpoint scopes are not required. Browser sessions and organization keys are not accepted."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "description": "A caller-generated key created once per intended POST. If a response is lost or times out, retry with the same key and unchanged request body. Use a new key for a new operation.",
        "schema": { "type": "string", "maxLength": 255 },
        "example": "job-2026-08-18-01HRYF7QW7"
      },
      "TranscriptionId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": { "type": "string", "format": "uuid" }
      }
    },
    "headers": {
      "RequestId": { "description": "PlainScribe request identifier.", "schema": { "type": "string", "format": "uuid" } },
      "RateLimitLimit": { "description": "Requests allowed in the current policy window.", "schema": { "type": "integer" } },
      "RateLimitRemaining": { "description": "Requests remaining in the current policy window.", "schema": { "type": "integer" } },
      "RateLimitReset": { "description": "Unix timestamp when the current window resets.", "schema": { "type": "integer" } }
    },
    "schemas": {
      "SignedPutHeaders": {
        "type": "object",
        "additionalProperties": false,
        "required": ["Content-Type"],
        "properties": {
          "Content-Type": {
            "type": "string",
            "description": "Send this exact media type with the signed PUT request."
          }
        }
      },
      "SignedTusHeaders": {
        "type": "object",
        "additionalProperties": false,
        "required": ["x-signature", "Upload-Metadata"],
        "properties": {
          "x-signature": {
            "type": "string",
            "description": "Time-limited Supabase upload signature. Send it raw, without a Bearer prefix."
          },
          "Upload-Metadata": {
            "type": "string",
            "description": "Base64-encoded TUS metadata. Send this value verbatim, or decode it into a TUS client's metadata option."
          }
        }
      },
      "CreateUploadRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["file_name", "size_bytes"],
        "properties": {
          "file_name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "description": "Supported extensions: aac, aif, aifc, aiff, au, avi, caf, flac, m4a, m4v, mkv, mov, mp3, mp4, mpeg, mpg, oga, ogg, opus, ts, wav, wave, weba, webm.",
            "example": "interview.mp3"
          },
          "size_bytes": { "type": "integer", "minimum": 1, "maximum": 1073741824 },
          "content_type": { "type": "string", "pattern": "^(audio|video)/", "example": "audio/mpeg" }
        }
      },
      "Upload": {
        "type": "object",
        "required": ["id", "object", "file_name", "size_bytes", "content_type", "status", "expires_at", "upload", "resumable_upload"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "object": { "const": "upload" },
          "file_name": { "type": "string" },
          "size_bytes": { "type": "integer" },
          "content_type": { "type": "string" },
          "status": { "const": "pending" },
          "expires_at": { "type": "string", "format": "date-time" },
          "upload": {
            "type": "object",
            "required": ["method", "url", "headers", "expires_at"],
            "properties": {
              "method": { "const": "PUT" },
              "url": { "type": "string", "format": "uri" },
              "headers": { "$ref": "#/components/schemas/SignedPutHeaders" },
              "expires_at": { "type": "string", "format": "date-time" }
            }
          },
          "resumable_upload": {
            "type": "object",
            "required": ["recommended", "protocol", "endpoint", "chunk_size_bytes", "headers"],
            "properties": {
              "recommended": { "type": "boolean" },
              "protocol": { "const": "tus-1.0" },
              "endpoint": {
                "type": "string",
                "format": "uri",
                "pattern": "/storage/v1/upload/resumable/sign$",
                "description": "Supabase's signed TUS endpoint. Use this returned URL verbatim; signed resumable uploads require the /resumable/sign route."
              },
              "chunk_size_bytes": { "type": "integer", "const": 6291456 },
              "headers": { "$ref": "#/components/schemas/SignedTusHeaders" }
            }
          }
        }
      },
      "UploadSource": {
        "type": "object",
        "additionalProperties": false,
        "required": ["type", "upload_id"],
        "properties": {
          "type": { "const": "upload" },
          "upload_id": { "type": "string", "format": "uuid" }
        }
      },
      "YouTubeSource": {
        "type": "object",
        "additionalProperties": false,
        "required": ["type", "url"],
        "properties": {
          "type": { "const": "youtube" },
          "url": { "type": "string", "format": "uri" },
          "language": { "type": "string", "default": "auto", "example": "auto" }
        }
      },
      "CreateTranscriptionRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["source"],
        "properties": {
          "source": { "oneOf": [{ "$ref": "#/components/schemas/UploadSource" }, { "$ref": "#/components/schemas/YouTubeSource" }] },
          "mode": { "type": "string", "enum": ["transcription", "translation"], "default": "transcription" }
        }
      },
      "Transcription": {
        "type": "object",
        "required": ["id", "object", "status", "source", "file_name", "mode", "created_at", "expires_at", "links"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "object": { "const": "transcription" },
          "status": { "type": "string", "enum": ["pending", "processing", "completed", "failed", "cancelled", "expired", "deleted"] },
          "source": { "type": "string", "enum": ["upload", "youtube"] },
          "file_name": { "type": "string" },
          "mode": { "type": "string", "enum": ["transcription", "translation"] },
          "duration_seconds": { "type": ["number", "null"] },
          "credit_cost": { "type": ["number", "null"] },
          "provider": { "type": ["string", "null"] },
          "error": {
            "oneOf": [
              { "type": "null" },
              {
                "type": "object",
                "required": ["code", "message"],
                "properties": {
                  "code": {
                    "type": "string",
                    "enum": ["no_speech", "transcription_timed_out", "transcription_failed"]
                  },
                  "message": { "type": "string" }
                }
              }
            ]
          },
          "created_at": { "type": "string", "format": "date-time" },
          "completed_at": { "type": ["string", "null"], "format": "date-time" },
          "expires_at": { "type": "string", "format": "date-time" },
          "links": {
            "type": "object",
            "required": ["self", "result"],
            "properties": {
              "self": { "type": "string", "format": "uri" },
              "result": { "type": "string", "format": "uri" }
            }
          }
        }
      },
      "TranscriptionList": {
        "type": "object",
        "required": ["object", "data", "has_more", "next_before"],
        "properties": {
          "object": { "const": "list" },
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/Transcription" } },
          "has_more": { "type": "boolean" },
          "next_before": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "TranscriptSegment": {
        "type": "object",
        "required": ["start", "text"],
        "properties": {
          "start": { "type": "number", "minimum": 0 },
          "end": { "type": "number", "minimum": 0 },
          "text": { "type": "string" },
          "speaker": { "type": "integer", "minimum": 0 }
        }
      },
      "TranscriptResult": {
        "type": "object",
        "required": ["schemaVersion", "jobId", "mode", "provider", "segments", "createdAt"],
        "properties": {
          "schemaVersion": { "type": "integer", "const": 1 },
          "jobId": { "type": "string", "format": "uuid" },
          "legacyFileId": { "type": "string" },
          "mode": { "type": "string", "enum": ["transcription", "translation"] },
          "provider": { "type": "string" },
          "targetLanguage": { "type": ["string", "null"] },
          "segments": { "type": "array", "items": { "$ref": "#/components/schemas/TranscriptSegment" } },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "CreditBalance": {
        "type": "object",
        "required": ["object", "balance", "trailing_balance", "estimated_transcription_minutes", "price_per_hour_usd"],
        "properties": {
          "object": { "const": "credit_balance" },
          "balance": { "type": "number", "minimum": 0 },
          "trailing_balance": { "type": "number", "minimum": 0 },
          "estimated_transcription_minutes": { "type": "integer", "minimum": 0 },
          "price_per_hour_usd": { "type": "number", "const": 4 }
        }
      },
      "ApiError": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message", "request_id"],
            "properties": {
              "code": { "type": "string" },
              "message": { "type": "string" },
              "request_id": { "type": "string", "format": "uuid" },
              "details": {}
            }
          }
        }
      }
    },
    "responses": {
      "BadRequest": { "description": "Invalid request.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } },
      "Unauthorized": { "description": "Missing, invalid, expired, or non-user API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } },
      "InsufficientCredits": { "description": "The account does not have enough credits.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } },
      "NotFound": { "description": "Resource not found or not owned by the caller.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } },
      "Conflict": { "description": "The request conflicts with current resource or idempotency state.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } },
      "Gone": { "description": "The resource expired.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } },
      "PayloadTooLarge": { "description": "The uploaded object exceeds 1GB.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } },
      "Unprocessable": { "description": "The media or account cannot be processed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } },
      "RateLimited": { "description": "Rate limit exceeded.", "headers": { "Retry-After": { "description": "Seconds before retrying.", "schema": { "type": "integer" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } },
      "ProviderFailure": { "description": "No transcription provider accepted the job.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } }
    }
  }
}
