HTTP API リファレンス

この API は、あなたのサーバー側のコードから呼び、セッションを探す・短期のセッショントークンを発行する・文字起こしを書き出す・利用量を見るために使います。ベース URL は https://moji.kaitoranma.com です。機械可読な版は openapi.yaml(OpenAPI 3.1)にあります。文字おこしくんが文字起こしの開始と終了をあなたの URL に知らせる仕組み(Webhook)は、Webhook リファレンスにあります。

エンドポイント一覧

メソッド・パス 認証 内容
GET /healthz なし 文字おこしくんが動いているかを確かめる
GET /v1/sessions API キー(transcripts:read) セッションを一覧する
POST /v1/sessions API キー(ingest:write) セッションを作る
GET /v1/sessions/{sessionId} API キー(transcripts:read) セッションの状態を見る
DELETE /v1/sessions/{sessionId} API キー(transcripts:delete) セッションを削除する
POST /v1/sessions/{sessionId}/end API キー(ingest:write) セッションを終える
POST /v1/sessions/{sessionId}/tokens API キー(transcripts:read) / API キー(ingest:write) セッショントークンを発行する
GET /v1/sessions/{sessionId}/transcript API キー(transcripts:read) / セッショントークン(transcript:read) / クエリの token(transcript:download) 文字起こしを書き出す
POST /v1/sessions/{sessionId}/downloads API キー(transcripts:read) 書き出しのリンクを作る
GET /v1/sessions/{sessionId}/chatgpt クエリの token(transcript:download) 記録を ChatGPT に渡す
GET /v1/usage API キー(usage:read) 利用量を見る
GET /v1/ingest 接続後の hello でセッショントークン WebSocket の Ingest(音声を送る)に接続する
GET /v1/subscribe 接続後の hello でセッショントークン WebSocket の Subscribe(結果を受け取る)に接続する

認証

Authorization: Bearer <資格情報>
資格情報 形 使える操作 ブラウザ・OBS に置いてよいか
API キー mjk_live_ + 38 文字(base62) キーのスコープが許す操作(スコープの表)。API キーは、API キーのテナント(Discord サーバー)のセッションと利用量だけを扱える 置かない(流出したら管理画面で取り消す)
セッショントークン(transcript:read) base64url.base64url そのセッションの書き出し(と WebSocket の Subscribe) 置いてよい(使えるセッションも権限も 1 つだけで、期限がある)
書き出しのリンクのトークン(transcript:download) base64url.base64url そのセッションの書き出しとChatGPT への転送だけ。Authorization ではなく、クエリの token で送る(書き出しのリンクに入っている) 置いてよい。チャットに貼ってもよい(書き出しにしか使えず、期限がある)

API キーは、ボットを入れた Discord サーバーの管理者が管理画面で作ります(クイックスタート)。作るときに、管理者はスコープと期限を選びます。管理画面は作ったキーをその 1 度だけ表示します。キーの形は mjk_live_ + 32 文字の乱数 + 6 文字のチェックサム(base62)で、シークレットスキャナーは mjk_live_[0-9A-Za-z]{38} でキーを見つけられます。

API キーで発行するセッショントークンの有効期間はレート制限と上限、期限の後の取り直しは再接続とエラーの扱いにあります。

エラー(problem details)

エラーの本文は RFC 9457 の problem details(Content-Type: application/problem+json; charset=utf-8)です。code で分岐してください。 error と message は、以前の形との後方互換のためのメンバーで、code / detail と同じ値を持ちます。

{
  "type": "https://moji.kaitoranma.com/docs/errors/#rate_limited",
  "title": "要求が多すぎます",
  "status": 429,
  "detail": "要求が多すぎます。しばらくしてから、要求をやり直してください",
  "code": "rate_limited",
  "error": "rate_limited",
  "message": "要求が多すぎます。しばらくしてから、要求をやり直してください",
  "retryAfterSeconds": 1
}
メンバー 型 説明
type string(URI) https://moji.kaitoranma.com/docs/errors/#<code>(コードごとに固定の識別子)
title string コードごとに固定の短い説明(日本語。文言は変わることがある)
status integer HTTP ステータス(応答と同じ)
detail / message string この要求で起きたエラーの説明(日本語。文言は変わることがある)
code / error string 機械向けのコード
retryAfterSeconds integer rate_limited・quota_exceeded: 待つ秒数(Retry-After と同じ)
quota object quota_exceeded: tenantId・dailyAudioSeconds・usedAudioSeconds・resetsAt(次の UTC 0 時)
requiredScope string insufficient_scope: 足りないスコープ

共通の決まり

レート制限

文字おこしくんは、API キーでの要求を、API キーごとに 1 分あたり 120 要求までに制限します。認証の無い要求・セッショントークンでの書き出しの要求・認証に失敗した要求は、送信元 IP ごとに 1 分あたり 120 要求までに制限します。制限はトークンバケットの方式で、使える要求の数は、毎秒 2 ずつ戻り、最大 120 までたまります。レート制限の対象の応答には、成功・失敗のどちらにも、IETF の草案(draft-ietf-httpapi-ratelimit-headers)の形のヘッダーが付きます。

RateLimit-Policy: "api-key";q=120;w=60
RateLimit: "api-key";r=117;t=2

Idempotency-Key(やり直しの安全)

POST /v1/sessions・POST /v1/sessions/{sessionId}/tokens・POST /v1/sessions/{sessionId}/downloads は Idempotency-Key ヘッダーを受け付けます。ネットワークの失敗でやり直すときに同じキーを使うと、文字おこしくんはセッションやトークンを二重に作りません。

項目 内容
値 1〜255 文字の印字可能な ASCII(UUID を勧める)。この形に合わない値には、文字おこしくんが 400 invalid_request を返す
同じ要求とみなす範囲 API キー × メソッドとパス × キーの値
覚える期間 24 時間
覚える応答 成功(2xx)した応答だけ。失敗した要求は同じキーでやり直せる
同じキー・同じ本文 文字おこしくんは、最初の応答をそのまま返す(Idempotent-Replayed: true)
同じキー・違う本文 文字おこしくんは 422 idempotency_mismatch を返す
同じキーの要求が処理中 文字おこしくんは 409 idempotency_in_progress(Retry-After: 1)を返す

ページ送り(cursor)

GET /v1/sessions は新しい順に limit 件(既定 20、最大 100)を返し、続きがあれば nextCursor を付けます。次のページは ?cursor=<nextCursor> で取ります。カーソルは不透明な文字列なので、中身に依存しないでください。ページの間にセッションが増えても、重複も抜けも出ません。nextCursor が無ければ最後のページです。

ヘルスチェック(Health)

クライアントは、この操作で文字おこしくんが動いているかを確かめる。

文字おこしくんが動いているかを確かめる(GET /healthz)

この操作は文字おこしくんが動いているかを返す。この操作には認証もレート制限も無い。

ステータス 意味
200 文字おこしくんが動いている。

応答 200:

フィールド 型 説明
ok true 必須
{
  "ok": true
}
curl -sS "$BASE/healthz"

セッションとトークン(Sessions)

クライアントは、API キーで、セッションの一覧・状態・作成・終了と、セッショントークンの発行を扱う。

セッションを一覧する(GET /v1/sessions)

文字おこしくんは、セッションを新しい順(createdAt の降順、同時刻は sessionId の降順)に返す。続きがあれば応答に nextCursor が付くので、次のページを取るときは、その値を cursor に渡す(キーセットのカーソルなので、ページの間にセッションが増えても、重複も抜けも出ない)。文字おこしくんは、API キーのテナント(Discord サーバー)のセッションだけを返す。

パラメーター 場所 型 説明
status query "active" / "ended" 任意 active(終了していない)か ended(終了した)を指定する。省くと、文字おこしくんは両方を返す。
limit query integer(1〜100、既定 20) 任意 1 ページの件数を指定する(既定 20)。
cursor query string(2048 文字まで) 任意 前のページの nextCursor を渡す。不正な値には、文字おこしくんが 400 invalid_request を返す。
endedSince query integer(0 以上) 任意 指定すると、文字おこしくんは、終了していないセッションと、この時刻(エポックミリ秒)以降に終了したセッションだけを返す。
ステータス 意味
200 文字おこしくんが、セッションの一覧を返した。
400 status・limit・cursor・endedSince の値が不正である(invalid_request)
401 Authorization: Bearer が無いか、不正である。または、API キーが失効している(unauthorized)
403 API キーに transcripts:read のスコープが無い。または、テナントで API キーの利用が無効になっている(insufficient_scope / api_access_disabled)
429 レート制限を超えた。Retry-After の秒数の後にやり直す(rate_limited)
default ほかのステータスのエラー(problem details)。たとえば 413 payload_too_large・405 method_not_allowed・500 internal・503 shutting_down がある。code で分岐し、知らないコードはステータスで扱う(5xx と 429 は待ってやり直す)。

応答 200:

フィールド 型 説明
sessions object[] 必須 文字おこしくんは、セッションを、createdAt の降順(同時刻は sessionId の降順)に並べる。
sessions[].sessionId string 必須 セッションの ID を表す。
sessions[].label string(1〜100 文字) 必須 表示名を表す。
sessions[].externalRef string(1〜256 文字) 任意 入力元での識別子を表す。公式の Discord ボットは、discord:<guildId>:<channelId> を使う。
sessions[].tenantId string 任意 テナント(Discord ではギルド ID)を表す。
sessions[].createdAt number(エポックミリ秒) 必須
sessions[].endedAt number(エポックミリ秒) 任意 終了した時刻を表す。セッションが終了していなければ、このフィールドは無い。
sessions[].live boolean 必須 文字おこしくんのサーバーのプロセスで、セッションが稼働中かどうかを表す(再起動の直後は、音声の送り手(Ingest)か受け手(Subscribe)がつなぎ直すまで false)。
sessions[].audioSeconds number(0 以上) 必須 このセッションの利用量を表す。利用量は音声認識にかけた音声の秒数で、小数第 3 位までの値である。
sessions[].retainUntil number(エポックミリ秒) 任意 終了したセッションが、保持期間を過ぎて消える時刻を表す。文字おこしくんがセッションを消さないなら、このフィールドは無い。テナント(Discord サーバー)の保持期間が延びると、文字おこしくんは、この時刻を、セッションの終了から延びた保持期間の後まで遅らせる。文字おこしくんは、この時刻を早めない。
nextCursor string(2048 文字まで) 任意 続きがあるときだけ付く。次のページを取るときは、この値を cursor に渡す(不透明な文字列なので、中身に依存しない)。
{
  "sessions": [
    {
      "sessionId": "0f0c3c1e-5b7a-4d2e-9c61-3f2d8a4b7e10",
      "label": "#雑談 VC",
      "externalRef": "discord:111111111111111111:222222222222222222",
      "tenantId": "111111111111111111",
      "createdAt": 1790350826814,
      "live": true,
      "audioSeconds": 312.5
    },
    {
      "sessionId": "7d1e2f3a-8b9c-4d0e-a1b2-c3d4e5f60718",
      "label": "#雑談 VC",
      "externalRef": "discord:111111111111111111:222222222222222222",
      "tenantId": "111111111111111111",
      "createdAt": 1790264426814,
      "endedAt": 1790268026814,
      "live": false,
      "audioSeconds": 1234.567,
      "retainUntil": 1792860026814
    }
  ],
  "nextCursor": "eyJjIjoxNzkwMzUwODI2ODE0LCJpIjoiMGYwYzNjMWUtNWI3YS00ZDJlLTljNjEtM2YyZDhhNGI3ZTEwIn0"
}
curl -sS "$BASE/v1/sessions?status=active&limit=5" \
  -H "Authorization: Bearer $MOJIOKOSHI_API_KEY"

セッションを作る(POST /v1/sessions)

クライアントは、この操作で、自分で音声を送る(Ingest)ためのセッションを作る。文字おこしくんは、作ったセッションをすぐ稼働させ、Ingest の接続を待つ(30 分つながらなければ、セッションは自動で終わる)。クライアントは、応答の ingest.token を音声の送り手に、subscribe.token / viewerUrl / overlayUrl を見る側に渡す。reuseActive: true なら、文字おこしくんは同じ externalRef の稼働中のセッションを使い回す(200)。テナントの今日の利用量が上限に達していれば、文字おこしくんは 429 quota_exceeded を返す(公式の文字おこしくんには、1 日の上限は無い)。文字おこしくんは、API キーで作ったセッションで開いているトラックを、全部の API キー(ほかの利用者のキーを含む)で合わせて数え、同時に 20 本までにする。API キーで作ったセッションのトラックの合計が 20 本に達しているときは、文字おこしくんは、新しい track_open を、Ingest の track_limit_exceeded で断る。公式の Discord ボットが使っているセッションには、音声を送らないこと(1 セッションの Ingest は 1 本だけ)。

パラメーター 場所 型 説明
Idempotency-Key header string 任意 同じ操作を安全にやり直すためのキーを表す(UUID を勧める)。文字おこしくんは、成功した応答を 24 時間覚え、同じキー・同じ本文の要求には最初の応答をそのまま返す(Idempotent-Replayed: true)。同じキーで本文が違えば、文字おこしくんは 422 idempotency_mismatch を返す。同じキーの要求が処理中なら、文字おこしくんは 409 idempotency_in_progress を返す。文字おこしくんは失敗した要求を覚えないので、同じキーでやり直せる。文字おこしくんは、認証に使った資格情報とメソッド・パスごとにキーを区別する。

要求の本文(JSON):

フィールド 型 説明
label string(1〜100 文字) 必須 表示名を表す(前後の空白を除いて 1〜100 文字)。
externalRef string | null(1〜256 文字) 任意 入力元での識別子を表す。reuseActive で使い回すセッションを探す鍵になる。API キーで discord: で始まる値を使うなら、discord:<自分の tenantId>: で始まる値だけを使える(違えば、文字おこしくんは 403 forbidden を返す)。
reuseActive boolean | null 任意 true のとき、同じ externalRef・同じテナントの終了していないセッションがあれば、文字おこしくんはそのセッションを使い回す(200、reused: true)。
tenantId string | null 任意 テナントを表す。クライアントは、テナントを省くか、API キーのテナントと同じ値にする(違えば、文字おこしくんは 403 forbidden を返す)。
{
  "label": "会議室マイク",
  "externalRef": "my-app:room-42",
  "reuseActive": true
}
ステータス 意味
200 文字おこしくんが、稼働中のセッションを使い回した(reused: true)。ラベルは既存のままで、トークンは新しく発行する。
201 文字おこしくんが、セッションを新しく作った。
400 本文か Idempotency-Key が不正である(invalid_json / invalid_request)
401 Authorization: Bearer が無いか、不正である。または、API キーが失効している(unauthorized)
403 tenantId か externalRef が API キーのテナントと違う。または、スコープが足りないか、API キーの利用が無効になっている(forbidden / insufficient_scope / api_access_disabled)
409 文字おこしくんが、同じ Idempotency-Key の前の要求をまだ処理している(idempotency_in_progress)
422 クライアントが、同じ Idempotency-Key で、本文の違う要求を送った(idempotency_mismatch)
429 今日(UTC)の利用上限に達した。または、レート制限を超えた(quota_exceeded / rate_limited)
default ほかのステータスのエラー(problem details)。たとえば 413 payload_too_large・405 method_not_allowed・500 internal・503 shutting_down がある。code で分岐し、知らないコードはステータスで扱う(5xx と 429 は待ってやり直す)。

応答 200:

フィールド 型 説明
sessionId string 必須 セッションの ID を表す。
label string(1〜100 文字) 必須 表示名を表す。
externalRef string(1〜256 文字) 任意 入力元での識別子を表す。公式の Discord ボットは、discord:<guildId>:<channelId> を使う。
tenantId string 任意 テナント(Discord ではギルド ID)を表す。
createdAt number(エポックミリ秒) 必須
reused boolean 必須 既存のセッションを使い回したかどうかを表す(使い回しなら 200、新規なら 201)。
ingest object 必須
ingest.url string 必須 WebSocket の URL(wss://<host>/v1/ingest)を表す。
ingest.token string(セッショントークン) 必須 ingest:write のセッショントークンを表す。クライアントは、このトークンを hello で送る。
subscribe object 必須
subscribe.url string 必須 WebSocket の URL(wss://<host>/v1/subscribe)を表す。
subscribe.token string(セッショントークン) 必須 transcript:read のセッショントークンを表す。クライアントは、このトークンを hello で送る。
viewerUrl string 必須 表示ページの URL を表す(トークンはフラグメント #token= に入る)。
overlayUrl string 必須 OBS のブラウザソース用の URL を表す(viewerUrl + &overlay=1)。
expiresAt number(エポックミリ秒) 必須 2 つのトークンの有効期限を表す。
{
  "sessionId": "0f0c3c1e-5b7a-4d2e-9c61-3f2d8a4b7e10",
  "label": "会議室マイク",
  "externalRef": "my-app:room-42",
  "tenantId": "111111111111111111",
  "createdAt": 1790350826814,
  "reused": true,
  "ingest": {
    "url": "wss://moji.kaitoranma.com/v1/ingest",
    "token": "eyJ2IjoxLCJzaWQiOiIwZjBjM2MxZS01YjdhIiwic2NvcGUiOiJpbmdlc3Q6d3JpdGUifQ.Zr8Tq1wE5yU3iO7pA9sD2fG4hJ6kL0zX1cV3bN5mQ7w"
  },
  "subscribe": {
    "url": "wss://moji.kaitoranma.com/v1/subscribe",
    "token": "eyJ2IjoxLCJzaWQiOiIwZjBjM2MxZS01YjdhIiwic2NvcGUiOiJ0cmFuc2NyaXB0OnJlYWQifQ.Hq3Jx9mK2vLw8ZtN4pR7sYc1dF5gH6jK0lM2nB3vC4x"
  },
  "viewerUrl": "https://moji.kaitoranma.com/#token=eyJ2IjoxLCJzaWQiOiIwZjBjM2MxZS01YjdhIiwic2NvcGUiOiJ0cmFuc2NyaXB0OnJlYWQifQ.Hq3Jx9mK2vLw8ZtN4pR7sYc1dF5gH6jK0lM2nB3vC4x",
  "overlayUrl": "https://moji.kaitoranma.com/#token=eyJ2IjoxLCJzaWQiOiIwZjBjM2MxZS01YjdhIiwic2NvcGUiOiJ0cmFuc2NyaXB0OnJlYWQifQ.Hq3Jx9mK2vLw8ZtN4pR7sYc1dF5gH6jK0lM2nB3vC4x&overlay=1",
  "expiresAt": 1790354426000
}
curl -sS -X POST "$BASE/v1/sessions" \
  -H "Authorization: Bearer $MOJIOKOSHI_API_KEY" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"label":"会議室マイク","externalRef":"my-app:room-42","reuseActive":true}'

セッションの状態を見る(GET /v1/sessions/{sessionId})

文字おこしくんは、セッションの状態と利用量を返す。API キーで別のテナントのセッションを指定すると、文字おこしくんは 404 を返す(存在を漏らさない)。

パラメーター 場所 型 説明
sessionId path string 必須 セッションの ID を指定する。
ステータス 意味
200 文字おこしくんが、セッションの状態を返した。
401 Authorization: Bearer が無いか、不正である。または、API キーが失効している(unauthorized)
403 API キーに transcripts:read のスコープが無い。または、テナントで API キーの利用が無効になっている(insufficient_scope / api_access_disabled)
404 セッションが無い。保持期間を過ぎて消えたセッションと、別のテナントのセッションにも、文字おこしくんはこの応答を返す(session_not_found)
429 レート制限を超えた。Retry-After の秒数の後にやり直す(rate_limited)
default ほかのステータスのエラー(problem details)。たとえば 413 payload_too_large・405 method_not_allowed・500 internal・503 shutting_down がある。code で分岐し、知らないコードはステータスで扱う(5xx と 429 は待ってやり直す)。

応答 200:

フィールド 型 説明
sessionId string 必須 セッションの ID を表す。
label string(1〜100 文字) 必須 表示名を表す。
externalRef string(1〜256 文字) 任意 入力元での識別子を表す。公式の Discord ボットは、discord:<guildId>:<channelId> を使う。
tenantId string 任意 テナント(Discord ではギルド ID)を表す。
createdAt number(エポックミリ秒) 必須
endedAt number(エポックミリ秒) 任意 終了した時刻を表す。セッションが終了していなければ、このフィールドは無い。
live boolean 必須 文字おこしくんのサーバーのプロセスで、セッションが稼働中かどうかを表す。
ingestConnected boolean 必須 Ingest(音声の送り手)がつながっているかどうかを表す。
viewers integer(0 以上) 必須 購読中の Subscribe の接続数を表す。
audioSeconds number(0 以上) 必須 このセッションの利用量を表す。利用量は音声認識にかけた音声の秒数で、小数第 3 位までの値である。文字おこしくんは、利用量をトラックごとに数えて足す。
retainUntil number(エポックミリ秒) 任意 終了したセッションが、保持期間を過ぎて消える時刻を表す。文字おこしくんがセッションを消さないなら、このフィールドは無い。テナント(Discord サーバー)の保持期間が延びると、文字おこしくんは、この時刻を、セッションの終了から延びた保持期間の後まで遅らせる。文字おこしくんは、この時刻を早めない。
{
  "sessionId": "0f0c3c1e-5b7a-4d2e-9c61-3f2d8a4b7e10",
  "label": "#雑談 VC",
  "externalRef": "discord:111111111111111111:222222222222222222",
  "tenantId": "111111111111111111",
  "createdAt": 1790350826814,
  "endedAt": 1790354426814,
  "live": false,
  "ingestConnected": false,
  "viewers": 0,
  "audioSeconds": 1234.567,
  "retainUntil": 1792946426814
}
curl -sS "$BASE/v1/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $MOJIOKOSHI_API_KEY"

セッションを削除する(DELETE /v1/sessions/{sessionId})

文字おこしくんは、終わったセッションの記録(文字起こし・話者・セッションの利用量の内訳)を消す。消した記録は戻らない。文字おこしくんが応答を返した時点で、セッションは一覧・状態・書き出しから消える。文字おこしくんは、記録の本文もすぐに裏で消し切る(ファイルにも残さない)。文字おこしくんは、テナントの利用量の累計を減らさない。そのセッションの Webhook の出来事(session.ended など)をまだ送り終えていなければ、文字おこしくんはその出来事を送らない。稼働中か終了の途中のセッションには、文字おこしくんは 409 session_active を返す(先に POST /v1/sessions/{sessionId}/end でセッションを終える)。

パラメーター 場所 型 説明
sessionId path string 必須 セッションの ID を指定する。
ステータス 意味
204 文字おこしくんが記録を消した(本文なし)。
401 Authorization: Bearer が無いか、不正である。または、API キーが失効している(unauthorized)
403 API キーに transcripts:delete のスコープが無い。または、テナントで API キーの利用が無効になっている(insufficient_scope / api_access_disabled)
404 セッションが無い。消えたセッションと、別のテナントのセッションにも、文字おこしくんはこの応答を返す(session_not_found)
409 セッションが稼働中か、終了の途中である(session_active)
429 レート制限を超えた。Retry-After の秒数の後にやり直す(rate_limited)
default ほかのステータスのエラー(problem details)。たとえば 413 payload_too_large・405 method_not_allowed・500 internal・503 shutting_down がある。code で分岐し、知らないコードはステータスで扱う(5xx と 429 は待ってやり直す)。
curl -sS -X DELETE "$BASE/v1/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $MOJIOKOSHI_API_KEY"

セッションを終える(POST /v1/sessions/{sessionId}/end)

文字おこしくんは、音声認識に送った音声の結果を最後まで受け取ってから(最大約 5 秒)、Subscribe でつないでいる全員に session_ended を送り、Ingest の接続も切る。文字おこしくんは、Ingest の接続を切った後に応答を返す。この操作は冪等で、文字おこしくんは、終了済みのセッションには最初の endedAt を返す。要求に本文は要らない。

パラメーター 場所 型 説明
sessionId path string 必須 セッションの ID を指定する。
ステータス 意味
200 文字おこしくんがセッションを終えた。
401 Authorization: Bearer が無いか、不正である。または、API キーが失効している(unauthorized)
403 API キーに ingest:write のスコープが無い。または、テナントで API キーの利用が無効になっている(insufficient_scope / api_access_disabled)
404 セッションが無い(session_not_found)
429 レート制限を超えた。Retry-After の秒数の後にやり直す(rate_limited)
default ほかのステータスのエラー(problem details)。たとえば 413 payload_too_large・405 method_not_allowed・500 internal・503 shutting_down がある。code で分岐し、知らないコードはステータスで扱う(5xx と 429 は待ってやり直す)。

応答 200:

フィールド 型 説明
sessionId string 必須
endedAt number(エポックミリ秒) 必須 終了した時刻を表す(終了済みなら最初の時刻)。
{
  "sessionId": "0f0c3c1e-5b7a-4d2e-9c61-3f2d8a4b7e10",
  "endedAt": 1790354426814
}
curl -sS -X POST "$BASE/v1/sessions/$SESSION_ID/end" \
  -H "Authorization: Bearer $MOJIOKOSHI_API_KEY"

セッショントークンを発行する(POST /v1/sessions/{sessionId}/tokens)

文字おこしくんは、ブラウザ・OBS・ボットに渡す短期のセッショントークンを発行する。transcript:read のトークンは Subscribe(/v1/subscribe の hello)と書き出しに、ingest:write のトークンは Ingest に使う。API キーで発行するには、transcript:read には transcripts:read のスコープが、ingest:write には ingest:write のスコープが要る。有効期間を指定しなければ 3600 秒になる。最大は 86400 秒である。文字おこしくんには、トークンを 1 つずつ失効させる仕組みが無い。クライアントは、有効期間を短くし、期限が近づいたら新しいトークンを取り直す。終わったセッションにも、クライアントは transcript:read のトークンを発行できる(書き出しと過去のログの表示に使う)。終わったセッションの ingest:write のトークンを要求すると、文字おこしくんは 409 session_ended を返す。

パラメーター 場所 型 説明
sessionId path string 必須 セッションの ID を指定する。
Idempotency-Key header string 任意 同じ操作を安全にやり直すためのキーを表す(UUID を勧める)。文字おこしくんは、成功した応答を 24 時間覚え、同じキー・同じ本文の要求には最初の応答をそのまま返す(Idempotent-Replayed: true)。同じキーで本文が違えば、文字おこしくんは 422 idempotency_mismatch を返す。同じキーの要求が処理中なら、文字おこしくんは 409 idempotency_in_progress を返す。文字おこしくんは失敗した要求を覚えないので、同じキーでやり直せる。文字おこしくんは、認証に使った資格情報とメソッド・パスごとにキーを区別する。

要求の本文(JSON):

フィールド 型 説明
scope "transcript:read" / "ingest:write" 必須 transcript:read(Subscribe と書き出し。API キーでは transcripts:read が要る)か ingest:write(Ingest。API キーでは ingest:write が要る)を指定する。
ttlSeconds integer | null(1〜86400) 任意 有効期間を秒で表す(既定 3600)。
{
  "scope": "transcript:read",
  "ttlSeconds": 3600
}
ステータス 意味
200 文字おこしくんがトークンを発行した。
400 scope・ttlSeconds・Idempotency-Key が不正である(invalid_json / invalid_request)
401 Authorization: Bearer が無いか、不正である。または、API キーが失効している(unauthorized)
403 API キーに、そのトークンを発行するスコープが無い。または、API キーの利用が無効になっている(insufficient_scope / api_access_disabled)
404 セッションが無い(session_not_found)
409 終わったセッションの ingest:write のトークンを要求した。または、文字おこしくんが同じ Idempotency-Key の前の要求をまだ処理している(session_ended / idempotency_in_progress)
422 クライアントが、同じ Idempotency-Key で、本文の違う要求を送った(idempotency_mismatch)
429 レート制限を超えた(rate_limited)
default ほかのステータスのエラー(problem details)。たとえば 413 payload_too_large・405 method_not_allowed・500 internal・503 shutting_down がある。code で分岐し、知らないコードはステータスで扱う(5xx と 429 は待ってやり直す)。

応答 200:

フィールド 型 説明
token string(セッショントークン) 必須 セッショントークンを表す。ブラウザ・OBS に渡してよい(使えるセッションも権限も 1 つだけで、期限がある)。
expiresAt number(エポックミリ秒) 必須 有効期限を表す。文字おこしくんは、接続するときにだけ有効期限を確かめる(つながっている WebSocket は切れない)。
{
  "token": "eyJ2IjoxLCJzaWQiOiIwZjBjM2MxZS01YjdhIiwic2NvcGUiOiJ0cmFuc2NyaXB0OnJlYWQifQ.Hq3Jx9mK2vLw8ZtN4pR7sYc1dF5gH6jK0lM2nB3vC4x",
  "expiresAt": 1790354426000
}
curl -sS -X POST "$BASE/v1/sessions/$SESSION_ID/tokens" \
  -H "Authorization: Bearer $MOJIOKOSHI_API_KEY" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"scope":"transcript:read","ttlSeconds":3600}'

書き出し(Transcripts)

クライアントは、API キーか transcript:read のセッショントークンで、文字起こしを書き出す。クライアントは、API キーで書き出しのリンクと記録を ChatGPT に渡すリンクを作り、資格情報を持たない人にリンクを渡すこともできる。

文字起こしを書き出す(GET /v1/sessions/{sessionId}/transcript)

文字おこしくんは、発話ごとの最新の改訂(final / refined。認識途中は含まない)を startedAt の順に返す。稼働中のセッションでは、その時点までの発話を返す。文字おこしくんは応答を少しずつ流す(chunked)。文字おこしくんの API 全体で同時に流せる書き出しは 2 本までで、超えると、文字おこしくんは 429 rate_limited(Retry-After: 5)を返す。HEAD では、文字おこしくんはヘッダーだけを返す。文字おこしくんは、セッションの終了から保持期間が過ぎた記録を消し、消した記録の要求には 404 を返す。書き出しのリンク(POST /v1/sessions/{sessionId}/downloads が作る)は transcript:download のトークンをクエリの token に載せて、この操作を呼ぶ。リンクで要求すると、文字おこしくんは、Content-Disposition: attachment(ファイル名つき)・Referrer-Policy: no-referrer・X-Robots-Tag: noindex を付ける。リンクに disposition=inline を付けると、文字おこしくんは、Content-Disposition: inline にし、X-Content-Type-Options: nosniff と Content-Security-Policy: default-src 'none'; sandbox も付ける(ブラウザは、応答をページとして表示する)。クエリの token には transcript:download のトークンだけを使える(transcript:read のトークンは使えない)。クライアントは、クエリの token と Authorization ヘッダーを同時に付けない(同時に付けると、文字おこしくんは 400 を返す)。

パラメーター 場所 型 説明
sessionId path string 必須 セッションの ID を指定する。
format query "json" / "text" / "csv"(既定 "json") 任意 json(既定)・text(1 発話 1 行)・csv(見出し 日時,話者,発言 と 1 発話 1 行。Excel で開ける)のどれかを指定する。
tz query string(1 文字以上、既定 "Asia/Tokyo") 任意 text の時刻のタイムゾーンを IANA 名で表す(既定は Asia/Tokyo)。csv の日時と、書き出しのリンクで保存するファイルの名前の日時にも、文字おこしくんはこのタイムゾーンを使う。
disposition query "attachment" / "inline"(既定 "attachment") 任意 書き出しのリンク(クエリの token)で、ブラウザに応答をどう扱わせるかを指定する。attachment(既定)ならファイルとして保存させ、inline ならページとして表示させる。ほかの資格情報では、文字おこしくんは値を確かめるだけで、Content-Disposition を付けない。
ステータス 意味
200 文字おこしくんが、文字起こしを書き出した。文字おこしくんは、format=json なら JSON を、format=text なら [HH:mm:ss] 名前: 本文 の行を、format=csv なら CSV(RFC 4180。先頭に UTF-8 の BOM、改行は CRLF、1 行目は見出し 日時,話者,発言、日時は YYYY-MM-DD HH:mm:ss)を返す。CSV では、文字おこしくんは、=・+・-・@・タブ・CR で始まるセルの先頭に ' を付ける(表計算ソフトに数式として読ませない)。
400 format・tz・disposition のどれかが不正である。または、クエリの token と Authorization ヘッダーを同時に付けた(invalid_request)
401 資格情報が、無いか、無効か、期限切れである(unauthorized)
403 トークンが別のセッションのものである。または、API キーのスコープが足りないか、API キーの利用が無効になっている(forbidden / insufficient_scope / api_access_disabled)
404 セッションが無い。保持期間を過ぎて消えたセッションにも、文字おこしくんはこの応答を返す(session_not_found)
429 レート制限を超えた。または、書き出しが混み合っている(rate_limited)
default ほかのステータスのエラー(problem details)。たとえば 413 payload_too_large・405 method_not_allowed・500 internal・503 shutting_down がある。code で分岐し、知らないコードはステータスで扱う(5xx と 429 は待ってやり直す)。

応答 200:

フィールド 型 説明
sessionId string 必須
label string 必須
speakers object[] 必須 そのセッションに一度でも現れた話者(途中で抜けた人も含む)の最後の情報を表す。
speakers[].speakerId string 必須
speakers[].displayName string(100 文字まで) 必須
speakers[].avatarUrl string(http(s) の URL) 任意
utterances object[] 必須 文字おこしくんは、発話ごとの最新の改訂を、startedAt の昇順に並べる。
utterances[].utteranceId string 必須 WebSocket の utterance と同じ ID を表す(WebSocket の発話と対応づけるのに使える)。
utterances[].trackId string 必須
utterances[].speakerId string 任意 話者を表す。話者の分からない発話には、このフィールドは無い。
utterances[].speakerName string 必須 表示名を表す(空・不明なら代替名)。
utterances[].stage "final" / "refined" 必須 final(音声認識の確定)か refined(整形後)を表す。認識途中(partial)は含まない。
utterances[].text string 必須 本文を表す(整形しなかったら rawText と同じ)。
utterances[].rawText string 必須 整形前の音声認識の結果を表す。
utterances[].startedAt number(エポックミリ秒) 必須 発話の最初の音声を文字おこしくんが受け取った時刻を表す。
{
  "sessionId": "0f0c3c1e-5b7a-4d2e-9c61-3f2d8a4b7e10",
  "label": "#雑談 VC",
  "speakers": [
    {
      "speakerId": "discord:333333333333333333",
      "displayName": "たろう",
      "avatarUrl": "https://cdn.discordapp.com/avatars/333333333333333333/a.png"
    },
    {
      "speakerId": "discord:444444444444444444",
      "displayName": "はなこ"
    }
  ],
  "utterances": [
    {
      "utteranceId": "5e0b8a54-3f21-4c7a-9d0e-7b6a5c4d3e21",
      "trackId": "discord:333333333333333333",
      "speakerId": "discord:333333333333333333",
      "speakerName": "たろう",
      "stage": "refined",
      "text": "こんにちは、今日はよろしくお願いします。",
      "rawText": "こんにちは今日はよろしくお願いします",
      "startedAt": 1790350831814
    },
    {
      "utteranceId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "trackId": "discord:444444444444444444",
      "speakerId": "discord:444444444444444444",
      "speakerName": "はなこ",
      "stage": "final",
      "text": "よろしくお願いします",
      "rawText": "よろしくお願いします",
      "startedAt": 1790350835814
    }
  ]
}

format=text の応答:

[21:03:05] たろう: こんにちは、今日はよろしくお願いします。
[21:03:09] はなこ: よろしくお願いします

format=csv の応答(先頭の BOM は省いて示す):

日時,話者,発言
2026-09-26 21:03:05,たろう,こんにちは、今日はよろしくお願いします。
2026-09-26 21:03:09,はなこ,よろしくお願いします
curl -sS "$BASE/v1/sessions/$SESSION_ID/transcript?format=text&tz=Asia/Tokyo" \
  -H "Authorization: Bearer $MOJIOKOSHI_API_KEY"

書き出しのリンクを作る(POST /v1/sessions/{sessionId}/downloads)

文字おこしくんは、クリックするだけで文字起こしをファイルとして保存できるリンク(CSV・JSON・テキスト)と、記録を ChatGPT に渡すリンク(chatgptUrl)を作る。書き出しのリンクは、transcript:download のトークンをクエリに載せた GET /v1/sessions/{sessionId}/transcript の URL である。chatgptUrl は、同じトークンを載せた GET /v1/sessions/{sessionId}/chatgpt の URL である。transcript:download のトークンでは、書き出しと ChatGPT への転送だけができる(Subscribe には使えない)。クライアントは、リンクを、チャットなどに貼って、資格情報を持たない人に渡してよい。文字おこしくんには、リンクを 1 つずつ失効させる仕組みが無い。有効期間を指定しなければ 3600 秒になる。最大は 86400 秒である。セッションに保持期限(retainUntil)があれば、文字おこしくんは、リンクの有効期限を保持期限より後にしない。保持期限を過ぎたセッションには、文字おこしくんは 404 を返す。クライアントは、稼働中のセッションにもリンクを作れる。リンクを開いた人は、開いた時点までの記録を受け取る。hasUtterances が false なら、書き出しに載る発話がまだ無い。クライアントは、要求の本文を省ける。

パラメーター 場所 型 説明
sessionId path string 必須 セッションの ID を指定する。
Idempotency-Key header string 任意 同じ操作を安全にやり直すためのキーを表す(UUID を勧める)。文字おこしくんは、成功した応答を 24 時間覚え、同じキー・同じ本文の要求には最初の応答をそのまま返す(Idempotent-Replayed: true)。同じキーで本文が違えば、文字おこしくんは 422 idempotency_mismatch を返す。同じキーの要求が処理中なら、文字おこしくんは 409 idempotency_in_progress を返す。文字おこしくんは失敗した要求を覚えないので、同じキーでやり直せる。文字おこしくんは、認証に使った資格情報とメソッド・パスごとにキーを区別する。

要求の本文(JSON):

フィールド 型 説明
ttlSeconds integer | null(1〜86400) 任意 リンクの有効期間を秒で表す(既定 3600)。セッションに保持期限(retainUntil)があれば、文字おこしくんは、有効期限を保持期限より後にしない。
{
  "ttlSeconds": 3600
}
ステータス 意味
200 文字おこしくんが、書き出しのリンクを作った。
400 ttlSeconds か Idempotency-Key が不正である(invalid_json / invalid_request)
401 Authorization: Bearer が無いか、不正である。または、API キーが失効している(unauthorized)
403 API キーに transcripts:read のスコープが無い。または、テナントで API キーの利用が無効になっている(insufficient_scope / api_access_disabled)
404 セッションが無い。保持期間を過ぎたセッションと、別のテナントのセッションにも、文字おこしくんはこの応答を返す(session_not_found)
409 文字おこしくんが、同じ Idempotency-Key の前の要求をまだ処理している(idempotency_in_progress)
422 クライアントが、同じ Idempotency-Key で、本文の違う要求を送った(idempotency_mismatch)
429 レート制限を超えた。Retry-After の秒数の後にやり直す(rate_limited)
default ほかのステータスのエラー(problem details)。たとえば 413 payload_too_large・405 method_not_allowed・500 internal・503 shutting_down がある。code で分岐し、知らないコードはステータスで扱う(5xx と 429 は待ってやり直す)。

応答 200:

フィールド 型 説明
expiresAt number(エポックミリ秒) 必須 リンクの有効期限を表す。どのリンクも同じトークンを使うので、有効期限も同じである。
hasUtterances boolean 必須 リンクを作った時点で、書き出しに載る発話(本文のある発話)が 1 件でもあったかを表す。false なら、クライアントはリンクを人に見せなくてよい。
urls object 必須 形式ごとの書き出しのリンクを表す。リンクは GET /v1/sessions/{sessionId}/transcript?format=<形式>&token=<transcript:download のトークン> の形である。リンクを開いたブラウザは、応答をファイルとして保存する(リンクに &disposition=inline を足すと、ブラウザは応答をページとして表示する)。文字おこしくんは、リンクを開いた時点までの記録を書き出す。
urls.json string 必須 format=json の書き出しのリンクを表す。
urls.text string 必須 format=text の書き出しのリンクを表す。
urls.csv string 必須 format=csv の書き出しのリンクを表す。
chatgptUrl string 必須 記録を ChatGPT に渡すリンク(GET /v1/sessions/{sessionId}/chatgpt?token=<同じトークン>)を表す。リンクを開いたブラウザは、定型文と記録の URL を入れた ChatGPT の新しい会話を開く。公式のサーバーでは、リンクの長さは Discord のリンクボタンの上限(512 文字)に収まる。
{
  "expiresAt": 1790354426000,
  "hasUtterances": true,
  "urls": {
    "json": "https://moji.kaitoranma.com/v1/sessions/0f0c3c1e-5b7a-4d2e-9c61-3f2d8a4b7e10/transcript?format=json&token=eyJ2IjoxLCJzaWQiOiIwZjBjM2MxZS01YjdhIiwic2NvcGUiOiJ0cmFuc2NyaXB0OmRvd25sb2FkIn0.Wm3Kp8Rt2Yv6Nq1Lx4Hc9Jd0Fs5Gb7Tz3Ea8Uw2Qo6r",
    "text": "https://moji.kaitoranma.com/v1/sessions/0f0c3c1e-5b7a-4d2e-9c61-3f2d8a4b7e10/transcript?format=text&token=eyJ2IjoxLCJzaWQiOiIwZjBjM2MxZS01YjdhIiwic2NvcGUiOiJ0cmFuc2NyaXB0OmRvd25sb2FkIn0.Wm3Kp8Rt2Yv6Nq1Lx4Hc9Jd0Fs5Gb7Tz3Ea8Uw2Qo6r",
    "csv": "https://moji.kaitoranma.com/v1/sessions/0f0c3c1e-5b7a-4d2e-9c61-3f2d8a4b7e10/transcript?format=csv&token=eyJ2IjoxLCJzaWQiOiIwZjBjM2MxZS01YjdhIiwic2NvcGUiOiJ0cmFuc2NyaXB0OmRvd25sb2FkIn0.Wm3Kp8Rt2Yv6Nq1Lx4Hc9Jd0Fs5Gb7Tz3Ea8Uw2Qo6r"
  },
  "chatgptUrl": "https://moji.kaitoranma.com/v1/sessions/0f0c3c1e-5b7a-4d2e-9c61-3f2d8a4b7e10/chatgpt?token=eyJ2IjoxLCJzaWQiOiIwZjBjM2MxZS01YjdhIiwic2NvcGUiOiJ0cmFuc2NyaXB0OmRvd25sb2FkIn0.Wm3Kp8Rt2Yv6Nq1Lx4Hc9Jd0Fs5Gb7Tz3Ea8Uw2Qo6r"
}
curl -sS -X POST "$BASE/v1/sessions/$SESSION_ID/downloads" \
  -H "Authorization: Bearer $MOJIOKOSHI_API_KEY" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"ttlSeconds":3600}'

記録を ChatGPT に渡す(GET /v1/sessions/{sessionId}/chatgpt)

文字おこしくんは、クエリの token(書き出しのリンクと同じ transcript:download のトークン)を確かめてから、ブラウザを ChatGPT の新しい会話(https://chatgpt.com/?q=…)へ 302 で転送する。q には定型文「次の URL は、Discord のボイスチャンネルの会話の文字おこしです。内容を読んで、議事録として要約してください。」と、記録をページとして表示する URL(GET /v1/sessions/{sessionId}/transcript?format=text&disposition=inline&token=<同じトークン>)が入る。POST /v1/sessions/{sessionId}/downloads の応答の chatgptUrl が、この操作の URL である。ChatGPT の URL は日本語の定型文を含んで長いので、文字おこしくんは、Discord のリンクボタン(URL は 512 文字まで)に載せられる短い URL を、この操作で ChatGPT の URL に転送する。保持期間を過ぎたセッションには、文字おこしくんは 404 を返す。クライアントは、クエリの token と Authorization ヘッダーを同時に付けない(同時に付けると、文字おこしくんは 400 を返す)。HEAD では、文字おこしくんは同じ応答を本文なしで返す。

パラメーター 場所 型 説明
sessionId path string 必須 セッションの ID を指定する。
ステータス 意味
302 文字おこしくんが、ブラウザを ChatGPT の新しい会話へ転送した(転送先は Location)。
400 クエリの token と Authorization ヘッダーを同時に付けた(invalid_request)
401 クエリの token が、無いか、無効か、期限切れである(unauthorized)
403 トークンが別のセッションのものである(forbidden)
404 セッションが無い。保持期間を過ぎたセッションにも、文字おこしくんはこの応答を返す(session_not_found)
429 レート制限(送信元 IP ごと)を超えた(rate_limited)
default ほかのステータスのエラー(problem details)。たとえば 413 payload_too_large・405 method_not_allowed・500 internal・503 shutting_down がある。code で分岐し、知らないコードはステータスで扱う(5xx と 429 は待ってやり直す)。
curl -sS -o /dev/null -w "%{redirect_url}\n" "$BASE/v1/sessions/$SESSION_ID/chatgpt?token=$DOWNLOAD_TOKEN"

利用量(Usage)

クライアントは、usage:read の API キーで、利用量を見る。

利用量を見る(GET /v1/usage)

文字おこしくんは、API キーのテナントの日ごとの利用量と、今日(UTC)の上限・残りを返す。利用量は音声認識にかけた音声の秒数で、文字おこしくんは利用量をトラックごとに数えて足す。from〜to には両端を含めて最大 366 日を指定できる。

パラメーター 場所 型 説明
from query string 任意 最初の日(UTC、YYYY-MM-DD)を指定する。既定は今日。
to query string 任意 最後の日(UTC、YYYY-MM-DD)を指定する。既定は今日。from 以後の日にする。
ステータス 意味
200 文字おこしくんが、利用量を返した。
400 from / to が、不正か、逆順か、長すぎる(invalid_request)
401 Authorization: Bearer が無いか、不正である。または、API キーが失効している(unauthorized)
403 API キーに usage:read のスコープが無い。または、テナントで API キーの利用が無効になっている(insufficient_scope / api_access_disabled)
429 レート制限を超えた。Retry-After の秒数の後にやり直す(rate_limited)
default ほかのステータスのエラー(problem details)。たとえば 413 payload_too_large・405 method_not_allowed・500 internal・503 shutting_down がある。code で分岐し、知らないコードはステータスで扱う(5xx と 429 は待ってやり直す)。

応答 200:

フィールド 型 説明
tenantId string 必須 テナント(Discord ではギルド ID)を表す。
plan string 必須 プラン名を表す。
from string 必須 UTC の日付(YYYY-MM-DD)を表す。
to string 必須 UTC の日付(YYYY-MM-DD)を表す。
days object[] 必須 文字おこしくんは、from〜to のすべての日(使っていない日は 0)を、日付の昇順に並べる。
days[].date string 必須 UTC の日付(YYYY-MM-DD)を表す。
days[].audioSeconds number(0 以上) 必須 その日の利用量を秒で表す。
totalAudioSeconds number(0 以上) 必須 期間の合計を秒で表す。
today object 必須 文字おこしくんは、範囲によらず、常に今日(UTC)の値と上限を返す。
today.date string 必須 UTC の日付(YYYY-MM-DD)を表す。
today.audioSeconds number(0 以上) 必須 今日の利用量を秒で表す(文字おこしくんがまだ保存していない直近の分も含む)。
today.limitAudioSeconds integer | null(0 以上) 必須 1 日の上限を秒で表す。null なら、日ごとの上限は無い。
today.remainingAudioSeconds number | null(0 以上) 必須 残りを秒で表す(0 未満にならない)。上限が無いなら null になる。
today.exceeded boolean 必須 audioSeconds >= limitAudioSeconds かどうかを表す(上限が無いなら false)。
today.resetsAt number(エポックミリ秒) 必須 次の UTC 0 時(日本時間 9:00)を表す。
{
  "tenantId": "111111111111111111",
  "plan": "level-0",
  "from": "2026-09-24",
  "to": "2026-09-26",
  "days": [
    {
      "date": "2026-09-24",
      "audioSeconds": 0
    },
    {
      "date": "2026-09-25",
      "audioSeconds": 5400.25
    },
    {
      "date": "2026-09-26",
      "audioSeconds": 1234.567
    }
  ],
  "totalAudioSeconds": 6634.817,
  "today": {
    "date": "2026-09-26",
    "audioSeconds": 1234.567,
    "limitAudioSeconds": null,
    "remainingAudioSeconds": null,
    "exceeded": false,
    "resetsAt": 1790467200000
  }
}
curl -sS "$BASE/v1/usage?from=2026-09-24&to=2026-09-26" \
  -H "Authorization: Bearer $MOJIOKOSHI_API_KEY"

WebSocket のエンドポイント(WebSocket)

WebSocket のエンドポイントには Ingest と Subscribe がある。メッセージの一覧は asyncapi.yaml にある。

WebSocket の Ingest(音声を送る)に接続する(GET /v1/ingest)

クライアントは、この操作で、WebSocket の Ingest(音声を送る)に接続する。メッセージの一覧は asyncapi.yaml と WebSocket API リファレンス にある。クライアントは、URL やヘッダーではなく、接続した後の最初のメッセージ hello で ingest:write のセッショントークンを送る。普通の HTTP で要求すると、文字おこしくんは 426 を返す。同時接続数は、文字おこしくんの API 全体で 4,096、同じ送信元から 1,024 までである。超えると、文字おこしくんは upgrade に 503 / 429 の too_many_connections を返す。

ステータス 意味
101 接続が WebSocket に切り替わった。この後のメッセージは asyncapi.yaml にある。
426 クライアントが、WebSocket ではない普通の HTTP で要求した(upgrade_required)
429 同じ送信元からの同時接続が多すぎるので、文字おこしくんが upgrade を断った。または、普通の HTTP での要求がレート制限を超えた(too_many_connections / rate_limited)
503 文字おこしくんの API 全体の同時接続が多すぎる。または、文字おこしくんが停止の途中である(too_many_connections / shutting_down)
const ws = new WebSocket("wss://moji.kaitoranma.com/v1/ingest");
ws.onopen = () => ws.send(JSON.stringify({ type: "hello", protocolVersion: 1, token: INGEST_TOKEN }));
ws.onmessage = (event) => console.log(event.data); // 詳しくは asyncapi.yaml

WebSocket の Subscribe(結果を受け取る)に接続する(GET /v1/subscribe)

クライアントは、この操作で、WebSocket の Subscribe(結果を受け取る)に接続する。メッセージの一覧は asyncapi.yaml と WebSocket API リファレンス にある。クライアントは、URL やヘッダーではなく、接続した後の最初のメッセージ hello で transcript:read のセッショントークンを送る。普通の HTTP で要求すると、文字おこしくんは 426 を返す。同時接続数は、文字おこしくんの API 全体で 4,096、同じ送信元から 1,024 までである。超えると、文字おこしくんは upgrade に 503 / 429 の too_many_connections を返す。

ステータス 意味
101 接続が WebSocket に切り替わった。この後のメッセージは asyncapi.yaml にある。
426 クライアントが、WebSocket ではない普通の HTTP で要求した(upgrade_required)
429 同じ送信元からの同時接続が多すぎるので、文字おこしくんが upgrade を断った。または、普通の HTTP での要求がレート制限を超えた(too_many_connections / rate_limited)
503 文字おこしくんの API 全体の同時接続が多すぎる。または、文字おこしくんが停止の途中である(too_many_connections / shutting_down)
const ws = new WebSocket("wss://moji.kaitoranma.com/v1/subscribe");
ws.onopen = () => ws.send(JSON.stringify({ type: "hello", protocolVersion: 1, token: READ_TOKEN }));
ws.onmessage = (event) => console.log(event.data); // 詳しくは asyncapi.yaml

TypeScript・Python の例

依存なしのクライアント examples/mojiokoshi-http.ts(Node 22.18 以降 / Deno / Bun / tsx)か examples/mojiokoshi_http.py(Python 3.10 以降、標準ライブラリだけ)と、ライブの購読の参照実装 examples/mojiokoshi-subscribe.js か examples/mojiokoshi_subscribe.py(websockets)を同じフォルダに置いて、例を動かします。例のコードは、API キーを環境変数 MOJIOKOSHI_API_KEY から読みます。

TypeScript

// recipes.mts を node recipes.mts で実行する(Node 22.18 以降。または npx tsx recipes.mts。examples/ の 2 つのファイルを同じフォルダに置く)
import { createMojiokoshiHttp } from "./mojiokoshi-http.ts";
import { subscribe, speakerName } from "./mojiokoshi-subscribe.js";

// MOJIOKOSHI_BASE_URL を省くと、公式の文字おこしくん(https://moji.kaitoranma.com)につなぐ
const api = createMojiokoshiHttp({ apiKey: process.env.MOJIOKOSHI_API_KEY!, baseUrl: process.env.MOJIOKOSHI_BASE_URL });

// 1. 自分のセッションを新しい順に一覧する(続きは nextCursor で取る。全部を取るなら allSessions() を使う)
const { sessions } = await api.listSessions({ limit: 5 });
for (const s of sessions) console.log(s.sessionId, s.live ? "live" : "ended", s.label);

// 2. 文字起こしを書き出す(JSON と、1 発話 1 行のテキストで)
const latest = sessions[0];
if (latest !== undefined) {
  const transcript = await api.exportTranscript(latest.sessionId);
  console.log(transcript.utterances.length, "発話");
  console.log(await api.exportTranscriptText(latest.sessionId, "Asia/Tokyo"));
}

// 3. transcript:read のトークンを発行して、ライブで購読する
const [live] = (await api.listSessions({ status: "active", limit: 1 })).sessions;
if (live !== undefined) {
  const { token } = await api.mintReadToken(live.sessionId, 3600); // ブラウザ・OBS には、このトークンだけを渡す
  subscribe({
    url: api.subscribeUrl(),
    token,
    onChange(state, message) {
      if (message.type === "utterance" && message.utterance.stage !== "partial") {
        console.log(`${speakerName(state, message.utterance)}: ${message.utterance.text}`);
      }
    },
    onStatus(status, detail) {
      // 期限(1 時間)の後に再接続すると、文字おこしくんが unauthorized を返し、購読が止まる。長く動かすなら、mintReadToken でトークンを取り直して、subscribe し直す。
      if (status === "failed" || status === "ended") console.error(status, detail?.code ?? "");
    },
  });
}

// 4. 今日(UTC)の利用量を見る
const usage = await api.getUsage();
console.log(`今日 ${(usage.today.audioSeconds / 60).toFixed(1)} 分`);

Python

# recipes.py を python recipes.py で実行する(examples/ の mojiokoshi_http.py と mojiokoshi_subscribe.py を同じフォルダに置く)
import asyncio
import os

from mojiokoshi_http import DEFAULT_BASE_URL, MojiokoshiHttp
from mojiokoshi_subscribe import run  # ライブの購読(pip install websockets)

api = MojiokoshiHttp(os.environ["MOJIOKOSHI_API_KEY"], os.environ.get("MOJIOKOSHI_BASE_URL", DEFAULT_BASE_URL))

# 1. 自分のセッションを新しい順に一覧する(続きは nextCursor を cursor に渡して取る)
page = api.list_sessions(limit=5)
for s in page["sessions"]:
    print(s["sessionId"], "live" if s["live"] else "ended", s["label"])

# 2. 文字起こしを書き出す(JSON と、1 発話 1 行のテキストで)
if page["sessions"]:
    session_id = page["sessions"][0]["sessionId"]
    print(len(api.export_transcript(session_id)["utterances"]), "発話")
    print(api.export_transcript_text(session_id, tz="Asia/Tokyo"))

# 3. 今日(UTC)の利用量を見る
today = api.get_usage()["today"]
print(f"今日 {today['audioSeconds'] / 60:.1f} 分")

# 4. transcript:read のトークンを発行して、ライブで購読する(Ctrl+C で止める)
live = api.list_sessions(status="active", limit=1)["sessions"]
if live:
    token = api.mint_read_token(live[0]["sessionId"], ttl_seconds=3600)["token"]
    asyncio.run(run(api.subscribe_url(), token))