{
  "openapi": "3.1.0",
  "info": {
    "title": "API transkrip.online",
    "version": "1.0.0",
    "summary": "API transkrip bahasa Indonesia (speech to text): audio/video ke teks berlabel pembicara.",
    "description": "Dokumentasi lengkap: https://transkrip.online/docs/api\n\nAutentikasi `Authorization: Bearer trk_live_…` (key dibuat di dasbor, menu API). Galat berbentuk `{\"error\":{\"message\",\"type\",\"code\",\"param\"}}`; kendalikan alur dengan `error.code`. Waktu dalam detik unix, uang dalam Rupiah. Harga per menit dibaca dari `GET /balance` (`price_idr_per_minute`)."
  },
  "servers": [
    {
      "url": "https://api.transkrip.online/v1"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Transkrip",
      "description": "API asinkron (utama)."
    },
    {
      "name": "Unggahan",
      "description": "Unggahan bertahap yang bisa dilanjutkan."
    },
    {
      "name": "Akun",
      "description": "Saldo."
    },
    {
      "name": "Kompatibel OpenAI",
      "description": "Endpoint sinkron berbentuk API transkripsi audio OpenAI. Tidak mengirim webhook."
    }
  ],
  "paths": {
    "/transcriptions": {
      "post": {
        "tags": ["Transkrip"],
        "operationId": "createTranscription",
        "summary": "Buat transkrip",
        "description": "Tiga bentuk badan: (1) `multipart/form-data` dengan `file`; (2) badan mentah `audio/*`, `video/*`, atau `application/octet-stream` dengan opsi di query string; (3) JSON `{\"upload_id\"}` setelah unggahan bertahap. Satu kiriman sampai 90 MB; berkas lebih besar (sampai 2 GB) lewat unggahan bertahap `/uploads` lalu JSON `{\"upload_id\"}`. Saldo ditahan sebesar `price_idr`. Parameter query hanya berlaku untuk badan mentah. Webhook dikirim ke `webhook_url` atau URL bawaan key (dicatat saat pekerjaan dibuat).",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255
            },
            "description": "Ulangan dengan kunci sama (per akun) mengembalikan objek yang sama tanpa tagihan kedua."
          },
          {
            "name": "language",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["id", "en", "auto"],
              "default": "id",
              "description": "Bahasa rekaman."
            }
          },
          {
            "name": "num_speakers",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 6,
              "description": "Jumlah pembicara. Kosong = dideteksi otomatis."
            }
          },
          {
            "name": "title",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 200,
              "description": "Judul yang tampil di dasbor."
            }
          },
          {
            "name": "webhook_url",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uri",
              "pattern": "^https://",
              "description": "URL webhook untuk pekerjaan ini (https, alamat publik). Kosong = URL webhook bawaan key."
            }
          },
          {
            "name": "metadata",
            "in": "query",
            "description": "Objek JSON (≤16 kunci, ≤8 KB) sebagai string; dikembalikan apa adanya.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filename",
            "in": "query",
            "description": "Nama berkas. WAJIB untuk `application/octet-stream`; untuk `audio/*` dan `video/*` boleh kosong (diturunkan dari tipe).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["file"],
                "properties": {
                  "file": {
                    "type": "string",
                    "contentMediaType": "application/octet-stream",
                    "description": "Rekaman audio atau video."
                  },
                  "language": {
                    "type": "string",
                    "enum": ["id", "en", "auto"],
                    "default": "id",
                    "description": "Bahasa rekaman."
                  },
                  "num_speakers": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 6,
                    "description": "Jumlah pembicara. Kosong = dideteksi otomatis."
                  },
                  "title": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Judul yang tampil di dasbor."
                  },
                  "webhook_url": {
                    "type": "string",
                    "format": "uri",
                    "pattern": "^https://",
                    "description": "URL webhook untuk pekerjaan ini (https, alamat publik). Kosong = URL webhook bawaan key."
                  },
                  "metadata": {
                    "type": "string",
                    "description": "Objek JSON (≤16 kunci, ≤8 KB) sebagai string; dikembalikan apa adanya."
                  }
                }
              },
              "encoding": {
                "file": {
                  "contentType": "audio/*, video/*, application/octet-stream"
                }
              }
            },
            "audio/*": {
              "schema": {
                "type": "string",
                "contentMediaType": "application/octet-stream"
              }
            },
            "video/*": {
              "schema": {
                "type": "string",
                "contentMediaType": "application/octet-stream"
              }
            },
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "contentMediaType": "application/octet-stream"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateFromUploadRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Pekerjaan diterima (biasanya `queued`).",
            "headers": {
              "Idempotency-Replayed": {
                "description": "`true` bila jawaban ini ulangan dari Idempotency-Key yang sama.",
                "schema": {
                  "type": "string",
                  "enum": ["true"]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Transcription"
                }
              }
            }
          },
          "400": {
            "description": "Permintaan tidak sah. Kode: `validation_failed`, `missing_file`, `invalid_multipart`, `invalid_metadata`, `invalid_idempotency_key`, `missing_filename`, `invalid_webhook_url`, `upload_interrupted`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Saldo atau batas key tidak cukup. Kode: `insufficient_balance`, `key_limit_exceeded`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unggahan tidak ditemukan. Kode: `not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Unggahan belum siap. Kode: `upload_not_ready`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Berkas terlalu besar. Kode: `file_too_large`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Content-Type tidak didukung. Kode: `unsupported_media_type`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Berkas tidak bisa dipakai. Kode: `unsupported_format`, `not_audio`, `too_long`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Terlalu banyak permintaan. Kode: `too_many_jobs`, `too_many_uploads`, `rate_limit_exceeded`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Detik sebelum mencoba lagi.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "Sementara tidak tersedia. Kode: `storage_full`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": ["Transkrip"],
        "operationId": "listTranscriptions",
        "summary": "Daftar transkrip",
        "description": "Terbaru dulu, tanpa teks dan segmen.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "after",
            "in": "query",
            "description": "Kursor: `last_id` halaman sebelumnya.",
            "schema": {
              "type": "string",
              "pattern": "^trn_[0-9a-f]{32}$"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/TranscriptionStatus"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Daftar.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TranscriptionList"
                }
              }
            }
          },
          "400": {
            "description": "Parameter tidak sah. Kode: `validation_failed`, `invalid_cursor`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/transcriptions/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^trn_[0-9a-f]{32}$"
          },
          "description": "Id transkrip."
        }
      ],
      "get": {
        "tags": ["Transkrip"],
        "operationId": "getTranscription",
        "summary": "Ambil status & hasil",
        "parameters": [
          {
            "name": "include",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["segments", "text"],
              "default": "segments"
            },
            "description": "`segments` = teks + segmen; `text` = teks saja."
          }
        ],
        "responses": {
          "200": {
            "description": "Objek transkrip.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Transcription"
                },
                "example": {
                  "id": "trn_0f6c2b9e4d1a47c8b3e5a6f7d8c9b0a1",
                  "object": "transcription",
                  "status": "completed",
                  "created_at": 1792677149,
                  "completed_at": 1792677901,
                  "title": "Wawancara informan 3",
                  "filename": "informan-3.m4a",
                  "language": "id",
                  "duration": 2832.4,
                  "billed_minutes": 48,
                  "price_idr": 2400,
                  "charged_idr": 2400,
                  "refunded": false,
                  "num_speakers": 2,
                  "progress": 1,
                  "queue_position": null,
                  "estimated_completion_at": null,
                  "expires_at": 1795269149,
                  "source": "api",
                  "metadata": {
                    "informan": "I-03"
                  },
                  "speakers": [
                    {
                      "id": "A",
                      "label": "Pewawancara"
                    },
                    {
                      "id": "B",
                      "label": "Narasumber"
                    }
                  ],
                  "text": "Sejak kapan usahanya berjalan? …",
                  "segments": [
                    {
                      "id": 0,
                      "start": 2.1,
                      "end": 9.8,
                      "speaker": "A",
                      "text": "Sejak kapan usahanya berjalan?",
                      "uncertain": false
                    }
                  ],
                  "error": null
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": ["Transkrip"],
        "operationId": "deleteTranscription",
        "summary": "Hapus teks & audio sekarang",
        "description": "Tagihan tetap tercatat.",
        "responses": {
          "200": {
            "description": "Terhapus.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeletedTranscription"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Belum bisa dihapus. Kode: `job_active`, `billing_pending`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/transcriptions/{id}/export": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^trn_[0-9a-f]{32}$"
          },
          "description": "Id transkrip."
        }
      ],
      "get": {
        "tags": ["Transkrip"],
        "operationId": "exportTranscription",
        "summary": "Unduh berkas jadi",
        "description": "Sama persis dengan tombol Unduh di web.",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": ["txt", "srt", "vtt", "docx"]
            }
          },
          {
            "name": "waktu",
            "in": "query",
            "description": "Hanya untuk `txt`: 1 = dengan stempel waktu, 0 = tanpa.",
            "schema": {
              "type": "string",
              "enum": ["0", "1"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Berkas.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              },
              "application/x-subrip": {
                "schema": {
                  "type": "string"
                }
              },
              "text/vtt": {
                "schema": {
                  "type": "string"
                }
              },
              "application/vnd.openxmlformats-officedocument.wordprocessingml.document": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
                }
              }
            }
          },
          "400": {
            "description": "Parameter tidak sah. Kode: `validation_failed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Transkrip belum selesai. Kode: `not_ready`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/transcriptions/estimate": {
      "post": {
        "tags": ["Transkrip"],
        "operationId": "estimateTranscription",
        "summary": "Hitung biaya tanpa memotong saldo",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["upload_id"],
                "properties": {
                  "upload_id": {
                    "type": "string",
                    "pattern": "^upl_[0-9a-f]{32}$"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Perkiraan biaya.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Estimate"
                }
              }
            }
          },
          "400": {
            "description": "Permintaan tidak sah. Kode: `validation_failed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Unggahan belum siap. Kode: `upload_not_ready`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/balance": {
      "get": {
        "tags": ["Akun"],
        "operationId": "getBalance",
        "summary": "Saldo & tarif",
        "responses": {
          "200": {
            "description": "Saldo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Balance"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/uploads": {
      "post": {
        "tags": ["Unggahan"],
        "operationId": "createUpload",
        "summary": "Mulai unggahan bertahap",
        "description": "Unggahan yang tidak dipakai kedaluwarsa setelah 24 jam.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["filename", "bytes"],
                "properties": {
                  "filename": {
                    "type": "string"
                  },
                  "bytes": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 2147483648
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Unggahan dibuat.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Upload"
                }
              }
            }
          },
          "400": {
            "description": "Permintaan tidak sah. Kode: `validation_failed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Berkas terlalu besar. Kode: `file_too_large`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Format tidak didukung. Kode: `unsupported_format`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "description": "Terlalu banyak permintaan. Kode: `too_many_uploads`, `rate_limit_exceeded`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Detik sebelum mencoba lagi.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "Sementara tidak tersedia. Kode: `storage_full`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/uploads/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^upl_[0-9a-f]{32}$"
          },
          "description": "Id unggahan."
        }
      ],
      "get": {
        "tags": ["Unggahan"],
        "operationId": "getUpload",
        "summary": "Status unggahan (untuk melanjutkan)",
        "responses": {
          "200": {
            "description": "Objek unggahan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Upload"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/uploads/{id}/parts": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^upl_[0-9a-f]{32}$"
          },
          "description": "Id unggahan."
        }
      ],
      "put": {
        "tags": ["Unggahan"],
        "operationId": "uploadPart",
        "summary": "Kirim satu potongan",
        "description": "Potongan ≤ 16 MB, dimulai di `offset` = `received_bytes`. Potongan yang sudah diterima dijawab sukses tanpa ditulis ulang.",
        "parameters": [
          {
            "name": "offset",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Posisi bait potongan ini."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "contentMediaType": "application/octet-stream"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Potongan diterima.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Upload"
                }
              }
            }
          },
          "400": {
            "description": "Permintaan tidak sah. Kode: `validation_failed`, `upload_interrupted`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Offset tidak cocok (pesan menyebut offset yang benar) atau unggahan sudah selesai. Kode: `offset_mismatch`, `upload_not_ready`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Potongan terlalu besar. Kode: `chunk_too_large`, `file_too_large`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Badan bukan biner. Kode: `bad_chunk`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/uploads/{id}/complete": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^upl_[0-9a-f]{32}$"
          },
          "description": "Id unggahan."
        }
      ],
      "post": {
        "tags": ["Unggahan"],
        "operationId": "completeUpload",
        "summary": "Selesaikan unggahan: ukur durasi & harga",
        "description": "Saldo belum dipotong. Lanjutkan dengan `POST /transcriptions {\"upload_id\"}`.",
        "responses": {
          "200": {
            "description": "Unggahan siap.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompletedUpload"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Unggahan belum lengkap. Kode: `upload_incomplete`, `upload_not_ready`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Berkas tidak bisa dipakai. Kode: `unsupported_format`, `not_audio`, `too_long`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/audio/transcriptions": {
      "post": {
        "tags": ["Kompatibel OpenAI"],
        "operationId": "createOpenAITranscription",
        "summary": "Transkripsi sinkron (kompatibel OpenAI)",
        "description": "Menunggu sampai selesai; naikkan timeout klien untuk rekaman panjang. Batas: 90 MB per kiriman, durasi ≤ 2 jam (berkas lebih besar pakai `/uploads` + `POST /transcriptions`), biaya & saldo sama, hasil tampil di dasbor (`source: api_sync`). Tidak mengirim webhook.\n\nKeepalive: status 200 bisa dikirim lebih dulu lalu diisi spasi (JSON) atau baris baru (text/srt/vtt) sampai hasil siap — lakukan `trim()` untuk text/srt/vtt. Kegagalan setelah header terkirim dilaporkan di badan `{\"error\":…}` dengan status 200 (saldo tetap kembali).\n\nKirim ulang berkas identik (akun sama, ≤1 jam, `language` & `num_speakers` sama) saat pekerjaan sebelumnya masih berjalan atau sudah selesai → menunggu/mengembalikan pekerjaan itu tanpa tagihan baru. Koneksi klien putus → pekerjaan tetap berjalan & ditagih; ambil lewat `GET /transcriptions/{id}` (id ada di header `Transkrip-Transcription-Id`).",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["file", "model"],
                "properties": {
                  "file": {
                    "type": "string",
                    "contentMediaType": "application/octet-stream"
                  },
                  "model": {
                    "type": "string",
                    "enum": ["transkrip-1"]
                  },
                  "language": {
                    "type": "string",
                    "enum": ["id", "en", "auto"],
                    "default": "id",
                    "description": "Bahasa rekaman."
                  },
                  "response_format": {
                    "type": "string",
                    "enum": ["json", "text", "srt", "vtt", "verbose_json", "diarized_json"],
                    "default": "json"
                  },
                  "num_speakers": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 6,
                    "description": "Jumlah pembicara. Kosong = dideteksi otomatis."
                  },
                  "stream": {
                    "type": "boolean",
                    "description": "`true` ditolak (`streaming_not_supported`)."
                  },
                  "prompt": {
                    "type": "string",
                    "description": "Diterima, diabaikan."
                  },
                  "temperature": {
                    "type": "number",
                    "description": "Diterima, diabaikan."
                  },
                  "timestamp_granularities[]": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Diterima, diabaikan."
                  },
                  "chunking_strategy": {
                    "type": "string",
                    "description": "Diterima, diabaikan."
                  },
                  "include[]": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Diterima, diabaikan."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Hasil sesuai `response_format`. Bisa berawalan spasi/baris baru (keepalive); periksa field `error` pada JSON.",
            "headers": {
              "Transkrip-Transcription-Id": {
                "description": "Id pekerjaan (`trn_…`).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/OpenAITranscription"
                    },
                    {
                      "$ref": "#/components/schemas/OpenAIVerboseTranscription"
                    },
                    {
                      "$ref": "#/components/schemas/OpenAIDiarizedTranscription"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "`text` dan `srt`."
                }
              },
              "text/vtt": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Permintaan tidak sah. Kode: `validation_failed`, `missing_file`, `invalid_multipart`, `model_not_found`, `streaming_not_supported`, `unsupported_language`, `invalid_response_format`, `upload_interrupted`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Saldo atau batas key tidak cukup. Kode: `insufficient_balance`, `key_limit_exceeded`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "description": "Berkas terlalu besar. Kode: `file_too_large`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Content-Type tidak didukung. Kode: `unsupported_media_type`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Berkas tidak bisa dipakai. Kode: `unsupported_format`, `not_audio`, `too_long`, `audio_unprocessable`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Terlalu banyak permintaan. Kode: `too_many_jobs`, `rate_limit_exceeded`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Detik sebelum mencoba lagi.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "Sementara tidak tersedia. Kode: `storage_full`, `engine_unavailable`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "Menunggu lebih dari 3 jam; ambil lewat GET dengan id di header `Transkrip-Transcription-Id`. Kode: `still_processing`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "transcriptionEvent": {
      "post": {
        "operationId": "transcriptionEvent",
        "summary": "Peristiwa transkrip",
        "description": "Dikirim ke `webhook_url` pekerjaan atau URL bawaan key (hanya API asinkron). Peristiwa: `transcription.completed`, `transcription.failed`, `webhook.test`. Payload adalah petunjuk; ambil hasil dengan `GET /transcriptions/{id}`.\n\nTanda tangan: `v1 = hex(HMAC-SHA256(key = rahasia utuh \"whsec_…\" sebagai UTF-8, pesan = t + \".\" + badan mentah))`. Bisa ada dua `v1=` selama 24 jam setelah rahasia diganti (cocok salah satu = sah). Bandingkan constant-time; tolak bila |sekarang − t| > 300 detik.\n\nBalas 2xx dalam 10 detik; badan jawaban diabaikan, redirect tidak diikuti. 408/429/5xx/galat jaringan dicoba ulang setelah 30 dtk, 2 mnt, 10 mnt, 30 mnt, 1 jam, 3 jam, 6 jam, 12 jam. 4xx lain/3xx = berhenti. Peristiwa bisa tiba lebih dari sekali: dedupe dengan `id`.",
        "security": [],
        "parameters": [
          {
            "name": "Transkrip-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": ["t=1792677901,v1=5f0c2a…"]
            }
          },
          {
            "name": "Transkrip-Event",
            "in": "header",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/WebhookEventType"
            }
          },
          {
            "name": "User-Agent",
            "in": "header",
            "schema": {
              "type": "string",
              "examples": ["Transkrip-Webhooks/1.0"]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Diterima (2xx apa pun)."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Key `trk_live_…` dari dasbor (menu API)."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Key tidak sah. Kode: `invalid_api_key`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Key tidak boleh memakai endpoint ini. Kode: `brand_forbidden`, `insufficient_scope`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Tidak ditemukan. Kode: `not_found`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Terlalu banyak permintaan. Kode: `rate_limit_exceeded`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "headers": {
          "Retry-After": {
            "description": "Detik sebelum mencoba lagi.",
            "schema": {
              "type": "integer"
            }
          }
        }
      }
    },
    "schemas": {
      "TranscriptionStatus": {
        "type": "string",
        "enum": ["queued", "processing", "completed", "failed"]
      },
      "Speaker": {
        "type": "object",
        "required": ["id", "label"],
        "properties": {
          "id": {
            "type": "string",
            "examples": ["A"]
          },
          "label": {
            "type": "string",
            "description": "Mengikuti nama yang diganti di dasbor."
          }
        }
      },
      "Segment": {
        "type": "object",
        "required": ["id", "start", "end", "speaker", "text", "uncertain"],
        "properties": {
          "id": {
            "type": "integer"
          },
          "start": {
            "type": "number",
            "description": "Detik."
          },
          "end": {
            "type": "number",
            "description": "Detik."
          },
          "speaker": {
            "type": "string",
            "description": "Merujuk `speakers[].id`."
          },
          "text": {
            "type": "string"
          },
          "uncertain": {
            "type": "boolean",
            "description": "`true` = bagian yang perlu dicek pengguna (keyakinan rendah atau suara yang semula terlewat). Kembali `false` setelah barisnya disunting atau ditandai sudah dicek di web."
          }
        }
      },
      "Transcription": {
        "type": "object",
        "required": ["id", "object", "status", "created_at"],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^trn_[0-9a-f]{32}$"
          },
          "object": {
            "const": "transcription"
          },
          "status": {
            "$ref": "#/components/schemas/TranscriptionStatus"
          },
          "created_at": {
            "type": "integer"
          },
          "completed_at": {
            "type": ["integer", "null"]
          },
          "title": {
            "type": ["string", "null"]
          },
          "filename": {
            "type": "string"
          },
          "language": {
            "type": "string",
            "enum": ["id", "en", "auto"]
          },
          "duration": {
            "type": ["number", "null"],
            "description": "Detik."
          },
          "billed_minutes": {
            "type": ["integer", "null"],
            "description": "Dibulatkan ke atas per menit, minimal 1."
          },
          "price_idr": {
            "type": ["integer", "null"],
            "description": "Harga pekerjaan ini (Rupiah). Pakai ini, bukan tarif tetap."
          },
          "charged_idr": {
            "type": ["integer", "null"]
          },
          "refunded": {
            "type": "boolean"
          },
          "num_speakers": {
            "type": ["integer", "null"]
          },
          "progress": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "queue_position": {
            "type": ["integer", "null"]
          },
          "estimated_completion_at": {
            "type": ["integer", "null"]
          },
          "expires_at": {
            "type": ["integer", "null"],
            "description": "Kapan teks dihapus otomatis."
          },
          "source": {
            "type": "string",
            "enum": ["web", "api", "api_sync"]
          },
          "metadata": {
            "type": ["object", "null"],
            "additionalProperties": true
          },
          "speakers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Speaker"
            }
          },
          "text": {
            "type": "string",
            "description": "Tidak ada di daftar."
          },
          "segments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Segment"
            },
            "description": "Hanya dengan `include=segments` (bawaan) pada GET satu transkrip."
          },
          "error": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "required": ["code", "message"],
                "properties": {
                  "code": {
                    "type": "string",
                    "examples": ["transcription_failed"]
                  },
                  "message": {
                    "type": "string"
                  }
                }
              }
            ]
          }
        }
      },
      "TranscriptionList": {
        "type": "object",
        "required": ["object", "data", "has_more"],
        "properties": {
          "object": {
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Transcription"
            }
          },
          "first_id": {
            "type": ["string", "null"]
          },
          "last_id": {
            "type": ["string", "null"]
          },
          "has_more": {
            "type": "boolean"
          }
        }
      },
      "DeletedTranscription": {
        "type": "object",
        "required": ["id", "object", "deleted"],
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "const": "transcription"
          },
          "deleted": {
            "const": true
          }
        }
      },
      "CreateFromUploadRequest": {
        "type": "object",
        "required": ["upload_id"],
        "properties": {
          "upload_id": {
            "type": "string",
            "pattern": "^upl_[0-9a-f]{32}$"
          },
          "language": {
            "type": "string",
            "enum": ["id", "en", "auto"],
            "default": "id",
            "description": "Bahasa rekaman."
          },
          "num_speakers": {
            "type": "integer",
            "minimum": 1,
            "maximum": 6,
            "description": "Jumlah pembicara. Kosong = dideteksi otomatis."
          },
          "title": {
            "type": "string",
            "maxLength": 200,
            "description": "Judul yang tampil di dasbor."
          },
          "webhook_url": {
            "type": "string",
            "format": "uri",
            "pattern": "^https://",
            "description": "URL webhook untuk pekerjaan ini (https, alamat publik). Kosong = URL webhook bawaan key."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "maxProperties": 16,
            "description": "≤16 kunci, ≤8 KB; dikembalikan apa adanya."
          }
        }
      },
      "Upload": {
        "type": "object",
        "required": ["id", "object", "filename", "bytes", "received_bytes", "status", "part_bytes"],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^upl_[0-9a-f]{32}$"
          },
          "object": {
            "const": "upload"
          },
          "filename": {
            "type": "string"
          },
          "bytes": {
            "type": "integer"
          },
          "received_bytes": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": ["uploading", "ready", "consumed", "failed"]
          },
          "part_bytes": {
            "type": "integer",
            "examples": [16777216]
          }
        }
      },
      "CompletedUpload": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Upload"
          },
          {
            "type": "object",
            "required": [
              "duration",
              "billed_minutes",
              "price_idr",
              "balance_idr",
              "enough_balance"
            ],
            "properties": {
              "duration": {
                "type": "number"
              },
              "billed_minutes": {
                "type": "integer"
              },
              "price_idr": {
                "type": "integer"
              },
              "balance_idr": {
                "type": "integer"
              },
              "enough_balance": {
                "type": "boolean"
              }
            }
          }
        ]
      },
      "Estimate": {
        "type": "object",
        "required": [
          "upload_id",
          "duration",
          "billed_minutes",
          "price_idr_per_minute",
          "price_idr",
          "balance_idr",
          "enough_balance"
        ],
        "properties": {
          "upload_id": {
            "type": "string"
          },
          "duration": {
            "type": "number"
          },
          "billed_minutes": {
            "type": "integer"
          },
          "price_idr_per_minute": {
            "type": "integer"
          },
          "price_idr": {
            "type": "integer"
          },
          "balance_idr": {
            "type": "integer"
          },
          "enough_balance": {
            "type": "boolean"
          }
        }
      },
      "Balance": {
        "type": "object",
        "required": ["object", "currency", "balance_idr", "reserved_idr", "price_idr_per_minute"],
        "properties": {
          "object": {
            "const": "balance"
          },
          "currency": {
            "const": "IDR"
          },
          "balance_idr": {
            "type": "integer"
          },
          "reserved_idr": {
            "type": "integer",
            "description": "Ditahan untuk transkrip yang belum selesai."
          },
          "price_idr_per_minute": {
            "type": "integer"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["message", "type", "code"],
            "properties": {
              "message": {
                "type": "string"
              },
              "type": {
                "type": "string"
              },
              "code": {
                "type": "string"
              },
              "param": {
                "type": ["string", "null"]
              }
            }
          }
        }
      },
      "Usage": {
        "type": "object",
        "required": ["type", "seconds"],
        "properties": {
          "type": {
            "const": "duration"
          },
          "seconds": {
            "type": "integer"
          }
        }
      },
      "OpenAITranscription": {
        "type": "object",
        "required": ["text"],
        "properties": {
          "text": {
            "type": "string"
          },
          "usage": {
            "$ref": "#/components/schemas/Usage"
          }
        }
      },
      "OpenAIVerboseTranscription": {
        "type": "object",
        "required": ["task", "language", "duration", "text", "segments"],
        "properties": {
          "task": {
            "const": "transcribe"
          },
          "language": {
            "type": "string",
            "examples": ["indonesian"]
          },
          "duration": {
            "type": "number"
          },
          "text": {
            "type": "string"
          },
          "segments": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "seek": {
                  "type": "integer"
                },
                "start": {
                  "type": "number"
                },
                "end": {
                  "type": "number"
                },
                "text": {
                  "type": "string"
                },
                "tokens": {
                  "type": "array",
                  "items": {
                    "type": "integer"
                  }
                },
                "temperature": {
                  "type": "number"
                },
                "speaker": {
                  "type": "string"
                }
              }
            }
          },
          "usage": {
            "$ref": "#/components/schemas/Usage"
          }
        }
      },
      "OpenAIDiarizedTranscription": {
        "type": "object",
        "required": ["task", "duration", "text", "segments"],
        "properties": {
          "task": {
            "const": "transcribe"
          },
          "duration": {
            "type": "number"
          },
          "text": {
            "type": "string"
          },
          "segments": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "const": "transcript.text.segment"
                },
                "id": {
                  "type": "string",
                  "examples": ["seg_0"]
                },
                "start": {
                  "type": "number"
                },
                "end": {
                  "type": "number"
                },
                "text": {
                  "type": "string"
                },
                "speaker": {
                  "type": "string"
                }
              }
            }
          },
          "speakers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Speaker"
            }
          },
          "usage": {
            "$ref": "#/components/schemas/Usage"
          }
        }
      },
      "WebhookEventType": {
        "type": "string",
        "enum": ["transcription.completed", "transcription.failed", "webhook.test"]
      },
      "WebhookEvent": {
        "type": "object",
        "required": ["id", "object", "type", "created_at", "data"],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]{32}$"
          },
          "object": {
            "const": "event"
          },
          "type": {
            "$ref": "#/components/schemas/WebhookEventType"
          },
          "created_at": {
            "type": "integer"
          },
          "data": {
            "type": "object",
            "required": ["id", "status"],
            "description": "Untuk `webhook.test`: `id` = null, `status` = \"test\".",
            "properties": {
              "id": {
                "type": ["string", "null"],
                "description": "Id transkrip (`trn_…`)."
              },
              "status": {
                "type": "string",
                "enum": ["completed", "failed", "test"]
              }
            }
          }
        }
      }
    }
  }
}
