{
  "title": "mojiokoshi error codes",
  "description": "このファイルは、文字おこしくんのエラーコードの一覧を持つ。websocket は Ingest / Subscribe の error.code を、http は HTTP API の problem details の code を表す（HTTP API の仕様は openapi.yaml にある）。文字おこしくんは、互換性のある変更としてコードを足すことがあるので、クライアントは未知のコードも受け入れること。文字おこしくんは、使わなくなったコードの項目も消さずに残し、その項目の deprecated に、そのコードを返さなくなった日を書く。文字おこしくんは WebSocket の close code を規定しない。クライアントは、切れた理由を、直前に受け取った error.code で判断する。",
  "protocolVersion": 1,
  "unknownCodePolicy": "Unknown codes may be added without a protocol version bump. Accept them and follow error.fatal: a fatal unknown code is handled like internal (reconnect with backoff). / クライアントは、未知のコードも受け入れ、error.fatal に従う。fatal な未知のコードでは、クライアントは internal と同じくバックオフで再接続する。",
  "reconnectPolicies": {
    "never": "Do not reconnect. / 再接続しない。",
    "never_if_fatal": "If fatal, do not reconnect. Otherwise keep using the connection. / fatal なら再接続しない。fatal でなければ、接続を使い続ける。",
    "backoff": "Reconnect with exponential backoff and jitter (e.g. 0.5s doubling up to 30s). / 指数バックオフで再接続する。",
    "long_backoff_if_fatal": "If fatal, wait the maximum backoff (e.g. 30s) before reconnecting. / fatal なら、上限の待ち時間をおいてから再接続する。"
  },
  "errors": [
    {
      "code": "unauthorized",
      "type": "https://moji.kaitoranma.com/docs/errors/#unauthorized",
      "summary": "クライアントが資格情報を送らなかった。または、送った資格情報が、不正か、期限切れか、失効している。または、トークンの権限（scope）が足りない。",
      "websocket": {
        "endpoints": [
          "ingest",
          "subscribe"
        ],
        "fatal": "always",
        "reconnect": "never"
      },
      "http": {
        "status": 401,
        "statuses": [
          401
        ],
        "title": "認証が必要です",
        "extras": "WWW-Authenticate: Bearer realm=\"mojiokoshi\""
      },
      "retryable": false,
      "fix": "新しいセッショントークン（Ingest は ingest:write、Subscribe は transcript:read）や有効な API キーを使う。"
    },
    {
      "code": "protocol_version_unsupported",
      "type": "https://moji.kaitoranma.com/docs/errors/#protocol_version_unsupported",
      "summary": "hello の protocolVersion を文字おこしくんが扱えない。",
      "websocket": {
        "endpoints": [
          "ingest",
          "subscribe"
        ],
        "fatal": "always",
        "reconnect": "never"
      },
      "http": null,
      "retryable": false,
      "fix": "hello の protocolVersion を 1 にする（クライアントを更新する）。"
    },
    {
      "code": "invalid_message",
      "type": "https://moji.kaitoranma.com/docs/errors/#invalid_message",
      "summary": "クライアントが、JSON として読めないメッセージか、形式に違反するメッセージを送った。hello の前にほかのメッセージを送ったときと、hello が時間内（10 秒）に届かないときも、文字おこしくんはこのコードを送る。",
      "websocket": {
        "endpoints": [
          "ingest",
          "subscribe"
        ],
        "fatal": "sometimes",
        "reconnect": "never_if_fatal"
      },
      "http": null,
      "retryable": false,
      "fix": "送るメッセージを JSON Schema で検証して直す。fatal なら、文字おこしくんが hello を拒んだので、同じ hello で再接続しない。"
    },
    {
      "code": "unknown_track",
      "type": "https://moji.kaitoranma.com/docs/errors/#unknown_track",
      "summary": "クライアントが、開いていない trackId か channel を参照した。",
      "websocket": {
        "endpoints": [
          "ingest"
        ],
        "fatal": "never",
        "reconnect": "backoff"
      },
      "http": null,
      "retryable": false,
      "fix": "track_open でトラックを開いてから、その channel で音声を送り、その trackId で speech_start / speech_end を送る。"
    },
    {
      "code": "unknown_speaker",
      "type": "https://moji.kaitoranma.com/docs/errors/#unknown_speaker",
      "summary": "クライアントが、speaker_upsert で宣言していない speakerId を参照した。たとえば、single トラックの speakerId が、宣言していない話者を指していた。",
      "websocket": {
        "endpoints": [
          "ingest"
        ],
        "fatal": "never",
        "reconnect": "backoff"
      },
      "http": null,
      "retryable": false,
      "fix": "speaker_upsert で話者を宣言してから、その話者の single トラックを track_open する。"
    },
    {
      "code": "channel_in_use",
      "type": "https://moji.kaitoranma.com/docs/errors/#channel_in_use",
      "summary": "track_open で指定した channel を、同じ接続の別のトラックが使っている。",
      "websocket": {
        "endpoints": [
          "ingest"
        ],
        "fatal": "never",
        "reconnect": "backoff"
      },
      "http": null,
      "retryable": false,
      "fix": "使っていない channel 番号で track_open し直す（track_close した番号は再利用してよい）。"
    },
    {
      "code": "track_limit_exceeded",
      "type": "https://moji.kaitoranma.com/docs/errors/#track_limit_exceeded",
      "summary": "トラックの数が、1 接続あたりの上限（64 本）を超えた。または、API キーで作ったセッションで開いているトラックの数が、全部の API キー（ほかの利用者のキーを含む）を合わせた上限を超えた。または、テナント（Discord サーバー）の同時トラック数が、文字おこしくんの運営者が付けた上限を超えた。公式の文字おこしくん（moji.kaitoranma.com）には、テナントの同時トラック数の上限は無い。",
      "websocket": {
        "endpoints": [
          "ingest"
        ],
        "fatal": "never",
        "reconnect": "backoff"
      },
      "http": null,
      "retryable": false,
      "fix": "使っていないトラックを track_close してから、新しいトラックを開く。文字おこしくんが開かなかったトラックの音声は送らない。全部の API キーを合わせた上限に当たったときは、時間をおいてから、文字おこしくんが開かなかったトラックを開き直す。"
    },
    {
      "code": "unsupported_format",
      "type": "https://moji.kaitoranma.com/docs/errors/#unsupported_format",
      "summary": "track_open の format（コーデック・サンプルレート・チャンネル数）を文字おこしくんが扱えない。",
      "websocket": {
        "endpoints": [
          "ingest"
        ],
        "fatal": "never",
        "reconnect": "backoff"
      },
      "http": null,
      "retryable": false,
      "fix": "opus は 48000 Hz・1/2ch、pcm_s16le は 8000〜48000 Hz・1/2ch で track_open し直す。"
    },
    {
      "code": "rate_limited",
      "type": "https://moji.kaitoranma.com/docs/errors/#rate_limited",
      "summary": "送信量・要求数・接続数・話者数などが上限を超えた。または、Subscribe のクライアントの受信が、文字おこしくんからの送信に追いつかない。",
      "websocket": {
        "endpoints": [
          "ingest",
          "subscribe"
        ],
        "fatal": "sometimes",
        "reconnect": "long_backoff_if_fatal"
      },
      "http": {
        "status": 429,
        "statuses": [
          429
        ],
        "title": "要求が多すぎます",
        "extras": "Retry-After、retryAfterSeconds、RateLimit / RateLimit-Policy"
      },
      "retryable": true,
      "fix": "送る量を減らす。HTTP では、Retry-After の秒数だけ待ってから要求をやり直す。WebSocket では、文字おこしくんが接続を切ったら、待ち時間をおいて再接続する。"
    },
    {
      "code": "session_ended",
      "type": "https://moji.kaitoranma.com/docs/errors/#session_ended",
      "summary": "セッションが終了しているか、存在しない。保持期間を過ぎて消えたセッションにも、文字おこしくんはこのコードを送る。",
      "websocket": {
        "endpoints": [
          "ingest",
          "subscribe"
        ],
        "fatal": "always",
        "reconnect": "never"
      },
      "http": {
        "status": 409,
        "statuses": [
          409
        ],
        "title": "セッションは終了しています"
      },
      "retryable": false,
      "fix": "再接続しない。文字起こしを続けるなら、新しいセッションを作る。"
    },
    {
      "code": "superseded",
      "type": "https://moji.kaitoranma.com/docs/errors/#superseded",
      "summary": "同じセッションに新しい Ingest の接続が来たので、文字おこしくんがこの接続を新しい接続に置き換えた。",
      "websocket": {
        "endpoints": [
          "ingest"
        ],
        "fatal": "always",
        "reconnect": "never"
      },
      "http": null,
      "retryable": false,
      "fix": "再接続しない。新しい接続のほうが有効になっている。1 セッションに、Ingest の送り手は 1 つだけにする。"
    },
    {
      "code": "quota_exceeded",
      "type": "https://moji.kaitoranma.com/docs/errors/#quota_exceeded",
      "summary": "テナント（Discord サーバー）の利用量が、今日（UTC）の利用上限に達した。Ingest では、セッションも終わっている。",
      "websocket": {
        "endpoints": [
          "ingest"
        ],
        "fatal": "always",
        "reconnect": "never"
      },
      "http": {
        "status": 429,
        "statuses": [
          429
        ],
        "title": "利用上限に達しました",
        "extras": "Retry-After（次の UTC 0 時までの秒数）、retryAfterSeconds、quota"
      },
      "retryable": true,
      "fix": "次の UTC 0 時（日本時間 9:00）の後に新しいセッションを作る。公式の文字おこしくん（moji.kaitoranma.com）には、1 日の利用量の上限は無い。上限を変えたいときは、文字おこしくんの運営者（contact@kaitoranma.com）に連絡する。"
    },
    {
      "code": "internal",
      "type": "https://moji.kaitoranma.com/docs/errors/#internal",
      "summary": "文字おこしくんの内部で問題が起きた。",
      "websocket": {
        "endpoints": [
          "ingest",
          "subscribe"
        ],
        "fatal": "sometimes",
        "reconnect": "backoff"
      },
      "http": {
        "status": 500,
        "statuses": [
          500
        ],
        "title": "文字おこしくんの内部のエラーです"
      },
      "retryable": true,
      "fix": "指数バックオフ（ジッター付き）でやり直す。問題が続くなら、文字おこしくんの運営者（contact@kaitoranma.com）に連絡する。"
    },
    {
      "code": "forbidden",
      "type": "https://moji.kaitoranma.com/docs/errors/#forbidden",
      "summary": "要求に使ったトークンが、別のセッションのものだった。または、要求の tenantId か externalRef が、API キーのテナントと違っていた。",
      "websocket": null,
      "http": {
        "status": 403,
        "statuses": [
          403
        ],
        "title": "この操作は許可されていません"
      },
      "retryable": false,
      "fix": "対象のセッションとテナントに合った資格情報を使う。"
    },
    {
      "code": "insufficient_scope",
      "type": "https://moji.kaitoranma.com/docs/errors/#insufficient_scope",
      "summary": "要求に使った API キーに、その操作のスコープが無い。",
      "websocket": null,
      "http": {
        "status": 403,
        "statuses": [
          403
        ],
        "title": "API キーの権限が足りません",
        "extras": "WWW-Authenticate（error=\"insufficient_scope\", scope=\"…\"）、requiredScope"
      },
      "retryable": false,
      "fix": "応答の requiredScope が示すスコープを持つ API キーを、Discord サーバーの管理者に管理画面で発行してもらう。"
    },
    {
      "code": "api_access_disabled",
      "type": "https://moji.kaitoranma.com/docs/errors/#api_access_disabled",
      "summary": "このテナント（Discord サーバー）では、API キーの利用が止まっている。公式の文字おこしくん（moji.kaitoranma.com）では、文字おこしくんの運営者が止めたときだけ、この状態になる。",
      "websocket": null,
      "http": {
        "status": 403,
        "statuses": [
          403
        ],
        "title": "API キーの利用が無効です"
      },
      "retryable": false,
      "fix": "文字おこしくんの運営者（contact@kaitoranma.com）に連絡する。"
    },
    {
      "code": "invalid_json",
      "type": "https://moji.kaitoranma.com/docs/errors/#invalid_json",
      "summary": "文字おこしくんが、要求の本文を JSON として読めなかった。",
      "websocket": null,
      "http": {
        "status": 400,
        "statuses": [
          400
        ],
        "title": "本文が JSON ではありません"
      },
      "retryable": false,
      "fix": "Content-Type: application/json で正しい JSON のオブジェクトを送る。"
    },
    {
      "code": "invalid_request",
      "type": "https://moji.kaitoranma.com/docs/errors/#invalid_request",
      "summary": "要求の値が不正だった。本文の形・クエリの形・パスの tenantId の形・Idempotency-Key の形のどれかが不正なときも、文字おこしくんはこのコードを返す。",
      "websocket": null,
      "http": {
        "status": 400,
        "statuses": [
          400
        ],
        "title": "要求の値が不正です"
      },
      "retryable": false,
      "fix": "detail が示すフィールドを直す。"
    },
    {
      "code": "session_not_found",
      "type": "https://moji.kaitoranma.com/docs/errors/#session_not_found",
      "summary": "指定したセッションが無い。保持期間を過ぎて消えたセッションを指定したときと、API キーのテナントとは別のテナントのセッションを指定したときにも、文字おこしくんはこのコードを返す。",
      "websocket": null,
      "http": {
        "status": 404,
        "statuses": [
          404
        ],
        "title": "セッションが見つかりません"
      },
      "retryable": false,
      "fix": "sessionId を確かめる。消えた記録は戻らないので、残したい記録は保持期間の内に書き出す。"
    },
    {
      "code": "key_not_found",
      "type": "https://moji.kaitoranma.com/docs/errors/#key_not_found",
      "summary": "指定した API キーが無いか、別のテナントのものだった。",
      "websocket": null,
      "http": {
        "status": 404,
        "statuses": [
          404
        ],
        "title": "API キーが見つかりません"
      },
      "retryable": false,
      "fix": "keyId を確かめる。"
    },
    {
      "code": "webhook_not_found",
      "type": "https://moji.kaitoranma.com/docs/errors/#webhook_not_found",
      "summary": "指定した Webhook の送信先が無いか、別のテナントのものだった。",
      "websocket": null,
      "http": {
        "status": 404,
        "statuses": [
          404
        ],
        "title": "Webhook の送信先が見つかりません"
      },
      "retryable": false,
      "fix": "webhookId を確かめる。"
    },
    {
      "code": "webhook_delivery_not_found",
      "type": "https://moji.kaitoranma.com/docs/errors/#webhook_delivery_not_found",
      "summary": "指定した送信の記録がその送信先に無い。終わった送信の記録は 7 日で削除される。削除された記録を指定したときにも、文字おこしくんはこのコードを返す。",
      "websocket": null,
      "http": {
        "status": 404,
        "statuses": [
          404
        ],
        "title": "Webhook の送信の記録が見つかりません"
      },
      "retryable": false,
      "fix": "deliveryId と webhookId の組を確かめる。"
    },
    {
      "code": "session_active",
      "type": "https://moji.kaitoranma.com/docs/errors/#session_active",
      "summary": "クライアントが、稼働中か終了の途中のセッションを削除しようとした。",
      "websocket": null,
      "http": {
        "status": 409,
        "statuses": [
          409
        ],
        "title": "セッションは終了していません"
      },
      "retryable": true,
      "fix": "POST /v1/sessions/:id/end でセッションを終えてから削除する。終了の途中なら、少し待ってから削除をやり直す。"
    },
    {
      "code": "key_limit_exceeded",
      "type": "https://moji.kaitoranma.com/docs/errors/#key_limit_exceeded",
      "summary": "テナントの有効な API キーの数が、上限（既定 25 個）に達した。",
      "websocket": null,
      "http": {
        "status": 409,
        "statuses": [
          409
        ],
        "title": "API キーの数が上限に達しています",
        "extras": "limit"
      },
      "retryable": false,
      "fix": "使っていないキーを管理画面で取り消してから、新しいキーを作る。"
    },
    {
      "code": "webhook_limit_exceeded",
      "type": "https://moji.kaitoranma.com/docs/errors/#webhook_limit_exceeded",
      "summary": "テナントの Webhook の送信先の数が上限（既定 5 個）に達した。",
      "websocket": null,
      "http": {
        "status": 409,
        "statuses": [
          409
        ],
        "title": "Webhook の送信先の数が上限に達しています",
        "extras": "limit"
      },
      "retryable": false,
      "fix": "使っていない送信先を管理画面で削除してから、新しい送信先を追加する。"
    },
    {
      "code": "idempotency_in_progress",
      "type": "https://moji.kaitoranma.com/docs/errors/#idempotency_in_progress",
      "summary": "同じ Idempotency-Key の前の要求を、文字おこしくんがまだ処理している。",
      "websocket": null,
      "http": {
        "status": 409,
        "statuses": [
          409
        ],
        "title": "同じ Idempotency-Key の要求を処理中です",
        "extras": "Retry-After: 1"
      },
      "retryable": true,
      "fix": "Retry-After の秒数だけ待ってから、同じキーと同じ本文で要求をやり直す。"
    },
    {
      "code": "idempotency_mismatch",
      "type": "https://moji.kaitoranma.com/docs/errors/#idempotency_mismatch",
      "summary": "クライアントが、前の要求と同じ Idempotency-Key で、本文の違う要求を送った。",
      "websocket": null,
      "http": {
        "status": 422,
        "statuses": [
          422
        ],
        "title": "この Idempotency-Key は、本文の違う要求で使用済みです"
      },
      "retryable": false,
      "fix": "別の操作には新しい Idempotency-Key を使う。"
    },
    {
      "code": "not_found",
      "type": "https://moji.kaitoranma.com/docs/errors/#not_found",
      "summary": "要求したパスの API が無い。",
      "websocket": null,
      "http": {
        "status": 404,
        "statuses": [
          404
        ],
        "title": "見つかりません"
      },
      "retryable": false,
      "fix": "パスを確かめる。API のパスは /v1 から始まる。"
    },
    {
      "code": "method_not_allowed",
      "type": "https://moji.kaitoranma.com/docs/errors/#method_not_allowed",
      "summary": "そのパスでは、要求したメソッドを使えない。",
      "websocket": null,
      "http": {
        "status": 405,
        "statuses": [
          405
        ],
        "title": "このメソッドは使えません"
      },
      "retryable": false,
      "fix": "リファレンス（openapi.yaml）が示すメソッドを使う。"
    },
    {
      "code": "payload_too_large",
      "type": "https://moji.kaitoranma.com/docs/errors/#payload_too_large",
      "summary": "要求の本文が 16 KiB を超えた。",
      "websocket": null,
      "http": {
        "status": 413,
        "statuses": [
          413
        ],
        "title": "本文が大きすぎます"
      },
      "retryable": false,
      "fix": "要求の本文を小さくする。"
    },
    {
      "code": "upgrade_required",
      "type": "https://moji.kaitoranma.com/docs/errors/#upgrade_required",
      "summary": "クライアントが、WebSocket のパス（/v1/ingest・/v1/subscribe）に普通の HTTP でアクセスした。",
      "websocket": null,
      "http": {
        "status": 426,
        "statuses": [
          426
        ],
        "title": "WebSocket で接続してください",
        "extras": "Upgrade: websocket"
      },
      "retryable": false,
      "fix": "WebSocket（wss://）でつなぐ。"
    },
    {
      "code": "activity_not_configured",
      "type": "https://moji.kaitoranma.com/docs/errors/#activity_not_configured",
      "summary": "文字おこしくんは、Discord アクティビティ専用の操作にだけ、このコードを返す。文字おこしくんのサーバーに、アクティビティの設定が無い。",
      "websocket": null,
      "http": {
        "status": 503,
        "statuses": [
          503
        ],
        "title": "アクティビティの設定がありません"
      },
      "retryable": false,
      "fix": "あなたの側では直せない（原因は、文字おこしくんの運営者の側の設定にある）。問題が続くときは、文字おこしくんの運営者に連絡する。"
    },
    {
      "code": "invalid_grant",
      "type": "https://moji.kaitoranma.com/docs/errors/#invalid_grant",
      "summary": "文字おこしくんは、Discord アクティビティ専用の操作にだけ、このコードを返す。クライアントが送った OAuth2 の認可コードが、不正か、期限切れか、使用済みだった。",
      "websocket": null,
      "http": {
        "status": 400,
        "statuses": [
          400
        ],
        "title": "認可コードが無効です"
      },
      "retryable": false,
      "fix": "アクティビティを開き直して認可をやり直す。"
    },
    {
      "code": "not_in_channel",
      "type": "https://moji.kaitoranma.com/docs/errors/#not_in_channel",
      "summary": "文字おこしくんは、Discord アクティビティ専用の操作にだけ、このコードを返す。要求した人が、アクティビティを開いた Discord サーバーのどのボイスチャンネルにもいない。要求した人がボイスチャンネルにいるかを確かめられないときも、文字おこしくんはこのコードを返す。",
      "websocket": null,
      "http": {
        "status": 403,
        "statuses": [
          403
        ],
        "title": "ボイスチャンネルにいません"
      },
      "retryable": true,
      "fix": "ボイスチャンネルに入ってから、アクティビティを開く。"
    },
    {
      "code": "no_active_session",
      "type": "https://moji.kaitoranma.com/docs/errors/#no_active_session",
      "summary": "文字おこしくんは、Discord アクティビティ専用の操作にだけ、このコードを返す。要求した人が今いるボイスチャンネルで、文字起こしが動いていない。",
      "websocket": null,
      "http": {
        "status": 404,
        "statuses": [
          404
        ],
        "title": "稼働中のセッションがありません"
      },
      "retryable": true,
      "fix": "誰かが /start を実行して、文字起こしが始まるのを待つ。"
    },
    {
      "code": "discord_unavailable",
      "type": "https://moji.kaitoranma.com/docs/errors/#discord_unavailable",
      "summary": "文字おこしくんは、Discord アクティビティ専用の操作にだけ、このコードを返す。Discord の障害か上限のために、文字おこしくんが Discord の API に問い合わせられない。",
      "websocket": null,
      "http": {
        "status": 502,
        "statuses": [
          502
        ],
        "title": "Discord に接続できません"
      },
      "retryable": true,
      "fix": "少し待ってから、要求をやり直す。"
    },
    {
      "code": "credits_exhausted",
      "type": "https://moji.kaitoranma.com/docs/errors/#credits_exhausted",
      "summary": "以前、Discord サーバーのクレジットが残っていないときに返していたコード。文字おこしくんは、2026-10-14 から、このコードを返さない。",
      "websocket": null,
      "http": {
        "status": 402,
        "statuses": [
          402
        ],
        "title": "クレジットが残っていません"
      },
      "retryable": false,
      "fix": "対処は要らない。古いクライアントは、このコードを扱う処理を残しておいてよい。",
      "deprecated": "2026-10-14"
    },
    {
      "code": "too_many_connections",
      "type": "https://moji.kaitoranma.com/docs/errors/#too_many_connections",
      "summary": "WebSocket の同時接続数が、上限（文字おこしくんの API 全体で 4,096、同じ送信元から 1,024）を超えた。文字おこしくんは、upgrade の要求への HTTP の応答でこのコードを返す。",
      "websocket": null,
      "http": {
        "status": 503,
        "statuses": [
          503,
          429
        ],
        "title": "接続が多すぎます",
        "extras": "API 全体の上限は 503、同じ送信元の上限は 429"
      },
      "retryable": true,
      "fix": "普通の切断と同じく指数バックオフで再接続する。使っていない接続を閉じる。"
    },
    {
      "code": "shutting_down",
      "type": "https://moji.kaitoranma.com/docs/errors/#shutting_down",
      "summary": "再起動かデプロイのために、文字おこしくんは停止の途中だった。",
      "websocket": null,
      "http": {
        "status": 503,
        "statuses": [
          503
        ],
        "title": "文字おこしくんは停止の途中です"
      },
      "retryable": true,
      "fix": "バックオフしてから、要求をやり直す。文字おこしくんが再起動した後は、要求が通る。"
    }
  ]
}
