v1
PoC / 契約素案
はじめに

インタビューアプリ API

WebRTC インタビュールーム機能を独立させた新基盤の公開 APIです。minedia-www のサーバーが、Session・参加トークン・録画・チャット履歴などを基盤と連携するためのサーバー間 REST API を定義します。

本書の位置づけ

GitHub Issue #5317 の設計成果物 F(API 契約素案)を可視化したものです。シングルテナント想定(tenant 0 = minedia-www)。本書確定後に OpenAPI 3.1 (YAML) へ落とします。

対象スコープ

プレーン通信認証状態
コントロールプレーンminedia-www サーバー → 基盤(サーバー間 REST)API key本書で定義
イベント通知基盤 → minedia-www(Webhook)HMAC 署名本書で定義

ベース URL

すべてのエンドポイントは /api/v1 配下です。

Base URL
https://interview.minedia.com/api/v1

エンティティ × 公開 API マトリクス

コア 16 エンティティが公開 API でどう露出するかの全体マップです。書き込み系は「調査 → 基盤」の連携点に限定し、ルーム内で発生する事実は読み取り専用 + Webhook で通知します。

エンティティ公開 API での扱い主なエンドポイントWebhook
Session✅ CRUD(作成・逆引き・取得・更新・解放)POSTGETPATCHDEL /sessionsW4 session.ended
Room✅ 取得 + 一括失効(作成は Session に内包)GET /rooms/{room_id}、POST …/revocations
参加トークン✅ 発行・個別失効POST /room-access-tokens
Participant✅ 読み取り + 個別 banGET …/participants、POST …/banW1 / W2
EntryHistory✅ 読み取り(純ログ)GET …/entry-historiesW1 / W2
ChatMessage✅ 読み取り + 削除(運用 purge)GET/DELETE …/chat-messages
RoomEvent✅ 読み取り(純ログ)GET …/room-events
Recording✅ 読み取り + 署名 URL + 削除GET …/recordings、GET …/download-urlW3 recording.status_changed
Transcription✅ 読み取り + 起動POST/GET …/transcriptionsW6 transcription.completed
Handout✅ 登録・一覧・更新・削除POST/GET …/handouts
ConnectionTest✅ 開始・履歴・結果取得POST/GET /connection-testsW5 connection_test.completed
Tenant / User❌ 非公開(基盤内部)
共通仕様

認証

API key はテナント(サーバー)を認証する長命・回転式の資格情報です。ブラウザには絶対に露出させないでください。

すべてのリクエストの Authorization ヘッダに API key を付与します。形式は <key_id>.<secret> です。

認証ヘッダ

PoC では tenant 0 の鍵 1 本のみ。scope はなく「有効 / 無効」の二値です。失効・未指定・不正な鍵はすべて 401 UNAUTHORIZED を返します。

ヘッダ説明
Authorization必須ApiKey <key_id>.<secret>
リクエスト例
curl https://interview.minedia.com/api/v1/sessions \
  -H "Authorization: ApiKey key_live_a1b2.sk_9f8e7d…"
401 レスポンス
{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "API キーが無効です"
  }
}
共通仕様

リソース識別子 / external_ref

基盤の全リソースは 単一のフィールド名 id で識別します。値の形式はリソース種別により UUID 文字列 または 整数 の 2 通りです。テナント側 PK との対応は external_type / external_id のペアで双方向解決します。

命名規約: 識別子は常に id

値が UUID でもフィールド名は id とします(内部 DB カラムは uuid のまま)。関連参照も session_id / room_id のように _id 接尾辞で揃えます。

id の値形式(2 種類)

形式対象リソース
① UUID 文字列通常リソース(Session / Room / Participant / RtcSession / Recording / Transcription / ChatChannel / ChatPost / Handout / ConnectionTest / User / Tenant)"id": "0190a1b2-…"
② 整数高ボリュームのログ系(EntryHistory / ChatMessage / RoomEvent"id": 1001

① UUID 形式について

基盤採番の不透明文字列で、内部キーは UUIDv7(時刻ソート可能)。推測不能なため列挙攻撃の二次防御になり、テナント側はこの文字列をそのまま保持して逆引きに使います。

② 整数 id(ログ系の例外)

高ボリュームなログ系リソース(EntryHistory / ChatMessage / RoomEvent)は UUID を持たず、BIGINT の整数id として露出します(カーソルの内部キーにも使用)。それ以外のリソースが UUID 形式という原則に対する明示的な例外です。

external_ref(テナント側 PK との対応)

external_type / external_id のペアで、物理 FK を張らずにテナント側リソースと双方向解決します。

リソースexternal_type
SessionSlot / ExtemporarySlot{"Slot","123"}
ParticipantUser / Employee / Operator{"User","999"}
HandoutProjectHandout{"ProjectHandout","12"}
共通仕様

テナント分離(L0)

他テナント所有のリソースと、存在しないリソースは、どちらも 404 NOT_FOUND を返します(存在秘匿。403 は返しません)。

PoC は 1 テナントですが、突合コードは最初から実装します。これにより、将来のマルチテナント化時にアクセス制御の挙動が変わらないことを保証します。

404 レスポンス(他テナント / 不存在 共通)
{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "リソースが見つかりません"
  }
}
共通仕様

レスポンス形式

成功・エラーともに success フラグで包むエンベロープ形式です(truffle-survey-v2 準拠)。

成功レスポンス

success: truedata を返します。本書のリソース表現・レスポンス例は原則として data の中身を示します(実レスポンスはエンベロープで包まれます)。

エラーレスポンス

success: falseerror を返します。code は UPPER_CASE の機械可読コード、details[] はフィールド単位のエラー(details[].code も UPPER_CASE)。

包まれない例外

/.well-known/jwks.json(RFC 7517 標準形)と /health / /health/ready(素の liveness / readiness)はエンベロープで包みません。204 No Content はボディなし。

日時フォーマット

ISO 8601 / UTC(例 2026-07-01T06:00:00Z)。

成功エンベロープ
{
  "success": true,
  "data": { "id": "0190a1b2-…" }
}
エラーエンベロープ
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "入力値が不正です",
    "details": [
      {
        "field": "ends_at",
        "code": "BEFORE_STARTS_AT",
        "message": "終了時刻は開始時刻より後である必要があります"
      }
    ]
  }
}
共通仕様

ページネーション

一覧系エンドポイントはカーソルページングです(truffle 準拠)。カーソルは採番時刻順の安定ソートを表す不透明 Base64 文字列で、クライアントは中身を解釈しません。

リクエストパラメータ

パラメータ説明
cursor任意前ページレスポンスの next_cursor をそのまま渡す Base64 トークン。初回は省略
limit任意1〜100(default 20)

レスポンスフィールド

フィールド説明
items[]リソース表現の配列
next_cursor次の cursor に渡す不透明 Base64。終端では null
has_more次ページの有無(boolean)

内部キーは UUID リソースでは UUIDv7(時刻ソート可能)、ログ系では整数 id を用います。各一覧の既定ソート順は各エンティティの節に明記します。

一覧レスポンス
{
  "success": true,
  "data": {
    "items": [ { "id": "0190a1b2-…" } ],
    "next_cursor": "eyJrIjoxMDQyfQ",
    "has_more": true
  }
}
次ページの取得
curl "…/api/v1/sessions?cursor=eyJrIjoxMDQyfQ&limit=20" \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
共通仕様

ステータスコード規約

400(スキーマ違反)と 422(業務ルール違反)を分離します。真の競合は 409 を維持します。

HTTP用途
200 / 201 / 204取得 / 作成 / 削除・失効(冪等)
400スキーマ / パース違反(型・必須・enum・単一フィールドの範囲・ペア指定 / JSON parse 失敗)。INVALID_REQUEST / INVALID_JSON
401API key 不正(UNAUTHORIZED
404不存在 or 他テナント(L0 存在秘匿・NOT_FOUND
409真の状態競合(解放済み・入室実績あり・ban・external_ref 重複など)
413ファイルサイズ超過(FILE_TOO_LARGE
422スキーマ通過後の業務ルール違反(時間窓・録画未完・拡張子など。VALIDATION_ERROR 系)
429レート制限(RATE_LIMIT_EXCEEDED・PoC では未実装・予約)
400 と 422 の境界

型・必須・enum・単一フィールドの数値範囲・ペア指定など スキーマで弾けるもの = 400。スキーマは通るが時間窓・状態・録画進捗などの 意味的 / 横断的ルール違反 = 422409 は「既に存在 / 既にその状態」という競合に限定します。

共通仕様

冪等性(Idempotency-Key)

ネットワーク再送による二重作成を防ぐため、すべての作成系 POST は Idempotency-Key ヘッダを標準採用します(Stripe 型)。

挙動

同一キーの再送は初回レスポンス(ステータス + ボディ)をそのまま返します(処理は 1 回だけ)。キーは Redis に保存(TTL = 24h 仮値)。

ケース挙動
同一キー + 同一ボディ初回レスポンスを再生
同一キー + 異なるボディ422 IDEMPOTENCY_KEY_CONFLICT

対象エンドポイント

すべての作成系 POST(/sessions/room-access-tokens/connection-tests/handouts/transcriptions、ban、revocations)。external_ref 一意などの自然キーは二次防御として併存します。

PoC では未指定リクエストも受理します(移行期互換)。将来必須化の余地あり。

リクエスト例
curl -X POST …/api/v1/sessions \
  -H "Authorization: ApiKey key_live_a1b2.sk_…" \
  -H "Idempotency-Key: 7c9e6679-7425-40de-944b-…" \
  -H "Content-Type: application/json" \
  -d '{ … }'
共通仕様

Webhook(基盤 → minedia-www)

ルーム内で発生した事実は、基盤から minedia-www へ Webhook で push 通知します。全イベント共通のエンベロープと HMAC 署名を持ちます。

署名検証

ヘッダ説明
X-Interview-Signaturesha256=<HMAC-SHA256(webhook_secret, timestamp + "." + body)>
X-Interview-Timestampunix 秒。受信側は署名検証 + 許容差 ±5 分を確認

配信仕様

観点仕様
冪等性id で重複排除(リトライで同一イベント再送あり)
リトライ2xx 以外で指数バックオフ・最大 5 回(1m / 5m / 30m / 2h / 12h)
順序順序保証なし。occurred_at で並べ替えること
スキーマ版数version で破壊的変更を吸収。未知 version は安全に無視 / 隔離

イベント一覧

IDtype説明
W1participant.entered入室(出欠判定材料)
W2participant.left退室
W3recording.status_changed録画状態変化
W4session.endedセッション終了 / 解放
W5connection_test.completed接続テスト完了
W6transcription.completed文字起こし完了
Webhook エンベロープ
{
  "id": "evt_0190a210-…",
  "version": "1",
  "type": "participant.left",
  "occurred_at": "2026-07-01T06:01:12Z",
  "data": { }
}
共通仕様

エラーコード

機械可読な code(UPPER_CASE)と HTTP ステータスの一覧です。各エンドポイント固有のエラーは、それぞれのエンティティ節にも記載します。

HTTPcode意味
400INVALID_REQUEST型・必須・enum・範囲・ペア指定などのスキーマ違反
400INVALID_JSONJSON パース失敗
400UNKNOWN_ROLE未知 / 不正なロール値(基盤がゲートキープ)
401UNAUTHORIZEDAPI key 不正・未指定・失効
404NOT_FOUND不存在 or 他テナント(存在秘匿)
409DUPLICATE_EXTERNAL_REF同一 external_ref の重複作成
409SESSION_ALREADY_STARTED入室実績ありの Session の時間枠変更
409ACTIVE_TOKENS_EXIST有効な参加トークンが存在
409SESSION_RELEASED解放済み Session への操作
409PARTICIPANT_BANNEDban 済み参加者へのトークン再発行
409RECORDING_NOT_AVAILABLE録画が利用可能状態でない
413FILE_TOO_LARGEファイルサイズ超過
422VALIDATION_ERROR業務ルール違反(汎用)
422INVALID_TIME_WINDOW開始 ≧ 終了、過去終了、上限超過などの時間窓違反
422OUTSIDE_TIME_WINDOW入室可能時間外
422UNSUPPORTED_FILE_TYPE許可されない拡張子
422IDEMPOTENCY_KEY_CONFLICT同一キーで異なるボディ
429RATE_LIMIT_EXCEEDEDレート制限(PoC 未実装・予約)
コアリソース

Session

現 Slot / ExtemporarySlot の「時間枠 + アクセストークン + RTC 構成」を汎用化したリソースです。1 Session = 1 Room のため、Room は Session 作成時に同時生成され、レスポンスに内包されます。

設計上の責務分界

RTC 構成・文字起こし構成は「呼び出し側テナントが作成時に渡す入力」です。Project / Slot 設定からの解決は minedia-www 側の責務であり、基盤は受け取った値を保存します。

旧エンティティとの差分(slots / extemporary_slotssessionsクリックで展開
🆕 新設 新基盤で新設(旧に対応列なし) → 移行 旧列をリネーム / 汎用化して移行 ❌ 対象外 新基盤に持ち込まない(調査ドメイン残置 or 廃止)
旧フィールド(現行 minedia-www)種別新フィールド(sessions)備考
🆕 新設id / tenant_id / uuid全コアテーブル共通の3 列をまとめた表記(1 つの識別子ではない)。id=内部 BIGINT 主キー(非公開・コア内 FK 用)/ tenant_id=テナントスコープ(PoC は 0 固定)/ uuid=公開識別子(グローバル一意・API では id として露出)。※DB の id と API の id は別物
slots.id / extemporary_slots.id→ 移行external_type / external_id出所 PK を external_ref 化。例 {"Slot","123"}FK ではない
room_access_token→ 移行access_tokenグローバル一意・ランダム
start_time→ 移行starts_at
end_time→ 移行ends_at
extemporary_slots.name→ 移行name任意
(As-Is は中止を調査側 AnswerSlot.status のみで保持)🆕 新設statusscheduled / active / cancelled / closed。Room 解放・入室可否・締切を基盤が単一根拠で判定
🆕 新設rtc_engineopentok / livekit / zoom。新基盤の既定は LiveKit
extemporary_slots.archive_mode / Project 設定→ 移行archive_mode呼び出し側が作成時に渡す入力に統一
archive_resolution(Project / 即席)→ 移行archive_resolution640x480 / 1280x720
stream_resolution(Project / 即席)→ 移行stream_resolution
stream_frame_rate(Project / 即席)→ 移行stream_frame_rate30 / 15 / 7 / 1
slots / extemporary_slots.transcription_language→ 移行transcription_language当初「調査残置」→ G5 で文字起こしコア化に伴いコアへ(default ja-JP
Project.transcription_service→ 移行transcription_service自動文字起こしの既定エンジン(openai 等)
slots.project_id / limit / segment / memo、extemporary_folder_id / memo❌ 対象外— (調査に残置)パネル募集・定員・セグメント・即席フォルダ・メモは調査ドメインの語彙
created_at / updated_at→ 移行同左

Session オブジェクト

idstring · UUID 形式
NOT NULL
基盤採番の一意識別子(不透明文字列・内部キーは UUIDv7)
external_typestring
NOT NULL
テナント側リソース種別。tenant 0 では Slot / ExtemporarySlot
external_idstring
NOT NULL
テナント側 PK(整数も文字列化)
namestring
nullable
表示名(未設定時は null
access_tokenstring
NOT NULL
Session 単位のランダムトークン(移行期互換用。参加認可の本線は JWT)
starts_atdatetime
NOT NULL
枠の開始時刻(ISO 8601 / UTC)
ends_atdatetime
NOT NULL
枠の終了時刻
rtc_engineenum
NOT NULL
opentok / livekit / zoom。新基盤の既定は livekit
archive_modeenum
NOT NULL
always / manual(既定 manual
archive_resolutionenum
NOT NULL
640x480 / 1280x720(既定 640x480
stream_resolutionenum
NOT NULL
320x240 / 640x480 / 1280x720(既定 320x240
stream_frame_rateinteger
NOT NULL
30 / 15 / 7 / 1(既定 15
transcription_languagestring
NOT NULL
文字起こし言語(BCP 47・既定 ja-JP)。録画完了後の自動文字起こしの既定言語
transcription_serviceenum
NOT NULL
eleven_labs / openai / azure / anyenv / rimo(既定 openai
statusenum
NOT NULL
scheduled(開始前)/ active(時間窓内)/ ended(時間窓終了)/ released(解放済み)
roomobject
NOT NULL
内包される Room(id / session_id / token_epoch / connect_url)。1 Session = 1 Room で常に同時生成
created_atdatetime
NOT NULL
作成日時
Session オブジェクト
{
  "id": "0190a1b2-…",
  "external_type": "Slot",
  "external_id": "123",
  "name": null,
  "access_token": "k3jH9x…",
  "starts_at": "2026-07-01T05:00:00Z",
  "ends_at": "2026-07-01T06:00:00Z",
  "rtc_engine": "livekit",
  "archive_mode": "always",
  "archive_resolution": "640x480",
  "stream_resolution": "640x480",
  "stream_frame_rate": 15,
  "transcription_language": "ja-JP",
  "transcription_service": "openai",
  "status": "scheduled",
  "room": {
    "id": "0190a1b3-…",
    "session_id": "0190a1b2-…",
    "token_epoch": 0,
    "connect_url": "https://interview.minedia.com/rooms/0190a1b3-…"
  },
  "created_at": "2026-06-10T09:00:00Z"
}

Session を作成する

POST/api/v1/sessions

minedia-www の Project / Slot 作成時(webrtc_engine = 新App 選択時)と即席調査(ExtemporarySlot)作成時に呼びます。Room が同時生成され、レスポンスに内包されます。

ボディパラメータ

パラメータ説明
external_type必須
string(≤64)
tenant 0: Slot(本番)/ ExtemporarySlot(即席)
external_id必須
string(≤64)
テナント側 PK(整数も文字列化)
starts_at必須
datetime
枠の開始時刻
ends_at必須
datetime
枠の終了時刻
name任意
string(≤255)
表示名
rtc_engine任意
enum
opentok / livekit / zoom(default livekit
archive_mode任意
enum
always / manual(default manual
archive_resolution任意
enum
640x480 / 1280x720(default 640x480
stream_resolution任意
enum
320x240 / 640x480 / 1280x720(default 320x240
stream_frame_rate任意
enum(int)
30 / 15 / 7 / 1(default 15
transcription_language任意
string(≤16)
文字起こし言語(BCP 47・default ja-JP
transcription_service任意
enum
文字起こしエンジン(default openai

エラー

条件レスポンス
external_type / external_id 必須・ペア指定 / enum 不正400 INVALID_REQUEST
[tenant_id, external_type, external_id] 重複409 DUPLICATE_EXTERNAL_REF
starts_at ≧ ends_at / ends_at が過去 / 枠が 24h 超422 INVALID_TIME_WINDOW

成功時は 201 CreatedLocation: /api/v1/sessions/{session_id} を返します。

リクエスト
curl -X POST …/api/v1/sessions \
  -H "Authorization: ApiKey key_live_a1b2.sk_…" \
  -H "Idempotency-Key: 7c9e6679-…" \
  -H "Content-Type: application/json" \
  -d '{
    "external_type": "Slot",
    "external_id": "123",
    "starts_at": "2026-07-01T05:00:00Z",
    "ends_at": "2026-07-01T06:00:00Z",
    "rtc_engine": "livekit",
    "archive_mode": "always"
  }'
201 レスポンス
{
  "id": "0190a1b2-…",
  "external_type": "Slot",
  "external_id": "123",
  "access_token": "k3jH9x…",
  "starts_at": "2026-07-01T05:00:00Z",
  "ends_at": "2026-07-01T06:00:00Z",
  "rtc_engine": "livekit",
  "archive_mode": "always",
  "status": "scheduled",
  "room": {
    "id": "0190a1b3-…",
    "token_epoch": 0,
    "connect_url": "https://interview.minedia.com/rooms/0190a1b3-…"
  },
  "created_at": "2026-06-10T09:00:00Z"
}

Session を一覧 / 逆引きする

GET/api/v1/sessions

テナント側 PK(Slot.id 等)から基盤の Session を逆引きします(「テナント → 基盤」方向の双方向解決)。既定ソートは created_at 降順です。

クエリパラメータ

パラメータ説明
external_type任意逆引きキー(external_id とペア指定)
external_id任意逆引きキー(external_type とペア指定)
cursor任意ページングカーソル
limit任意1〜100(default 20)

エラー

条件レスポンス
external_type / external_id の片方のみ指定400 INVALID_REQUEST
リクエスト
curl "…/api/v1/sessions?external_type=Slot&external_id=123" \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
200 レスポンス
{
  "items": [
    { "id": "0190a1b2-…", "external_type": "Slot", "external_id": "123", "status": "scheduled" }
  ],
  "next_cursor": null,
  "has_more": false
}

Session を取得する

GET/api/v1/sessions/{session_id}

id(UUID 形式)で 1 件取得します。Room を内包したリソース表現を返します。

エラー

条件レスポンス
不存在 or 他テナント404 NOT_FOUND
リクエスト
curl …/api/v1/sessions/0190a1b2-… \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
200 レスポンス
{
  "id": "0190a1b2-…",
  "external_type": "Slot",
  "external_id": "123",
  "status": "active",
  "room": { "id": "0190a1b3-…", "token_epoch": 0 }
}

Session を更新する

PATCH/api/v1/sessions/{session_id}

作成時のフィールドを部分更新します(external_type / external_id は変更不可)。JSON Merge Patch(RFC 7396)準拠で、リクエストにキーが存在するフィールドだけ更新します。

更新セマンティクス

リクエストでの現れ方挙動
キーを省略そのフィールドは変更しない
キーあり・値 nullクリア(null 化)。nullable なのは name のみ
キーあり・値ありその値に更新(作成時のバリデーション適用)

エラー

条件レスポンス
starts_at / ends_at / rtc_engine 変更時、入室実績(EntryHistory)が 1 件以上409 SESSION_ALREADY_STARTED
同上の変更時、有効な参加トークンが存在409 ACTIVE_TOKENS_EXIST
解放済み(released)Session409 SESSION_RELEASED
時間窓 / enum 制約違反400 / 422

割付後リスケ不可などは調査ドメイン側の業務ルールです。基盤はトークン・入室実績との整合のみ強制します。

リクエスト
curl -X PATCH …/api/v1/sessions/0190a1b2-… \
  -H "Authorization: ApiKey key_live_a1b2.sk_…" \
  -H "Content-Type: application/merge-patch+json" \
  -d '{
    "ends_at": "2026-07-01T06:30:00Z",
    "name": "コンセプトA 追加ヒアリング"
  }'
200 レスポンス
{
  "id": "0190a1b2-…",
  "name": "コンセプトA 追加ヒアリング",
  "ends_at": "2026-07-01T06:30:00Z",
  "status": "scheduled"
}

Session を解放する

DELETE/api/v1/sessions/{session_id}

パネルのキャンセル・欠席時に、先行作成した Session / Room を解放します。

副作用

対象挙動
参加トークンRoom の token_epoch を +1 し、発行済み参加トークンを全失効
接続中の参加者メディア切断
既発生の事実データ録画・チャット・入退室履歴は削除しない(事後参照可能)
WebhookW4 session.endedreason: "released")を送信

成功時は 204 No Content。解放済みへの再実行は冪等で 204 を返します。

リクエスト
curl -X DELETE …/api/v1/sessions/0190a1b2-… \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
レスポンス
204 No Content

Webhook: session.ended

W4session.ended

セッションの時間窓終了、またはテナントによる解放時に送信されます。振り返りフェーズ開始の起点として利用できます。

reason意味
expired時間窓の自然終了(entity 側 closed
releasedテナントによる解放・キャンセル(entity 側 cancelled

API 露出名 ended / released と entity-design 側の closed / cancelled の語彙対応は、OpenAPI 化時に一本化予定です。

Webhook ペイロード
{
  "id": "evt_0190a210-…",
  "version": "1",
  "type": "session.ended",
  "occurred_at": "2026-07-01T06:00:00Z",
  "data": {
    "session_id": "0190a1b2-…",
    "session_external_ref": { "external_type": "Slot", "external_id": "123" },
    "room_id": "0190a1b3-…",
    "reason": "expired",
    "ended_at": "2026-07-01T06:00:00Z"
  }
}
コアリソース

Room

WebRTC の接続先となる RTC ルーム(集約ルート)です。1 Session = 1 Room で、Session 作成時に同時生成され Session に従属します。単体の作成 / 削除 API はなく、取得トークンの一括失効のみを公開します。

作成・解放は Session に内包

Room の生成は Session 作成に、解放は Session 解放に内包されます。本リソースは、生成済みの Room を id で取得すること、および発行済み参加トークンを世代単位で一括失効させること(強制退室)に責務を限定します。

旧エンティティとの差分(roomsroomsクリックで展開
🆕 新設 新基盤で新設 → 移行 リネーム / 汎用化して移行 ❌ 対象外 調査残置 or 廃止
旧フィールド種別新フィールド備考
🆕 新設id / tenant_id / uuid全コアテーブル共通の3 列をまとめた表記id=内部 BIGINT 主キー(非公開)/ tenant_id=テナントスコープ/ uuid=公開識別子(API では id として露出)
slot_id / extemporary_slot_id(二股FK)→ 移行session_id二股 FK を sessions.id へ一本化。出所の多態は Session.external_type が吸収(コア内部 FK)
stream_resolution→ 移行stream_resolutiondefault 320x240
stream_frame_rate→ 移行stream_frame_ratedefault 15
🆕 新設token_epochRoom 単位の参加トークン世代。一括失効(revocations)/ Session 解放で +1 し旧世代を全失効。JWT epoch クレームと突合
created_at / updated_at→ 移行同左

Room オブジェクト

idstring · UUID 形式
NOT NULL
基盤採番の一意識別子(不透明文字列・内部キーは UUIDv7)
session_idstring · UUID 形式
NOT NULL
この Room が従属する Sessionid。1 Session = 1 Room
stream_resolutionenum
NOT NULL
320x240 / 640x480 / 1280x720(既定 320x240)。Session 作成時の入力を継承
stream_frame_rateinteger
NOT NULL
30 / 15 / 7 / 1(既定 15
token_epochinteger
NOT NULL
参加トークンの世代。一括失効(revocations)・Session 解放のたびに +1 され、旧世代で発行済みの全参加トークンが無効化されます(既定 0
connect_urlstring
NOT NULL
ルーム接続 URL。参加クライアントが WebRTC 接続を確立するためのエンドポイント
created_atdatetime
NOT NULL
作成日時(ISO 8601 / UTC)
Room オブジェクト
{
  "id": "0190a1b3-…",
  "session_id": "0190a1b2-…",
  "stream_resolution": "640x480",
  "stream_frame_rate": 15,
  "token_epoch": 0,
  "connect_url": "https://interview.minedia.com/rooms/0190a1b3-…",
  "created_at": "2026-06-10T09:00:00Z"
}

Room を取得する

GET/api/v1/rooms/{room_id}

id(UUID 形式)で Room を 1 件取得します。現在の token_epoch や接続先 URL を参照する用途に使います。

エラー

条件レスポンス
不存在 or 他テナント404 NOT_FOUND
リクエスト
curl …/api/v1/rooms/0190a1b3-… \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
200 レスポンス
{
  "id": "0190a1b3-…",
  "session_id": "0190a1b2-…",
  "stream_resolution": "640x480",
  "stream_frame_rate": 15,
  "token_epoch": 0,
  "connect_url": "https://interview.minedia.com/rooms/0190a1b3-…",
  "created_at": "2026-06-10T09:00:00Z"
}

トークンを一括失効する

POST/api/v1/rooms/{room_id}/revocations

「このルームの全員を蹴る」操作です。Room の token_epoch を +1 し、発行済みの全参加トークンを一括で無効化します。接続中の参加者はメディアを切断されます。Session は解放しません(解放を伴う場合は Session 解放を使います)。

revocation は名詞リソースとしてモデル化しています(token_epoch の世代を 1 つ進める作成操作)。単体取得エンドポイントは持たないため、Location は影響先の Room を指します。再送防止のため Idempotency-Key ヘッダに対応します。

ボディパラメータ

パラメータ説明
reason必須
string(1..255)
失効理由。RoomEventroom.tokens_revoked)として監査記録されます

エラー

条件レスポンス
reason 必須 / 長さ違反400 INVALID_REQUEST
不存在 or 他テナント404 NOT_FOUND

成功時は 201 CreatedLocation: /api/v1/rooms/{room_id} を返します。ボディには更新後の token_epoch を含みます。

リクエスト
curl -X POST …/api/v1/rooms/0190a1b3-…/revocations \
  -H "Authorization: ApiKey key_live_a1b2.sk_…" \
  -H "Idempotency-Key: 7c9e6679-…" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "進行終了のため全員を退室"
  }'
201 レスポンス
{
  "room_id": "0190a1b3-…",
  "token_epoch": 3
}
コアリソース

参加トークン

永続エンティティではなく発行物です。発行の副作用として Participant が生成・紐付けされます。現行の room_access_token(Slot 単位)と force_access_token(緊急バイパス)は本 API に収斂します(DEC-05)。

委譲信頼・Model A

テナントのサーバーが、自テナントで認証済みの参加者を participant に変換して発行を依頼します(委譲信頼・Model A)。基盤はテナントの認証結果を信頼し、participant トークン(JWT)を発行します。署名は EdDSAkid 付き)で、公開鍵は JWKS(/.well-known/jwks.json)から取得します。

トークン発行レスポンス

tokenstring
NOT NULL
署名済み participant JWT(EdDSA・kid 付き)。ルーム接続時にこのトークンで認可
jtistring
NOT NULL
トークン一意 ID。個別失効(§ DELETE /room-access-tokens/{jti})の単体アドレス
not_beforedatetime
NOT NULL
トークンが有効になる時刻(JWT nbf に対応・ISO 8601 / UTC)
expires_atdatetime
NOT NULL
トークンの失効時刻(JWT exp に対応)
room_connect_urlstring
NOT NULL
参加者ブラウザがルームへ接続する URL
participantobject
NOT NULL
発行の副作用で生成・紐付けされた Participant。id / room_id / role / display_name / external_type / external_id を内包(匿名参加者は external_type / external_idnull

JWT クレーム

発行される token に含まれるクレームです。署名は EdDSA(kid 付き・公開鍵は JWKS)。

クレーム説明
iss発行者(基盤)
aud受信者(ルームランタイム)
sub主体(participant)
tenantテナント(PoC は 0 固定)
room対象 Room の id
participantParticipant の id
rolepanelist / moderator / observer / minedia_observer
name表示名(発行時に封入・以後変更不可。なりすまし防止)
exp失効時刻(expires_at
nbf有効開始時刻(not_before
iat発行時刻
jtiトークン一意 ID(個別失効の検証キー)
epoch発行時点の Room 世代(token_epoch)。一括失効の検証に使用
pepoch発行時点の Participant 世代。ban の再入室阻止検証に使用(G1)
発行レスポンス
{
  "token": "eyJhbGciOiJFZERTQSIsImtpZCI6ImludGVydmlldy0yMDI2LTA2In0...",
  "jti": "8f14e45f-...",
  "not_before": "2026-07-01T04:30:00Z",
  "expires_at": "2026-07-01T05:15:00Z",
  "room_connect_url": "https://interview.minedia.com/rooms/0190a1b3-...",
  "participant": {
    "id": "0190a1d0-...",
    "room_id": "0190a1b3-...",
    "role": "panelist",
    "display_name": "山田太郎",
    "external_type": "User",
    "external_id": "999"
  }
}

トークンを発行する

POST /api/v1/room-access-tokens

テナントのサーバーが、委譲信頼で自テナントの認証済み参加者の発行を依頼します(Model A)。Idempotency-Key ヘッダに対応します。緊急発行は force: true で時間窓チェックをスキップします。

リクエストボディ

フィールド説明
room_id必須
uuid
対象ルーム
role必須
enum
panelist / moderator / observer / minedia_observer(DEC-03 正典)
display_name必須
string · 1..64
表示名。発行時に JWT の name クレームへ封入(以後変更不可・なりすまし防止)
participant.external_type任意
string · ≤64
参加者のテナント側 identity。tenant 0: User / Employee / Operator
participant.external_id任意
string · ≤64
同上。両方 null =匿名参加者(即席調査の匿名入室を許容)
ttl_seconds任意
integer
60〜3600(既定 900)
force任意
boolean
緊急発行(既定 false)。時間窓チェックをスキップ

クレーム導出規則

クレーム通常force: true
nbfmax(now, starts_at − 30min)now
expmin(now + ttl_seconds, ends_at + 30min)now + ttl_seconds
epoch発行時点の Room token_epoch同左
pepoch発行時点の Participant token_epoch(ban 検証用・G1)同左

エラー

条件レスポンス
自テナント外の room_id(不存在含む・存在秘匿)404 NOT_FOUND
未知 / 不正な role(基盤がゲートキープ)400 UNKNOWN_ROLE
display_name / ペア指定 / ttl_seconds 範囲などのスキーマ違反400 INVALID_REQUEST
時間窓外(force: true のみスキップ)422 OUTSIDE_TIME_WINDOW
解放済み Session の Room(force でも不可409 SESSION_RELEASED
ban 済み Participant への再発行(force でも不可・G1)409 PARTICIPANT_BANNED

レスポンスは 201 CreatedLocation: /api/v1/room-access-tokens/{jti})。

リクエスト例
curl -X POST "https://interview.minedia.com/api/v1/room-access-tokens" \
  -H "Authorization: ApiKey key_live_a1b2.sk_9f8e7d…" \
  -H "Idempotency-Key: 7c9e6679-7425-40de-944b-…" \
  -H "Content-Type: application/json" \
  -d '{
    "room_id": "0190a1b3-…",
    "role": "panelist",
    "display_name": "山田太郎",
    "participant": { "external_type": "User", "external_id": "999" },
    "ttl_seconds": 900
  }'
201 Created
{
  "token": "eyJhbGciOiJFZERTQSIsImtpZCI6ImludGVydmlldy0yMDI2LTA2In0...",
  "jti": "8f14e45f-...",
  "not_before": "2026-07-01T04:30:00Z",
  "expires_at": "2026-07-01T05:15:00Z",
  "room_connect_url": "https://interview.minedia.com/rooms/0190a1b3-...",
  "participant": {
    "id": "0190a1d0-...",
    "room_id": "0190a1b3-...",
    "role": "panelist",
    "display_name": "山田太郎",
    "external_type": "User",
    "external_id": "999"
  }
}

トークンを個別失効する

DELETE /api/v1/room-access-tokens/{jti}

jti をブラックリスト(Redis・TTL =残存 exp)に登録し、その 1 トークンだけを取り消します(誤発行の撤回・一時的な切断)。当該トークンで接続中なら切断します。

ban との使い分け(G1)

jti 失効は再発行で素通りします(発行は新しい jti を返すため)=再入室は阻止しません。恒久的に締め出すには Participant の bantoken_epoch++ + status: banned)を使ってください。jti 失効=一時的取り消し、ban=恒久的締め出し、という役割分担です。

エラー

条件レスポンス
他テナント発行 / 不存在の jti(存在秘匿)404 NOT_FOUND

レスポンスは 204 No Content(期限切れ済みへの実行も冪等で 204)。自テナント発行の jti のみ対象です。

リクエスト例
curl -X DELETE "https://interview.minedia.com/api/v1/room-access-tokens/8f14e45f-…" \
  -H "Authorization: ApiKey key_live_a1b2.sk_9f8e7d…"
204 レスポンス
204 No Content
コアリソース

Participant

「参加者 × 役割」を Room 単位で束縛したリソースです。生成は参加トークン発行(§4.3.1)の副作用であり、直接の作成 API はありません。公開 API は読み取り+個別 banのみで、リアルタイムの参加者一覧はランタイム側が担います。

identity-less 原則

参加者の身元はテナントが保証し、基盤は per-room の役割束縛・external_ref・表示名スナップショットだけを持ちます。テナント委譲の参加者は external_type / external_id、基盤運用者(minedia_observer)は内部 user_id で表現し、両系統は排他です(どちらも null=匿名入室を許容)。

旧エンティティとの差分(現行の entry_role / chat 由来 → participantsクリックで展開
🆕 新設 新基盤で新設 → 移行 リネーム/汎用化して移行 ❌ 対象外 調査残置 or 廃止
旧フィールド種別新フィールド備考
—(専用テーブルなし。参加者 × ルームを初めてテーブル化)🆕 新設id / tenant_id / uuid全コアテーブル共通の3 列をまとめた表記(1 つの識別子ではない)。id=内部 BIGINT 主キー(非公開・コア内 FK 用)/ tenant_id=テナントスコープ(PoC は 0 固定)/ uuid=公開識別子(グローバル一意・API では id として露出
🆕 新設room_idコア内部 FK → rooms.id(NOT NULL)。参加者を所属 Room に束縛
room_entry_histories.entry_role / URL param interview_role→ 移行rolemoderator / observer / panelist / minedia_observer。クライアント申告 param → 署名トークン由来へ(DEC-03)
🆕 新設user_idコア内部 FK → users.id(nullable)。基盤運用者(minedia_observer)の入室時のみ台帳に紐づく
room_chat_messages.user_id / employee_id(物理 FK・identity の出所)→ 移行external_type / external_idテナント側 identity。FK ではない(identity FK を external_ref に置換)。匿名は null
room_entry_histories.name / room_chat_messages.sender_name→ 移行display_name表示名スナップショット
—(As-Is は強制退出を OpenTok 直叩き・記録なし)🆕 新設statusactive / kicked / banned再入室阻止の単一根拠(§7-1 P5・G1)
🆕 新設kicked_at強制退出 / BAN の発生時刻(nullable・監査用)
🆕 新設token_epochparticipant JWT の世代。強制退出時に +1 → 旧 epoch のトークンを失効させ再入室を物理阻止(G1)

Participant オブジェクト

idstring · UUID 形式
NOT NULL
基盤採番の一意識別子(不透明文字列)
room_idstring · UUID 形式
NOT NULL
所属 Room の id。参加者は Room 単位で束縛される
roleenum
NOT NULL
moderator / observer / panelist / minedia_observer。署名トークン由来
display_namestring
NOT NULL
入室時点の表示名スナップショット
external_typestring
nullable
テナント側リソース種別(例 User / Employee)。匿名入室時は null
external_idstring
nullable
テナント側 PK(整数も文字列化)。匿名入室時は null
statusenum
NOT NULL
active / kicked / banned。再入室阻止の単一根拠
token_epochinteger
NOT NULL
participant 単位の JWT 世代(G1)。ban のたびに +1
kicked_atdatetime
nullable
強制退出 / ban の発生時刻(ISO 8601 / UTC)。未発生時は null
created_atdatetime
NOT NULL
作成日時(=初回入室トークン発行時刻)
Participant オブジェクト
{
  "id": "0190a1d0-…",
  "room_id": "0190a1b3-…",
  "role": "panelist",
  "display_name": "山田太郎",
  "external_type": "User",
  "external_id": "999",
  "status": "active",
  "token_epoch": 0,
  "kicked_at": null,
  "created_at": "2026-07-01T04:58:01Z"
}

Participant を一覧する

GET/api/v1/rooms/{room_id}/participants

参加者 × 役割の束縛の事後参照です。見学者一覧・参加実績の突合に使います(リアルタイム一覧はランタイム側)。既定ソートは created_at 昇順です。

クエリパラメータ

パラメータ説明
role任意役割で絞り込み(moderator / observer / panelist / minedia_observer
status任意状態で絞り込み(active / kicked / banned
cursor任意ページングカーソル
limit任意1〜100(default 20)

エラー

条件レスポンス
room_id が不存在 or 他テナント所有404 NOT_FOUND
role / status の enum 不正400 INVALID_REQUEST
リクエスト
curl "…/api/v1/rooms/0190a1b3-…/participants?role=panelist&status=active" \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
200 レスポンス
{
  "items": [
    {
      "id": "0190a1d0-…",
      "room_id": "0190a1b3-…",
      "role": "panelist",
      "display_name": "山田太郎",
      "external_type": "User",
      "external_id": "999",
      "status": "active",
      "token_epoch": 0,
      "kicked_at": null,
      "created_at": "2026-07-01T04:58:01Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}

Participant を ban する

POST/api/v1/rooms/{room_id}/participants/{participant_id}/ban

特定参加者を再入室不可にします。Room 一括失効が「全員を蹴る」のに対し、これは1 人だけを恒久的に締め出す操作です(観察者会議・進行中ルームで他者を巻き込まない)。Idempotency-Key に対応します。

副作用

対象挙動
当該 Participanttoken_epoch を +1 し、statusbanned に更新
参加トークン発行済みの当該 participant トークンを全失効(再発行されても無効)
接続中の参加者メディア切断
監査RoomEventparticipant.banned・payload に participant_id / 理由)として監査記録

ボディパラメータ

パラメータ説明
reason必須
string(1..255)
ban 理由(監査用)

エラー

条件レスポンス
room_id / participant_id が不存在 or 他テナント所有404 NOT_FOUND
reason 欠落 / 範囲外(1..255)400 INVALID_REQUEST

成功時は 200 OK。ban 済みへの再実行は冪等で 200 を返します。

G1: 再入室阻止の実効化

JWT は Room の epoch に加え participant 単位の pepoch クレームを持ち、connect 時に runtime が「Room.token_epoch == jwt.epoch かつ Participant.token_epoch == jwt.pepoch かつ status != banned」を検証します。これにより、ban 後に §4.3.1 で再発行されたトークン(新 jti)でも入室を拒否できます(jti ブラックリスト単独では再発行で素通りしてしまう問題の解消)。

リクエスト
curl -X POST …/api/v1/rooms/0190a1b3-…/participants/0190a1d0-…/ban \
  -H "Authorization: ApiKey key_live_a1b2.sk_…" \
  -H "Idempotency-Key: 7c9e6679-…" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "規約違反のため強制退出"
  }'
200 レスポンス
{
  "participant_id": "0190a1d0-…",
  "status": "banned",
  "token_epoch": 1
}
コアリソース

EntryHistory

ルームへの入退室+デバイス計測の純ログです(書き込みはランタイム側のみ)。出欠判定(is_attend)の材料となりますが、判定ロジックは調査ドメイン側の責務であり、基盤は事実のみを返します。

ログ系の識別子例外(C4)

EntryHistory は高ボリュームのログ系リソースのため、idUUID ではなく BIGINT の整数です(コア共通の UUID 規約に対する明示的な例外。カーソルの内部キーにも使用)。フィールド名は他リソースと同じく id ですが、値の形式のみ整数である点に注意してください。

旧エンティティとの差分(room_entry_historiesentry_historiesクリックで展開
🆕 新設 新基盤で新設(旧に対応列なし) → 移行 旧列をリネーム / 汎用化して移行 ❌ 対象外 新基盤に持ち込まない(調査ドメイン残置 or 廃止)
旧フィールド(現行 minedia-www)種別新フィールド(entry_histories)備考
🆕 新設id / tenant_idログ系は uuid を持たない純ログ。id=BIGINT 主キー(API でも整数 id として露出・C4 例外)/ tenant_id=テナントスコープ(PoC は 0 固定)
room_id🆕 新設room_id旧 integer FK → 新 bigint FK(rooms.id
🆕 新設participant_idFK(participants.id)。匿名入室を許容するため nullable
name→ 移行name表示名スナップショット
entry_role→ 移行roleinteger enum → 同語彙の文字列化moderator / observer / panelist / minedia_observer
browser_name / browser_version / platform_name / platform_version / ip_address→ 移行同左デバイス計測。現状踏襲
(As-Is は退室時刻列なし=created_at のみ)🆕 新設exited_at退室時刻(disconnect webhook で更新・nullable)。入退室をペアで記録API では left_at として露出
🆕 新設duration_sec滞在秒数(exited_at − created_at)。課金計測・出欠(is_attend)・障害(録画欠落)判定の根拠
created_at / updated_at→ 移行同左entered_at = created_at(現 entry_time メソッド踏襲)

EntryHistory オブジェクト

idinteger
NOT NULL
基盤採番の一意識別子。ログ系のため UUID ではなく BIGINT 整数(§識別子 C4 例外)
room_idstring · UUID 形式
NOT NULL
所属する Room の識別子(rooms への参照)
participant_idstring · UUID 形式
nullable
入室した Participant の識別子。匿名入室を許容するため null あり
namestring
NOT NULL
入室時の表示名スナップショット
roleenum
NOT NULL
moderator / observer / panelist / minedia_observer
browser_namestring
nullable
ブラウザ名(例 Chrome
browser_versionstring
nullable
ブラウザバージョン(例 137
platform_namestring
nullable
OS / プラットフォーム名(例 iOS
platform_versionstring
nullable
プラットフォームバージョン(例 19.1
ip_addressstring
nullable
入室元 IP アドレス
entered_atdatetime
NOT NULL
入室時刻(ISO 8601 / UTC・created_at と同値)
left_atdatetime
nullable
退室時刻(disconnect 検知で更新・未退室時は null)。entity 側 exited_at
duration_secinteger
nullable
滞在秒数(left_at − entered_at)。課金計測・出欠・障害判定の根拠
EntryHistory オブジェクト
{
  "id": 1001,
  "room_id": "0190a1b3-…",
  "participant_id": "0190a1d0-…",
  "name": "山田太郎",
  "role": "panelist",
  "browser_name": "Chrome",
  "browser_version": "137",
  "platform_name": "iOS",
  "platform_version": "19.1",
  "ip_address": "203.0.113.10",
  "entered_at": "2026-07-01T04:58:01Z",
  "left_at": "2026-07-01T06:01:12Z",
  "duration_sec": 3791
}

EntryHistory を一覧する

GET/api/v1/rooms/{room_id}/entry-histories

指定 Room の入退室履歴を一覧します。出欠判定(is_attend)の材料として調査ドメイン側が pull します。既定ソートは entered_at 昇順です。

クエリパラメータ

パラメータ説明
role任意役割で絞り込み(moderator / observer / panelist / minedia_observer
cursor任意ページングカーソル
limit任意1〜100(default 20)
リクエスト
curl "…/api/v1/rooms/0190a1b3-…/entry-histories?role=panelist" \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
200 レスポンス
{
  "items": [
    {
      "id": 1001,
      "room_id": "0190a1b3-…",
      "participant_id": "0190a1d0-…",
      "name": "山田太郎",
      "role": "panelist",
      "browser_name": "Chrome",
      "browser_version": "137",
      "platform_name": "iOS",
      "platform_version": "19.1",
      "ip_address": "203.0.113.10",
      "entered_at": "2026-07-01T04:58:01Z",
      "left_at": "2026-07-01T06:01:12Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}

W1 / W2 participant.entered / participant.left

W1 / W2participant.entered / participant.left

ルーム内の入退室の事実を push 通知します。Participant 単体の入退室 Webhook はなく、入退室の事実は EntryHistory の W1 / W2 が担い、data に Participant を内包します。

type用途
participant.entered W1入室。出欠判定の材料(現行 OpenTok callback の touch_interview_attendance 後継)
participant.left W2退室。W1 の dataleft_atduration_sec を追加。インタビューコメント(感想投稿)導線・オペ評価工程の起点。謝礼確定のトリガーではない(謝礼は調査側のオペ評価 is_reward で確定)

退室理由 leave_reason の取り扱いは後日意思決定です。現行は connectionDestroyed を noop で捨てており退室理由を一切保持していません。採用時は W2・EntryHistory に leave_reason(voluntary / kicked / disconnected / session_ended)を追加し、ban は kicked として表現します。

Webhook ペイロード(W2 participant.left)
{
  "id": "evt_0190a210-…",
  "version": "1",
  "type": "participant.left",
  "occurred_at": "2026-07-01T06:01:12Z",
  "data": {
    "room_id": "0190a1b3-…",
    "session_id": "0190a1b2-…",
    "session_external_ref": { "external_type": "Slot", "external_id": "123" },
    "participant": {
      "id": "0190a1d0-…",
      "role": "panelist",
      "display_name": "山田太郎",
      "external_type": "User",
      "external_id": "999"
    },
    "entry_history_id": 1001,
    "entered_at": "2026-07-01T04:58:01Z",
    "left_at": "2026-07-01T06:01:12Z",
    "duration_sec": 3791
  }
}
コアリソース

Chat

チャットの送受信はランタイム側の責務です。公開 API は振り返りフェーズの事後参照のみを提供します(チャット履歴の持ち手=基盤)。チャンネル単位の権限(visibility)は ChatMessage に内包され、クエリ条件として露出します。

設計上の責務分界

ChatChannel は単体エンドポイントを持たず、ChatMessagevisibility(チャンネルの権限軸)として内包・露出します。ChatPost(添付ファイル)の事後参照は v2 候補です(現行の事後閲覧ユースケースの有無を確認してから追補)。

ChatMessage オブジェクト

idinteger
NOT NULL
基盤採番の一意識別子。ログ系(高ボリューム)リソースのため UUID ではなく BIGINT 整数(カーソルの内部キーにも使用)
chat_channelobject
NOT NULL
内包されるチャンネル。id(UUID 形式)と visibilitypublic / private / observer_only)を内包
participant_idstring · UUID 形式
nullable
送信した参加者の id。system メッセージは null
sender_namestring
NOT NULL
送信者の表示名(送信時点のスナップショット)
content_typeenum
NOT NULL
text / stamp / archive_status
textstring
nullable
本文。stamp 等の非テキストでは null
created_atdatetime
NOT NULL
送信日時(ISO 8601 / UTC)
ChatMessage オブジェクト
{
  "id": 501,
  "chat_channel": {
    "id": "0190a1e0-…",
    "visibility": "public"
  },
  "participant_id": "0190a1d0-…",
  "sender_name": "山田太郎",
  "content_type": "text",
  "text": "よろしくお願いします",
  "created_at": "2026-07-01T05:00:30Z"
}

旧エンティティとの差分(room_chat_channels / room_chat_messages / room_chat_postschat_*クリックで展開
🆕 新設 新基盤で新設(旧に対応列なし) → 移行 旧列をリネーム / 汎用化して移行 ❌ 対象外 新基盤に持ち込まない

chat_channels(現 RoomChatChannel)

旧フィールド(現行 minedia-www)種別新フィールド(chat_channels)備考
🆕 新設id / tenant_id / uuid全コアテーブル共通の 3 列。uuid は API では id(UUID 形式)として露出
usage (integer enum)→ 移行visibility文字列化。public / private / observer_onlyunique [room_id, visibility](現 [room_id, usage] を踏襲)

chat_messages(現 RoomChatMessage)

旧フィールド(現行 minedia-www)種別新フィールド(chat_messages)備考
🆕 新設id / tenant_id / uuid共通 3 列。ただしログ系のため API は整数 id を露出uuid は内部一意制約用)
room_chat_channel_id→ 移行chat_channel_idFK → chat_channels.id
user_id / employee_id (物理FK)→ 移行participant_ididentity 物理FK をコア内参加者 FK に置換(nullable・system メッセージ用)
content_type→ 移行content_typetext / stamp / archive_status
text→ 移行text
sender_id (global_id 相当)→ 移行sender_refテナント不透明文字列の表示用送信者 ID
sender_name→ 移行sender_name送信時点のスナップショット

chat_posts(現 RoomChatPost・添付)

旧フィールド(現行 minedia-www)種別新フィールド(chat_posts)備考
🆕 新設id / tenant_id / uuid共通 3 列
room_chat_channel_id→ 移行chat_channel_idFK → chat_channels.id
employee_id (物理FK)→ 移行participant_ididentity FK を置換(nullable)
file→ 移行fileアップローダ。保存先はテナント設定

チャット履歴を取得する

GET/api/v1/rooms/{room_id}/chat-messages

振り返りフェーズの事後参照用に、ルームのチャット履歴を取得します。サーバー間参照のため全 visibility を返せます。エンドユーザー(クライアント / オペレーター)への表示可否(private / observer_only の閲覧制限)はテナント側の認可責務です。既定ソートは created_at 昇順です。

クエリパラメータ

パラメータ説明
visibility任意絞り込み。public / private / observer_only。未指定は全チャンネル
cursor任意ページングカーソル
limit任意1〜100(default 20)

エラー

条件レスポンス
不存在 or 他テナントの room404 NOT_FOUND
visibility が enum 外400 INVALID_REQUEST
リクエスト
curl "…/api/v1/rooms/0190a1b3-…/chat-messages?visibility=public&limit=20" \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
200 レスポンス
{
  "items": [
    {
      "id": 501,
      "chat_channel": { "id": "0190a1e0-…", "visibility": "public" },
      "participant_id": "0190a1d0-…",
      "sender_name": "山田太郎",
      "content_type": "text",
      "text": "よろしくお願いします",
      "created_at": "2026-07-01T05:00:30Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}

チャット履歴を削除する(運用 purge)

DELETE/api/v1/rooms/{room_id}/chat-messages

ルームのチャット履歴をまとめて削除します(現行 operator 画面の destroy_messages 後継。誤投稿・個人情報の消去等の運用操作)。チャット履歴を基盤が所有するため、削除も基盤側 API で行います。Idempotency-Key に対応します。

クエリパラメータ

パラメータ説明
visibility任意絞り込み。public / private / observer_only。未指定はルームの全チャンネルを対象

副作用

対象挙動
対象メッセージ対象範囲の ChatMessage を削除
添付紐づく ChatPost も併せて削除
監査RoomEvent chat.purged(payload に件数・要求元)を記録

成功時は 204 No Content。対象 0 件でも冪等で 204 を返します。

個別削除は PoC では非対応です(必要なら v2 で DELETE …/chat-messages/{id} を追補する余地があります)。

リクエスト
curl -X DELETE "…/api/v1/rooms/0190a1b3-…/chat-messages?visibility=public" \
  -H "Authorization: ApiKey key_live_a1b2.sk_…" \
  -H "Idempotency-Key: 7c9e6679-…"
レスポンス
204 No Content
コアリソース

RoomEvent

Room 内で発生した事実を記録する汎用イベントログです。書き込み(提示物の show / hide 等)はランタイム側の能力であり、公開 API は読み取りのみを提供します(URL レポート・再生・監査用)。

ログ系のため id は BIGINT 整数

RoomEvent は高ボリュームなログ系リソース(C4)のため、UUID を持たず BIGINT の整数id として露出します(カーソルの内部キーにも使用)。UUID 文字列を id とする通常リソースに対する明示的な例外です。

RoomEvent オブジェクト

idinteger
NOT NULL
基盤採番の一意識別子(ログ系のため BIGINT 整数。カーソルの内部キーにも使用)
room_idstring · UUID 形式
NOT NULL
イベントが発生した Room の id
event_typestring
NOT NULL
イベント種別。コア語彙(例 handout.show)に加え、未知の値はテナント拡張として不透明文字列で保持(enum 化しない)
payloadobject · json
nullable
イベント付随データ(例 handout.showhandout_id)。未設定時は null
created_atdatetime
NOT NULL
イベント発生日時(ISO 8601 / UTC)
RoomEvent オブジェクト
{
  "id": 9001,
  "room_id": "0190a1b3-…",
  "event_type": "handout.show",
  "payload": {
    "handout_id": "0190a1c0-…"
  },
  "created_at": "2026-07-01T05:10:00Z"
}

旧エンティティとの差分(room_eventsroom_eventsクリックで展開
🆕 新設 新基盤で新設(旧に対応列なし) → 移行 旧列をリネーム / 汎用化して移行 ❌ 対象外 新基盤に持ち込まない(調査ドメイン残置 or 廃止)
旧フィールド(現行 minedia-www)種別新フィールド(room_events)備考
🆕 新設id / tenant_id共通規約(uuid なし・純ログ)。id はログ系のため BIGINT の整数(カーソルの内部キーにも使用)/ tenant_id=テナントスコープ(PoC は 0 固定)
room_id→ 移行room_idFK → rooms.id
type→ 移行event_typeSTI 無効化用の type をリネーム(現 inheritance_column = :_type_disabled)。SHOW_PROJECT_HANDOUT 等の調査固有語彙は不透明文字列のまま
payload→ 移行payloadtext (JSON serialize) → json / text
created_at / updated_at→ 移行同左

RoomEvent を一覧する

GET/api/v1/rooms/{room_id}/room-events

Room 配下のイベントを時系列で取得します。再生ロジック(SHOW / HIDE 圧縮)の帰属は未決のため、本 API は生イベントの時系列のみ返します。既定ソートは created_at 昇順です。

クエリパラメータ

パラメータ説明
event_type任意イベント種別での絞り込み(例 handout.show
cursor任意ページングカーソル
limit任意1〜100(default 20)

エラー

条件レスポンス
Room が不存在 or 他テナント404 NOT_FOUND
event_type の語彙

コア語彙の例: handout.show / handout.hide / token.force_issued / room.tokens_revoked / recording.failed / chat.purged。未知の値はテナント拡張として不透明文字列で保持します。

リクエスト
curl "…/api/v1/rooms/0190a1b3-…/room-events?event_type=handout.show" \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
200 レスポンス
{
  "items": [
    {
      "id": 9001,
      "room_id": "0190a1b3-…",
      "event_type": "handout.show",
      "payload": { "handout_id": "0190a1c0-…" },
      "created_at": "2026-07-01T05:10:00Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
コアリソース

Recording

録画の事実を表すリソースです。録画の開始 / 停止はランタイム側の能力(recording.control=moderator)または archive_mode: always の自動録画で行われ、公開 API は録画の事実の読み取り+署名 URL の払い出し+運用削除のみを提供します。

RtcSession の内包 / 文字起こしのコア化

RtcSession(エンジン抽象)は単体エンドポイントを持たず、Recording に rtc_session_id として内包されます。文字起こしはコア化されており(G5)、録画完了後の自動 / 手動文字起こしは Transcription ページを参照してください。

Recording オブジェクト

idstring · UUID 形式
NOT NULL
基盤採番の一意識別子(不透明文字列・内部キーは UUIDv7)。調査側の録画参照キー
rtc_session_idstring · UUID 形式
NOT NULL
内包する RtcSession(エンジン抽象)の識別子。RtcSession は単体エンドポイントを持たず、ここに rtc_session_id として現れる
room_idstring · UUID 形式
nullable
所属する Room の識別子。接続テスト用録画は null(ConnectionTest の recordings から参照され、/rooms/{room_id}/recordings には現れない)
engineenum
NOT NULL
opentok / livekit / zoom。RtcSession から非正規化(クエリ容易化)
provider_archive_idstring
NOT NULL
エンジン側の録画 ID(LiveKit=Egress ID / Zoom=recording ID / OpenTok=Archive ID)
statusenum
NOT NULL
started / paused / stopped / uploaded / available / expired / failed。署名 URL 払い出しは available のみ
output_modeenum
NOT NULL
composed(合成)/ individual(個別)
sizeinteger
NOT NULL
録画ファイルのサイズ(bytes)
duration_secnumber
NOT NULL
録画の長さ(秒・小数あり)
created_atdatetime
NOT NULL
作成日時(ISO 8601 / UTC)
updated_atdatetime
NOT NULL
更新日時
Recording オブジェクト
{
  "id": "0190a1f0-…",
  "rtc_session_id": "0190a1e5-…",
  "room_id": "0190a1b3-…",
  "engine": "livekit",
  "provider_archive_id": "EG_xxxx",
  "status": "available",
  "output_mode": "composed",
  "size": 734003200,
  "duration_sec": 3612.5,
  "created_at": "2026-07-01T05:00:05Z",
  "updated_at": "2026-07-01T06:05:00Z"
}

旧エンティティとの差分(ot_archivesrecordingsクリックで展開
🆕 新設 新基盤で新設 → 移行 リネーム / 汎用化して移行 ❌ 対象外 調査残置 or 廃止
旧フィールド種別新フィールド備考
🆕 新設id / tenant_id / uuid全コアテーブル共通の3 列をまとめた表記id=内部 BIGINT 主キー(非公開)/ tenant_id=テナントスコープ(PoC は 0 固定)/ uuid=公開識別子(API では id として露出・調査側の録画参照キー)
ot_session_id→ 移行rtc_session_idコア内部 FK → rtc_sessions.id。OpenTok 直結から汎用エンジン抽象へ
🆕 新設engineRtcSession から非正規化(クエリ容易化・任意)
archive_id(OpenTok)→ 移行provider_archive_id汎用名へ(LiveKit=Egress ID / Zoom=recording ID)
status→ 移行statusstarted / paused / stopped / uploaded / available / expired / failed。DEC-02 確定時に「正規化 status+provider_status」2 層化候補
output_mode→ 移行output_modecomposed / individual
size→ 移行sizebytes
file_duration_sec→ 移行duration_secdecimal(10,4)
duration_from_tokbox❌ 対象外ベンダ固有の生値・持ち込まない
#video_s3_key(メソッド+Settings.opentok)→ 移行storage_key生成ロジックはコアへ移植、bucket / apikey はテナント設定経由
created_at / updated_at→ 移行同左

Room の録画を一覧する

GET/api/v1/rooms/{room_id}/recordings

Room 配下の全 RtcSession に紐づく Recording を返します。既定ソートは created_at 昇順です。接続テスト用録画(room_idnull)はここには現れません。

クエリパラメータ

パラメータ説明
cursor任意ページングカーソル
limit任意1〜100(default 20)

エラー

条件レスポンス
Room が不存在 or 他テナント404 NOT_FOUND
リクエスト
curl "…/api/v1/rooms/0190a1b3-…/recordings" \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
200 レスポンス
{
  "items": [
    {
      "id": "0190a1f0-…",
      "rtc_session_id": "0190a1e5-…",
      "room_id": "0190a1b3-…",
      "engine": "livekit",
      "status": "available",
      "output_mode": "composed",
      "duration_sec": 3612.5
    }
  ],
  "next_cursor": null,
  "has_more": false
}

Recording を取得する

GET/api/v1/recordings/{recording_id}

id(UUID 形式)で録画の事実を 1 件取得します。録画の実体ファイルへのアクセスは署名付き URL の払い出し(次節)を使います。

エラー

条件レスポンス
不存在 or 他テナント404 NOT_FOUND
リクエスト
curl …/api/v1/recordings/0190a1f0-… \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
200 レスポンス
{
  "id": "0190a1f0-…",
  "rtc_session_id": "0190a1e5-…",
  "room_id": "0190a1b3-…",
  "engine": "livekit",
  "provider_archive_id": "EG_xxxx",
  "status": "available",
  "output_mode": "composed",
  "size": 734003200,
  "duration_sec": 3612.5,
  "created_at": "2026-07-01T05:00:05Z",
  "updated_at": "2026-07-01T06:05:00Z"
}

署名付き URL を払い出す

GET/api/v1/recordings/{recording_id}/download-url

録画ファイル(S3・非公開バケット)への短期署名 URL を払い出します。文字起こし処理・録画閲覧画面の双方で使います。

認可の責務分界

基盤は「API key 認証済みテナント」にのみ署名 URL を払い出します。エンドユーザー(クラ / オペ)への閲覧可否はテナント側が判断します。録画=個人情報のため、払い出しは基盤側でアクセスログを記録します(recording_id / 払い出し時刻 / expires_at / 要求元テナント)。

クエリパラメータ

パラメータ説明
expires_in_seconds任意
integer
署名 URL の有効秒数。6010800(default 3600

エラー

条件レスポンス
statusavailable 以外409 RECORDING_NOT_AVAILABLE
expires_in_seconds が範囲外400 INVALID_REQUEST
不存在 or 他テナント404 NOT_FOUND
リクエスト
curl "…/api/v1/recordings/0190a1f0-…/download-url?expires_in_seconds=3600" \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
200 レスポンス
{
  "url": "https://s3.../signed...",
  "expires_at": "2026-07-01T07:05:00Z"
}

Recording を削除する(運用・個人情報消去)

DELETE/api/v1/recordings/{recording_id}

録画の実体(S3 オブジェクト)と事実レコードを削除します(現行 operator 画面の DELETE ot_archives/:id 後継)。録画=個人情報のため、保持期間超過・誤録画・削除依頼への対応として基盤側で物理削除します。Idempotency-Key ヘッダに対応します。

副作用

対象挙動
録画ファイル / レコードS3 の録画ファイルと Recording レコードを削除
Transcription当該録画にぶら下がる Transcription連鎖削除
監査ログ削除の事実を基盤側で記録(recording_id / 削除時刻 / 要求元テナント)

エラー

条件レスポンス
録画中(statusstarted / paused)は削除不可(停止・完了後に削除)409 RECORDING_NOT_AVAILABLE

成功時は 204 No Content。削除済みへの再実行は冪等で 204 を返します。

リクエスト
curl -X DELETE …/api/v1/recordings/0190a1f0-… \
  -H "Authorization: ApiKey key_live_a1b2.sk_…" \
  -H "Idempotency-Key: 7c9e6679-…"
レスポンス
204 No Content

W3 recording.status_changed

W3recording.status_changed

録画の状態が変化したときに送信されます。available への遷移は文字起こし・事後処理の起点として、failed への遷移は録画障害アラート・オペ対応の起点として利用できます(現行 app_main の録画監視は基盤側へ移り、通知だけ調査側に飛びます)。

status用途
available文字起こし・事後処理の起点
failed録画障害アラート・オペ対応(failure_reason に原因)
Webhook ペイロード
{
  "id": "evt_0190a210-…",
  "version": "1",
  "type": "recording.status_changed",
  "occurred_at": "2026-07-01T06:05:00Z",
  "data": {
    "recording_id": "0190a1f0-…",
    "room_id": "0190a1b3-…",
    "session_id": "0190a1b2-…",
    "session_external_ref": { "external_type": "Slot", "external_id": "123" },
    "status": "available",
    "previous_status": "uploaded",
    "failure_reason": null
  }
}
コアリソース

Transcription

2026-06-18 の G5 決定でコア化した、録画の子リソースです。基盤は文字起こしの事実+結果を持ち、要約・集計は調査ドメインTranscription.id を起点に行います。公開 API は読み取り+完了 Webhookを基本としつつ、加えて手動起動を持ちます。

自動文字起こしと手動起動

録画完了時に基盤が自動で文字起こしを実行します。手動起動は、別言語・再実行・翻訳をオペレーション起点でオンデマンド要求するための後継機能です(read-only 原則に対する運用例外)。

旧エンティティとの差分(transcription_requeststranscriptionsクリックで展開
🆕 新設 新基盤で新設(旧に対応列なし) → 移行 旧列をリネーム / 汎用化して移行 ❌ 対象外 新基盤に持ち込まない(調査ドメイン残置 or 廃止)
旧フィールド(現行 minedia-www)種別新フィールド(transcriptions)備考
🆕 新設id / tenant_id / uuid全コアテーブル共通の3 列をまとめた表記(1 つの識別子ではない)。id=内部 BIGINT 主キー(非公開・コア内 FK 用)/ tenant_id=テナントスコープ(PoC は 0 固定)/ uuid=公開識別子(API では id として露出・調査側の要約/集計の参照キー)。※DB の id と API の id は別物
ot_archive_id→ 移行recording_idコア内部 FK → recordings.id(OtArchive → Recording に追随)
transcription_service(5 ベンダ integer enum)→ 移行providereleven_labs / openai / azure / anyenv / rimoengine と同じくベンダ中立の文字列化
transcription_id(ベンダのジョブ ID)→ 移行provider_transcription_id汎用名へ。unique [provider, provider_transcription_id]
status(6 種 integer enum)→ 移行statussending_request / processing / retrying / done / error / request_error(文字列化)
result_json→ 移行result_jsonVTT/JSON の文字起こし結果(ベンダ生形式はここに閉じる)。API は result_url で払い出す(本体は基盤内部)
duration→ 移行duration_secdecimal(10,4)
language→ 移行languageG5 副作用でコアに入る。default ja-JP
local(true=自前 / false=翻訳)→ 移行local自前文字起こし vs 翻訳の区別(現状踏襲・default true
error_message→ 移行error_message
created_at / updated_at→ 移行同左

Transcription オブジェクト

idstring · UUID 形式
NOT NULL
基盤採番の一意識別子(不透明文字列・内部キーは UUIDv7)。調査ドメインが要約・集計の起点に使う参照キー
recording_idstring · UUID 形式
NOT NULL
親の録画(Recording.id)。1 録画に複数言語/翻訳がぶら下がる
room_idstring · UUID 形式
NOT NULL
録画が属する Room(Room.id
providerenum
NOT NULL
文字起こしエンジン。eleven_labs / openai / azure / anyenv / rimo
provider_transcription_idstring
nullable
ベンダ側のジョブ ID(未連携時は null
statusenum
NOT NULL
sending_request / processing / retrying / done / error / request_error
languagestring
NOT NULL
出力言語(BCP 47・既定 ja-JP
localboolean
NOT NULL
true=自前文字起こし / false=翻訳
result_urlstring
nullable
VTT/JSON 結果への短期署名 URL(処理未完時は null)。本体(result_json)は基盤内部に閉じ、録画 download-url と同じ非公開バケット方針
duration_secnumber
nullable
文字起こし対象の長さ(秒)。処理未完時は null
created_atdatetime
NOT NULL
作成日時
updated_atdatetime
NOT NULL
更新日時
Transcription オブジェクト
{
  "id": "0190a210-…",
  "recording_id": "0190a1f0-…",
  "room_id": "0190a1b3-…",
  "provider": "openai",
  "provider_transcription_id": "tr_xxxx",
  "status": "done",
  "language": "ja-JP",
  "local": true,
  "result_url": "https://s3.../signed-vtt…",
  "duration_sec": 3612.5,
  "created_at": "2026-07-01T06:10:00Z",
  "updated_at": "2026-07-01T06:14:30Z"
}

録画の文字起こしを一覧する

GET/api/v1/recordings/{recording_id}/transcriptions

1 録画にぶら下がる文字起こし(複数言語 / 翻訳)を一覧します。既定ソートは created_at 昇順です。

クエリパラメータ

パラメータ説明
cursor任意ページングカーソル
limit任意1〜100(default 20)

エラー

条件レスポンス
録画が不存在 or 他テナント404 NOT_FOUND
リクエスト
curl …/api/v1/recordings/0190a1f0-…/transcriptions \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
200 レスポンス
{
  "items": [
    { "id": "0190a210-…", "recording_id": "0190a1f0-…", "language": "ja-JP", "local": true, "status": "done" }
  ],
  "next_cursor": null,
  "has_more": false
}

Transcription を取得する

GET/api/v1/transcriptions/{transcription_id}

id(UUID 形式)で 1 件取得します。done の場合は result_url に短期署名 URL を含みます。

エラー

条件レスポンス
不存在 or 他テナント404 NOT_FOUND
リクエスト
curl …/api/v1/transcriptions/0190a210-… \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
200 レスポンス
{
  "id": "0190a210-…",
  "recording_id": "0190a1f0-…",
  "room_id": "0190a1b3-…",
  "provider": "openai",
  "status": "done",
  "language": "ja-JP",
  "local": true,
  "result_url": "https://s3.../signed-vtt…",
  "duration_sec": 3612.5
}

文字起こし/翻訳を起動する

POST/api/v1/recordings/{recording_id}/transcriptions

録画に対し文字起こし / 翻訳をオンデマンド起動します(現行 operator 画面の transcribe / translate 後継)。録画完了時の自動文字起こしに加え、別言語・再実行・翻訳をオペ起点で要求する書き込み系です。Idempotency-Key に対応します。

ボディパラメータ

パラメータ説明
language任意
string(≤16)
出力言語(BCP 47)。未指定は Session の transcription_language
translate任意
boolean
true=翻訳(local=false)/ false=自前文字起こし(default false
provider任意
enum
エンジン上書き(eleven_labs / openai / azure / anyenv / rimo)。未指定は Session の transcription_service

エラー

条件レスポンス
録画 statusavailable 以外409 RECORDING_NOT_AVAILABLE
language / provider が不正400 INVALID_REQUEST
同一 (recording, language, translate) が処理中409 TRANSCRIPTION_IN_PROGRESS

成功時は 201 CreatedLocation: /api/v1/transcriptions/{transcription_id}status: sending_request)を返します。完了は W6 transcription.completed で push 通知します。

リクエスト
curl -X POST …/api/v1/recordings/0190a1f0-…/transcriptions \
  -H "Authorization: ApiKey key_live_a1b2.sk_…" \
  -H "Idempotency-Key: 7c9e6679-…" \
  -H "Content-Type: application/json" \
  -d '{
    "language": "en-US",
    "translate": true,
    "provider": "openai"
  }'
201 レスポンス
{
  "id": "0190a211-…",
  "recording_id": "0190a1f0-…",
  "room_id": "0190a1b3-…",
  "provider": "openai",
  "status": "sending_request",
  "language": "en-US",
  "local": false,
  "result_url": null,
  "duration_sec": null,
  "created_at": "2026-07-01T06:20:00Z",
  "updated_at": "2026-07-01T06:20:00Z"
}

W6 transcription.completed

W6transcription.completed

文字起こしは録画完了(W3 available)より遅れて非同期に付くため、push 通知が必要です。minedia-www はこれを受けて Transcription を取得し、要約・集計を調査側で生成します。status: error / request_error は文字起こし失敗としてオペ / 調査側に通知します。

Webhook ペイロード
{
  "id": "evt_0190a212-…",
  "version": "1",
  "type": "transcription.completed",
  "occurred_at": "2026-07-01T06:14:30Z",
  "data": {
    "transcription_id": "0190a210-…",
    "recording_id": "0190a1f0-…",
    "room_id": "0190a1b3-…",
    "session_id": "0190a1b2-…",
    "session_external_ref": { "external_type": "Slot", "external_id": "123" },
    "status": "done",
    "language": "ja-JP",
    "failure_reason": null
  }
}
コアリソース

Handout

画質保持のため、メディアストリームではなく事前アップロード+ブラウザ直表示で提示する資料(image / video)です。割付ロジック(target_slot_type / project_handouts_slots)は調査ドメイン側に残置し、調査側が割付解決済みのファイルを Session に登録します。

設計上の責務分界

「どの枠にどの提示物を出すか」の割付は Project の語彙に依存するため調査ドメイン側に残置します。基盤は割付解決済みのリストだけを受け取り、提示の実体(ファイル・順序・種別)のみを保持します。提示操作(show / hide)はルーム内ランタイムの能力(handout.present=moderator)であり、本 API の対象外です。

旧エンティティとの差分(project_handouts / extemporary_project_handoutshandoutsクリックで展開
🆕 新設 新基盤で新設(旧に対応列なし) → 移行 旧列をリネーム / 汎用化して移行 ❌ 対象外 新基盤に持ち込まない(調査ドメイン残置 or 廃止)
旧フィールド(現行 minedia-www)種別新フィールド(handouts)備考
🆕 新設id / tenant_id / uuid全コアテーブル共通の3 列をまとめた表記(1 つの識別子ではない)。id=内部 BIGINT 主キー(非公開・コア内 FK 用)/ tenant_id=テナントスコープ(PoC は 0 固定)/ uuid=公開識別子(RoomEvent payload・API での参照キー・API では id として露出
project_handouts.id / extemporary_project_handouts.id(出所 PK)→ 移行external_type / external_id出所 PK を external_ref 化。例 {"ProjectHandout","12"}FK ではない。調査側の再登録・同期時の重複排除キー
project_id / extemporary_slot_id + slot 割付の解決結果→ 移行session_idコア内部 FK → sessions.id。調査側が割付(Slot#all_project_handouts=all + slot 指定)を解決してから登録する
file(ProjectHandoutUploader)→ 移行file非公開バケット+短期署名 URL を踏襲(file_url として露出)
content_type→ 移行content_typeinteger enum(1=image / 2=video)→ 文字列化image / video
sort→ 移行sort提示順(default 0)
project_handouts.target_slot_type(all/selected)/ project_handouts_slots(join)❌ 対象外— (調査に残置)割付は Project の語彙。コアは割付解決済みのリストだけを受け取る
created_at / updated_at→ 移行同左

Handout オブジェクト

idstring · UUID 形式
NOT NULL
基盤採番の一意識別子(不透明文字列・内部キーは UUIDv7)
session_idstring · UUID 形式
NOT NULL
所属する Session の id
external_typestring
nullable
テナント側リソース種別。tenant 0 では ProjectHandout / ExtemporaryProjectHandout(未指定時は null
external_idstring
nullable
テナント側 PK(整数も文字列化)。重複排除キーとして external_type とペアで使用
file_namestring
NOT NULL
アップロードされたファイル名
file_urlstring
NOT NULL
署名付き URL(非公開バケット)。<img> / <video> でブラウザ直表示する(WebRTC ストリーム非経由)
content_typeenum
NOT NULL
image / video
sortinteger
NOT NULL
提示順(昇順・既定 0
created_atdatetime
NOT NULL
作成日時(ISO 8601 / UTC)
id と file_url の扱い

id は RoomEvent payload(handout.show / handout.hidehandout_id)での参照キーです。file_url の有効期限はインタビュー時間(ends_at − 取得時刻)より長いことが制約です。

Handout オブジェクト
{
  "id": "0190a1c0-…",
  "session_id": "0190a1b2-…",
  "external_type": "ProjectHandout",
  "external_id": "12",
  "file_name": "concept_A.png",
  "file_url": "https://s3.../signed…",
  "content_type": "image",
  "sort": 0,
  "created_at": "2026-06-10T09:00:00Z"
}

Handout を登録する

POST/api/v1/sessions/{session_id}/handouts

調査側が割付(target_slot_type / project_handouts_slots)を解決した後、提示する実体ファイルを Session に登録します。multipart/form-data でファイル本体を送信し、Idempotency-Key に対応します。

ボディパラメータ(multipart/form-data

パラメータ説明
file必須
binary
提示物ファイル本体
content_type必須
enum
image / video
sort任意
integer ≥0
提示順(default 0
external_type任意
string(≤64)
tenant 0: ProjectHandout / ExtemporaryProjectHandout
external_id任意
string(≤64)
テナント側 PK(整数も文字列化)。同一 external_ref の再登録は置換(上書き)=調査側の再同期を冪等にする

エラー

条件レスポンス
file / content_type 必須・external_ref ペア指定・sort は 0 以上の整数400 INVALID_REQUEST
拡張子ホワイトリスト外(image=jpg/jpeg/gif/png/bmp/tiff、video=mp4/mov。PDF・Office 不可)422 UNSUPPORTED_FILE_TYPE
サイズ上限超過(image 20MB / video 300MB・仮値413 FILE_TOO_LARGE
解放済み(released)Session への登録409 SESSION_RELEASED

成功時は 201 CreatedLocation: /api/v1/handouts/{handout_id} を返します。

リクエスト
curl -X POST …/api/v1/sessions/0190a1b2-…/handouts \
  -H "Authorization: ApiKey key_live_a1b2.sk_…" \
  -H "Idempotency-Key: 7c9e6679-…" \
  -F "file=@concept_A.png" \
  -F "content_type=image" \
  -F "sort=0" \
  -F "external_type=ProjectHandout" \
  -F "external_id=12"
201 レスポンス
{
  "id": "0190a1c0-…",
  "session_id": "0190a1b2-…",
  "external_type": "ProjectHandout",
  "external_id": "12",
  "file_name": "concept_A.png",
  "file_url": "https://s3.../signed…",
  "content_type": "image",
  "sort": 0,
  "created_at": "2026-06-10T09:00:00Z"
}

Handout を一覧する

GET/api/v1/sessions/{session_id}/handouts

Session に登録済みの提示物を一覧します。既定ソートは sort 昇順です。

クエリパラメータ

パラメータ説明
cursor任意ページングカーソル
limit任意1〜100(default 20)
リクエスト
curl …/api/v1/sessions/0190a1b2-…/handouts \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
200 レスポンス
{
  "items": [
    { "id": "0190a1c0-…", "session_id": "0190a1b2-…", "content_type": "image", "sort": 0 }
  ],
  "next_cursor": null,
  "has_more": false
}

Handout を更新する

PATCH/api/v1/handouts/{handout_id}

メタデータ(sort 等)の部分更新と、任意のファイル差し替えを行います(multipart/form-data・全フィールド任意・指定したもののみ更新)。sort のみの並べ替えはファイル再送なしで行える点が、POST upsert(同 external_ref 全置換)との使い分けです。

ボディパラメータ(multipart/form-data

パラメータ説明
sort任意
integer ≥0
提示順
content_type任意
enum
image / video
file任意
binary
差し替えファイル本体(指定時のみ置換。拡張子 / サイズは登録時と同一検証)

エラー

条件レスポンス
file 差し替え時、拡張子ホワイトリスト外422 UNSUPPORTED_FILE_TYPE
file 差し替え時、サイズ上限超過413 FILE_TOO_LARGE
sort が 0 以上の整数でない400 INVALID_REQUEST
ルーム内で提示中(直近 handout.show 後に hide なし)の file 差し替え(sort 変更は可)409 HANDOUT_IN_USE
解放済み(released)Session の Handout409 SESSION_RELEASED

成功時は 200 とリソース表現を返します。

リクエスト(並べ替えのみ)
curl -X PATCH …/api/v1/handouts/0190a1c0-… \
  -H "Authorization: ApiKey key_live_a1b2.sk_…" \
  -F "sort=2"
200 レスポンス
{
  "id": "0190a1c0-…",
  "session_id": "0190a1b2-…",
  "file_name": "concept_A.png",
  "content_type": "image",
  "sort": 2,
  "created_at": "2026-06-10T09:00:00Z"
}

Handout を削除する

DELETE/api/v1/handouts/{handout_id}

登録済みの提示物を削除します。

エラー

条件レスポンス
ルーム内で提示中(直近 handout.show 後に hide なし)は削除不可409 HANDOUT_IN_USE

成功時は 204 No Content

リクエスト
curl -X DELETE …/api/v1/handouts/0190a1c0-… \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
レスポンス
204 No Content
コアリソース

ConnectionTest

現 BrowserTestHistory を汎用化した「被験者ブラウザ / 回線の品質計測」リソースです。接続テストは基盤で実施し(ダミー配信+購読負荷・計測 15 秒以上)、専用の RtcSession を 1 本持ちます。合格判定(User.webrtc_test_status 相当)はテナント側の責務であり、基盤は計測の事実だけを返します。

公開範囲の責務分界

診断ログ ConnectionTestEvent(entity §3.14)は基盤内部のサポート / ファネル分析用で、公開 API には露出しません。API が返すのは ConnectionTest の計測結果(summary / baseline / load)と録画参照までです。

ConnectionTest オブジェクト

idstring · UUID 形式
NOT NULL
基盤採番の一意識別子(不透明文字列・内部キーは UUIDv7)。調査側の結果参照キー
statusenum
NOT NULL
ライフサイクル状態。created / completed / failed
measurement_completedboolean
NOT NULL
計測成立フラグfalse = 計測時間が 15 秒未満(MEASUREMENT_MIN_SECONDS)等で測定不成立 →summary / baseline / loadnull。テナントは「測定できませんでした / 動画を直接確認」を表示
external_typestring
nullable
被験者のテナント側参照種別(tenant 0: User)。匿名テストは null
external_idstring
nullable
被験者のテナント側 PK(整数も文字列化)。匿名テストは null
rtc_engineenum
NOT NULL
テスト対象エンジン。opentok / livekit / zoom
dummy_stream_countinteger
NOT NULL
負荷シミュレーションのダミー配信本数
hardware_concurrencyinteger
nullable
被験者端末の論理 CPU コア数(取得できない場合 null
connected_subscriber_countinteger
nullable
負荷時に実接続できた購読数(測定不成立時 null
summaryobject
nullable
一覧表示用の総合集計(平均値 / 中央値 / パケットロス)。管理画面 Web RTC 一覧の列に対応。measurement_completed=false 時は null
baselineobject
nullable
負荷前の詳細指標(詳細画面向け)。測定不成立時 null
loadobject
nullable
負荷時の詳細指標。単一参加者の旧記録(legacy_record?)は null
recordingsarray
NOT NULL
接続テスト録画への参照 { id, status } の配列。再生は §録画 download-url。なければ空配列
provider_inspector_urlstring
nullable
エンジン固有の診断ビューア URL(livekit=LiveKit Cloud セッション画面)。提供されないエンジンは null
completed_atdatetime
nullable
完了時刻(未完了時 null
created_atdatetime
NOT NULL
作成日時

summary / baseline / loadエンジン中立語彙(bitrate / packet loss / fps / jitter)で表現します。エンジン固有の生統計は基盤内部の raw_data に閉じ、API には出しません(OpenTok→LiveKit 交代でも列はそのまま使えます)。

ConnectionTest オブジェクト
{
  "id": "0190a200-…",
  "status": "completed",
  "measurement_completed": true,
  "external_type": "User",
  "external_id": "999",
  "rtc_engine": "livekit",
  "dummy_stream_count": 5,
  "hardware_concurrency": 8,
  "connected_subscriber_count": 5,
  "summary": {
    "video_average_bitrate": 441000.0,
    "video_median_bitrate": 477000.0,
    "video_average_packets_loss_ratio": 0.0,
    "video_median_packets_loss_ratio": 0.0,
    "audio_average_bitrate": 31000.0,
    "audio_median_bitrate": 32000.0,
    "audio_average_packets_loss_ratio": 0.0,
    "audio_median_packets_loss_ratio": 0.0
  },
  "baseline": {
    "video_average_bitrate": 1850000.0,
    "video_average_packets_loss_ratio": 0.001,
    "frames_dropped_ratio": 0.0,
    "fps": 29.8,
    "audio_bitrate": 32000.0,
    "audio_packets_loss_ratio": 0.0,
    "audio_jitter": 0.004
  },
  "load": {
    "video_average_bitrate": 1520000.0,
    "video_average_packets_loss_ratio": 0.01,
    "worst_video_bitrate": 980000.0,
    "worst_video_packets_loss_ratio": 0.03,
    "overall_frames_dropped_ratio": 0.02,
    "worst_frames_dropped_ratio": 0.06,
    "overall_fps": 27.0,
    "worst_fps": 18.0,
    "audio_average_bitrate": 31000.0,
    "audio_average_packets_loss_ratio": 0.002,
    "worst_audio_packets_loss_ratio": 0.01,
    "audio_average_jitter": 0.006,
    "worst_audio_jitter": 0.02
  },
  "recordings": [
    { "id": "0190a1f0-…", "status": "available" }
  ],
  "provider_inspector_url": "https://cloud.livekit.io/projects/p_1uvc9dsiv14/sessions/RM_UNAo9Dh8Pq9t",
  "completed_at": "2026-06-10T09:05:00Z",
  "created_at": "2026-06-10T09:00:00Z"
}

旧エンティティとの差分(browser_test_historiesconnection_testsクリックで展開
🆕 新設 新基盤で新設 → 移行 リネーム / 汎用化して移行 ❌ 対象外 調査残置 or 廃止
旧フィールド(現行 minedia-www)種別新フィールド(connection_tests)備考
🆕 新設id / tenant_id / uuid全コアテーブル共通の3 列をまとめた表記id=内部 BIGINT 主キー(非公開)/ tenant_id=テナントスコープ(PoC は 0 固定)/ uuid=公開識別子(API では id として露出
has_one :ot_session→ 移行rtc_session_id所有方向を逆転しコア内部 FK 化 → rtc_sessions.id(テスト専用セッション・unique
user_id(物理 FK)→ 移行external_type / external_id被験者 identity(tenant 委譲。例 {"User","999"})。FK ではない
🆕 新設statusライフサイクル created / completed / failed に明示化
dummy_stream_count / hardware_concurrency / connected_subscriber_count→ 移行同左負荷シミュレーション構成(現状踏襲)
baseline_* 計測群(video bitrate / loss / fps / audio jitter 等)→ 移行同左(API baselineベースライン計測(現状踏襲)
load_* 計測群(average / worst の bitrate / loss / fps / jitter)→ 移行同左(API load負荷時計測(現状踏襲)
bits_data(JSON serialize)→ 移行raw_data汎用名へ(json)。生サンプル系列・エンジン固有の生統計はここに閉じる
総合集計 8 列(video / audio_average / median_bitrate / *_packets_loss_ratio)→ 移行同左(API summary一覧描画に必須のためコアへ移植(旧「持ち込まない」判断を撤回)。管理画面 Web RTC 一覧の平均値 / 中央値 / パケットロス

接続テストを開始する

POST/api/v1/connection-tests

被験者用の接続テストを作成し、基盤のテスト画面 URL(短命トークン付き)を払い出します。テナントは被験者ブラウザをこの URL に誘導してください。Idempotency-Key に対応します。

ボディパラメータ

パラメータ説明
external_type任意
string(≤64)
被験者のテナント側参照(tenant 0: User)。external_id とペア指定。両方 null で匿名テスト
external_id任意
string(≤64)
被験者のテナント側 PK。external_type とペア指定
rtc_engine任意
enum
テスト対象エンジン(default はテナント設定)

エラー

条件レスポンス
external_ref を片方のみ指定400 INVALID_REQUEST

成功時は 201 CreatedLocation: /api/v1/connection-tests/{connection_test_id} を返します。

テスト URL の有効期限は 30 分固定です。

リクエスト
curl -X POST …/api/v1/connection-tests \
  -H "Authorization: ApiKey key_live_a1b2.sk_…" \
  -H "Idempotency-Key: 7c9e6679-…" \
  -H "Content-Type: application/json" \
  -d '{
    "external_type": "User",
    "external_id": "999",
    "rtc_engine": "livekit"
  }'
201 レスポンス
{
  "id": "0190a200-…",
  "test_url": "https://interview.minedia.com/connection-tests/0190a200-…?t=…",
  "expires_at": "2026-06-10T09:30:00Z"
}

被験者ごとの履歴を取得する

GET/api/v1/connection-tests

minedia-www の operators/users/{id}「Web RTC」一覧のデータ源泉です。各項目が summary(平均値 / 中央値 / パケットロス)・recordings(動画)・provider_inspector_url(Inspector)・measurement_completed を内包するため、1 リクエストで一覧表を描画でき、行ごとの詳細呼び出し(N+1)は不要です。既定ソートは created_at 降順です。

クエリパラメータ

パラメータ説明
external_type必須被験者単位の照会キー(external_id とペア指定)
external_id必須被験者単位の照会キー(external_type とペア指定)
cursor任意ページングカーソル
limit任意1〜100(default 20)。最新 N 件は limit で取得

external_ref を省略して作成した匿名テストは本一覧(被験者単位照会)には現れません。匿名テストは id 直接取得(次節)でのみ参照できます。

リクエスト
curl "…/api/v1/connection-tests?external_type=User&external_id=999" \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
200 レスポンス
{
  "items": [
    {
      "id": "0190a200-…",
      "status": "completed",
      "measurement_completed": true,
      "external_type": "User",
      "external_id": "999",
      "rtc_engine": "livekit",
      "summary": {
        "video_average_bitrate": 441000.0,
        "video_median_bitrate": 477000.0,
        "video_average_packets_loss_ratio": 0.0,
        "audio_average_bitrate": 31000.0,
        "audio_median_bitrate": 32000.0,
        "audio_average_packets_loss_ratio": 0.0
      },
      "recordings": [ { "id": "0190a1f0-…", "status": "available" } ],
      "provider_inspector_url": "https://cloud.livekit.io/projects/p_1uvc9dsiv14/sessions/RM_UNAo9Dh8Pq9t",
      "created_at": "2026-06-10T09:00:00Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}

結果を取得する

GET/api/v1/connection-tests/{connection_test_id}

id(UUID 形式)で 1 件取得します。匿名テストもこの経路でのみ参照できます。計測結果(summary / baseline / load)を内包したリソース表現を返します。

エラー

条件レスポンス
不存在 or 他テナント404 NOT_FOUND
リクエスト
curl …/api/v1/connection-tests/0190a200-… \
  -H "Authorization: ApiKey key_live_a1b2.sk_…"
200 レスポンス
{
  "id": "0190a200-…",
  "status": "completed",
  "measurement_completed": true,
  "external_type": "User",
  "external_id": "999",
  "rtc_engine": "livekit",
  "dummy_stream_count": 5,
  "connected_subscriber_count": 5,
  "summary": { "video_average_bitrate": 441000.0, "video_median_bitrate": 477000.0 },
  "baseline": { "video_average_bitrate": 1850000.0, "fps": 29.8 },
  "load": { "video_average_bitrate": 1520000.0, "worst_fps": 18.0 },
  "recordings": [ { "id": "0190a1f0-…", "status": "available" } ],
  "provider_inspector_url": "https://cloud.livekit.io/projects/p_1uvc9dsiv14/sessions/RM_UNAo9Dh8Pq9t",
  "completed_at": "2026-06-10T09:05:00Z",
  "created_at": "2026-06-10T09:00:00Z"
}

W5 connection_test.completed

W5connection_test.completed

接続テストの計測完了時に送信されます。minedia-www はこれを受けて GET /connection-tests/{connection_test_id} で計測値を取得し、合格判定(User.webrtc_test_status)を実施します。

data フィールド意味
connection_test_id完了した接続テストの公開 ID(結果取得に使用)
external_ref被験者参照 { external_type, external_id }(匿名テストは双方 null
statuscompleted / failed
Webhook ペイロード
{
  "id": "evt_0190a210-…",
  "version": "1",
  "type": "connection_test.completed",
  "occurred_at": "2026-06-10T09:05:00Z",
  "data": {
    "connection_test_id": "0190a200-…",
    "external_ref": { "external_type": "User", "external_id": "999" },
    "status": "completed"
  }
}
システム

署名鍵 (JWKS)

参加トークン(EdDSA 署名)の検証用公開鍵セットです。kid で鍵をローテーションします。

エンベロープ・認証について

このエンドポイントは共通エンベロープ(success / data)に包まれず、RFC 7517 標準形をそのまま返します。認証不要です。

公開鍵を取得する

GET/.well-known/jwks.json

認証不要。Model A では主に基盤内部の検証用ですが、テナント側での事前検証にも利用可能です。

JWKS(RFC 7517)
{
  "keys": [
    {
      "kty": "OKP",
      "crv": "Ed25519",
      "kid": "interview-2026-06",
      "x": "...",
      "use": "sig"
    }
  ]
}
システム

ヘルスチェック

基盤 API down 時のフォールバック判断を minedia-www が行うための死活確認です。

エンベロープ・認証について

health 系は共通エンベロープに包まれず、素の liveness / readiness を返します。認証不要です。

liveness

GET/health

認証不要。浅い liveness(プロセス生存のみ)を確認します。正常時 200、異常時 503 を返します。

200 OK
{
  "status": "ok"
}

readiness

GET/health/ready

DB / Redis / ストレージなど依存先まで含む準備状態を確認します。負荷分散・デプロイ制御用です。正常時 200、依存先異常時 503 を返します。

二重運用期のフォールバック

二重運用期に minedia-www は作成前に /health で基盤の可用性を見て、down ならフォールバック判断を行います(案件途中のエンジン切替は不可=作成時に確定)。

200 OK
{
  "status": "ready",
  "checks": {
    "db": "ok",
    "redis": "ok"
  }
}