{
  "openapi": "3.1.0",
  "info": {
    "title": "Pinch API",
    "description": "Speech translation API. Real-time audio translation via WebSocket and async video dubbing via REST.",
    "version": "1.0.0",
    "contact": {
      "name": "Pinch Support",
      "email": "support@startpinch.com",
      "url": "https://www.startpinch.com/docs"
    }
  },
  "servers": [
    {
      "url": "https://api.startpinch.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Real-time Translation",
      "description": "Create sessions for real-time speech translation via WebSocket (LiveKit)."
    },
    {
      "name": "Video Dubbing",
      "description": "Async video dubbing. Submit a video, get a dubbed version in another language."
    }
  ],
  "paths": {
    "/api/beta1/session": {
      "post": {
        "operationId": "createTranslationSession",
        "summary": "Create a real-time translation session",
        "description": "Creates a new real-time translation session and returns WebSocket connection credentials. Connect to the returned `url` with a LiveKit client SDK using the `token` to start streaming audio and receiving translated audio + transcripts.",
        "tags": ["Real-time Translation"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateSessionRequest"
              },
              "example": {
                "source_language": "en-US",
                "target_language": "es-ES",
                "voice_type": "clone"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionResponse"
                },
                "example": {
                  "url": "wss://pinch-prod-interpreter-jgw70la3.livekit.cloud",
                  "token": "eyJhbGciOiJIUzI1NiJ9...",
                  "room_name": "api-44a70196"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (e.g. unsupported language)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "invalid_language",
                    "message": "Unsupported target language: xx-XX"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/dubbing/upload-url": {
      "post": {
        "operationId": "getUploadUrl",
        "summary": "Get a presigned upload URL",
        "description": "Returns a presigned S3 PUT URL for direct video upload. Use this if your video isn't publicly accessible. After uploading, use the returned `source_url` when creating a dubbing job.",
        "tags": ["Video Dubbing"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UploadUrlRequest"
              },
              "example": {
                "filename": "video.mp4",
                "content_type": "video/mp4"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Upload URL generated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadUrlResponse"
                },
                "example": {
                  "upload_url": "https://s3.amazonaws.com/pinch-dubbing/uploads/abc123?X-Amz-Algorithm=...",
                  "source_url": "https://s3.amazonaws.com/pinch-dubbing/uploads/abc123",
                  "upload_id": "abc123",
                  "max_file_size_bytes": 500000000,
                  "expires_in_sec": 3600
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/dubbing/jobs": {
      "post": {
        "operationId": "createDubbingJob",
        "summary": "Create a dubbing job",
        "description": "Submit a video for dubbing. The video will be processed asynchronously. Poll `GET /api/dubbing/jobs/{id}` to track progress. Typical processing time is 2-5 minutes per minute of input video.",
        "tags": ["Video Dubbing"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateJobRequest"
              },
              "example": {
                "source_url": "https://example.com/video.mp4",
                "target_lang": "es",
                "source_lang": "auto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Job created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateJobResponse"
                },
                "example": {
                  "job_id": "dub_7f3a1b2c",
                  "status": "pending",
                  "source_lang": "auto",
                  "target_lang": "es",
                  "created_at": "2026-03-05T14:30:00Z",
                  "limits": {
                    "max_duration_sec": 600,
                    "max_file_size_bytes": 500000000
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalid_url": {
                    "value": {
                      "error": {
                        "code": "invalid_url",
                        "message": "The provided source URL is not a valid URL"
                      }
                    }
                  },
                  "unsupported_language": {
                    "value": {
                      "error": {
                        "code": "unsupported_language",
                        "message": "Unsupported target language: xx"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Insufficient balance",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "insufficient_balance",
                    "message": "Your account balance is too low to process this video"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "operationId": "listDubbingJobs",
        "summary": "List dubbing jobs",
        "description": "Retrieve a paginated list of your dubbing jobs, ordered by creation date (newest first).",
        "tags": ["Video Dubbing"],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Number of jobs to return (default: 20, max: 100)",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Offset for pagination (default: 0)",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of jobs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListJobsResponse"
                },
                "example": {
                  "jobs": [
                    {
                      "job_id": "dub_7f3a1b2c",
                      "status": "completed",
                      "source_lang": "en",
                      "target_lang": "es",
                      "created_at": "2026-03-05T14:30:00Z",
                      "updated_at": "2026-03-05T14:35:22Z"
                    }
                  ],
                  "total": 5,
                  "limit": 20,
                  "offset": 0
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/dubbing/jobs/{id}": {
      "get": {
        "operationId": "getDubbingJobStatus",
        "summary": "Get job status",
        "description": "Poll this endpoint to track the progress of a dubbing job. When status is `completed`, the `output_url` field contains a presigned download URL (expires after 48 hours).",
        "tags": ["Video Dubbing"],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The dubbing job ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Job status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobStatusResponse"
                },
                "example": {
                  "job_id": "dub_7f3a1b2c",
                  "status": "completed",
                  "source_lang": "auto",
                  "target_lang": "es",
                  "error": null,
                  "progress": {
                    "stage": "completed",
                    "percent": 100
                  },
                  "input_duration_sec": 61.5,
                  "cost_usd": 0.51,
                  "output_url": "https://s3.amazonaws.com/pinch-dubbing/results/dub_7f3a1b2c.mp4?X-Amz-Algorithm=...",
                  "output_expires_at": "2026-03-06T14:30:00Z",
                  "created_at": "2026-03-05T14:30:00Z",
                  "updated_at": "2026-03-05T14:35:22Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Job not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/dubbing/jobs/{id}/result": {
      "get": {
        "operationId": "getDubbingJobResult",
        "summary": "Get download URL",
        "description": "Generates a fresh presigned download URL for a completed dubbing job. Use this if the original `output_url` from the status endpoint has expired.",
        "tags": ["Video Dubbing"],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The dubbing job ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Download URL",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobResultResponse"
                },
                "example": {
                  "job_id": "dub_7f3a1b2c",
                  "download_url": "https://s3.amazonaws.com/pinch-dubbing/results/dub_7f3a1b2c.mp4?X-Amz-Algorithm=...",
                  "expires_at": "2026-03-06T14:30:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Job not found or not yet completed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key from the Pinch Developer Portal (https://portal.startpinch.com/dashboard/developers)"
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Invalid or expired API token",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "unauthorized",
                "message": "Invalid or expired API token"
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests (max 10 per minute)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "Too many requests. Maximum 10 requests per minute."
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Internal server error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "internal_error",
                "message": "An internal error occurred"
              }
            }
          }
        }
      }
    },
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string",
                "description": "Machine-readable error code"
              },
              "message": {
                "type": "string",
                "description": "Human-readable error message"
              }
            }
          }
        }
      },
      "CreateSessionRequest": {
        "type": "object",
        "required": ["source_language", "target_language"],
        "properties": {
          "source_language": {
            "type": "string",
            "description": "Source language code (e.g. \"en-US\"). 50+ languages supported.",
            "example": "en-US"
          },
          "target_language": {
            "type": "string",
            "description": "Target language code (e.g. \"es-ES\"). 50+ languages supported.",
            "example": "es-ES"
          },
          "voice_type": {
            "type": "string",
            "enum": ["clone", "female", "male"],
            "default": "clone",
            "description": "Voice type for translated audio. \"clone\" uses the speaker's voice."
          }
        }
      },
      "SessionResponse": {
        "type": "object",
        "required": ["url", "token", "room_name"],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "WebSocket URL for LiveKit connection"
          },
          "token": {
            "type": "string",
            "description": "JWT token for authenticating to the room"
          },
          "room_name": {
            "type": "string",
            "description": "Unique room identifier (format: api-<id>)"
          }
        }
      },
      "UploadUrlRequest": {
        "type": "object",
        "required": ["filename", "content_type"],
        "properties": {
          "filename": {
            "type": "string",
            "description": "Name of the video file",
            "example": "video.mp4"
          },
          "content_type": {
            "type": "string",
            "description": "MIME type of the file",
            "example": "video/mp4",
            "enum": ["video/mp4", "video/quicktime", "video/x-matroska"]
          }
        }
      },
      "UploadUrlResponse": {
        "type": "object",
        "required": ["upload_url", "source_url", "upload_id", "max_file_size_bytes", "expires_in_sec"],
        "properties": {
          "upload_url": {
            "type": "string",
            "format": "uri",
            "description": "Presigned S3 PUT URL. Upload the video file with a PUT request to this URL."
          },
          "source_url": {
            "type": "string",
            "format": "uri",
            "description": "The URL to use as `source_url` when creating a dubbing job."
          },
          "upload_id": {
            "type": "string",
            "description": "Unique identifier for this upload"
          },
          "max_file_size_bytes": {
            "type": "integer",
            "description": "Maximum allowed file size in bytes (500 MB)"
          },
          "expires_in_sec": {
            "type": "integer",
            "description": "Seconds until the upload URL expires"
          }
        }
      },
      "CreateJobRequest": {
        "type": "object",
        "required": ["source_url", "target_lang"],
        "properties": {
          "source_url": {
            "type": "string",
            "format": "uri",
            "description": "URL of the video to dub. Either a public URL or the `source_url` from the upload endpoint."
          },
          "target_lang": {
            "type": "string",
            "enum": ["en", "es", "fr", "de", "it", "pt", "ru", "ja", "ko", "zh"],
            "description": "Target language code"
          },
          "source_lang": {
            "type": "string",
            "default": "auto",
            "description": "Source language code, or \"auto\" for auto-detection.",
            "enum": ["auto", "en", "es", "fr", "de", "it", "pt", "ru", "ja", "ko", "zh"]
          }
        }
      },
      "CreateJobResponse": {
        "type": "object",
        "required": ["job_id", "status", "source_lang", "target_lang", "created_at"],
        "properties": {
          "job_id": {
            "type": "string",
            "description": "Unique identifier for the dubbing job"
          },
          "status": {
            "type": "string",
            "enum": ["pending"],
            "description": "Initial job status (always \"pending\")"
          },
          "source_lang": {
            "type": "string",
            "description": "Source language (or \"auto\")"
          },
          "target_lang": {
            "type": "string",
            "description": "Target language code"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp"
          },
          "limits": {
            "type": "object",
            "properties": {
              "max_duration_sec": {
                "type": "integer",
                "description": "Maximum input video duration in seconds"
              },
              "max_file_size_bytes": {
                "type": "integer",
                "description": "Maximum file size in bytes"
              }
            }
          }
        }
      },
      "JobStatusResponse": {
        "type": "object",
        "required": ["job_id", "status", "source_lang", "target_lang", "created_at", "updated_at"],
        "properties": {
          "job_id": {
            "type": "string",
            "description": "Unique identifier for the dubbing job"
          },
          "status": {
            "type": "string",
            "enum": ["pending", "downloading", "processing", "uploading", "completed", "failed"],
            "description": "Current job status. Flow: pending → downloading → processing → uploading → completed (or failed)."
          },
          "source_lang": {
            "type": "string",
            "description": "Source language"
          },
          "target_lang": {
            "type": "string",
            "description": "Target language code"
          },
          "error": {
            "type": ["string", "null"],
            "description": "Error message if status is \"failed\". Codes: video_too_long, video_too_large, unsupported_format, download_failed, processing_failed."
          },
          "progress": {
            "type": "object",
            "description": "Progress details during processing",
            "properties": {
              "stage": {
                "type": "string",
                "description": "Current processing stage"
              },
              "stage_name": {
                "type": "string",
                "description": "Human-readable stage name"
              },
              "percent": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100,
                "description": "Completion percentage"
              }
            }
          },
          "input_duration_sec": {
            "type": "number",
            "description": "Duration of the input video in seconds"
          },
          "cost_usd": {
            "type": "number",
            "description": "Cost charged for this job in USD ($0.50/min)"
          },
          "output_url": {
            "type": ["string", "null"],
            "format": "uri",
            "description": "Presigned download URL. Available when status is \"completed\". Expires after 48 hours."
          },
          "output_expires_at": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "Expiry time of the download URL (ISO 8601)"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "JobResultResponse": {
        "type": "object",
        "required": ["job_id", "download_url", "expires_at"],
        "properties": {
          "job_id": {
            "type": "string",
            "description": "The dubbing job identifier"
          },
          "download_url": {
            "type": "string",
            "format": "uri",
            "description": "Fresh presigned download URL"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Expiry time of the download URL (ISO 8601)"
          }
        }
      },
      "JobSummary": {
        "type": "object",
        "properties": {
          "job_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": ["pending", "downloading", "processing", "uploading", "completed", "failed"]
          },
          "source_lang": {
            "type": "string"
          },
          "target_lang": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ListJobsResponse": {
        "type": "object",
        "required": ["jobs", "total", "limit", "offset"],
        "properties": {
          "jobs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobSummary"
            }
          },
          "total": {
            "type": "integer",
            "description": "Total number of jobs"
          },
          "limit": {
            "type": "integer",
            "description": "Limit used in the query"
          },
          "offset": {
            "type": "integer",
            "description": "Offset used in the query"
          }
        }
      }
    }
  }
}
