API リファレンス (Studio /v1)
Studio API の公開面。すべて /v1 prefix を持ちます。エラーは RFC 9457 (application/problem+json) envelope。SSE は text/event-stream。認証は OIDC Cookie + サーバ側 JWT (15 分短命 + refresh)。
API の全体規約
共通の HTTP 規約とエラー形式。すべての API は /v1 配下です。
認証
- OIDC (Google Workspace / Auth0 / Keycloak) → Cookie セッション (HttpOnly Secure SameSite=Lax)
- サーバ側 JWT: 15 分短命 + refresh
- 未認証時は
401+WWW-Authenticate: Bearer - RBAC:
rbac.matrix_json設定。違反時は403
エラー envelope (RFC 9457)
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{
"type": "https://errors.suntory-nedo.io/bundle-not-ready",
"title": "Bundle not ready",
"status": 409,
"detail": "Bundle bdl_01H... is still building (phase=rpeak).",
"instance": "/v1/bundles/bdl_01H.../manifest",
"trace_id": "abc123..."
} レート制限
未指定。テナント単位の cost cap (bq.daily_tenant_cap_usd) と user 単位の cost cap (bq.daily_user_cap_usd) が事実上のレート制御となる。超過時は 402 Payment Required。
idempotency
書き込み系 (POST /bundles, /jobs/*, /drafts) は Idempotency-Key ヘッダで重複防止可能。Bundle 作成は加えて canonical_hash でサーバ側 idempotency を持つ (詳細: studio § canonical_hash)。
1. Health & Catalog
API の死活確認。認証不要。
Response 200
{ "status": "ok", "version": "0.1.0", "uptime_sec": 3600 } 非表示でない catalog source を返す。include_hidden=true で隠しソースも取得。
Query
include_hidden(boolean, optional, default false) — `is_hidden=true` のソースも含める (admin)signal_family(string, optional) — `ecg` / `eeg` / `ppg` / `acc` / `vital` / `meta` のいずれかでフィルタ
Response 200
[
{
"source_key": "xhro_04.eeg",
"dataset": "xhro_04",
"table_name": "eeg",
"signal_family": "ecg",
"display_name": "XHRO-04 ECG (ch1-ch2 差動)",
"sample_rate_hz": 250,
"is_hidden": false
},
{ "source_key": "xhro_04.acc", "signal_family": "acc", "sample_rate_hz": 25, ... }
] 被験者検索。raw UID / email は返さない (PII redaction)。
Query
q(string, optional) — `hid` または `public_id` の prefixcohort(string, optional) — `cohort_tags` 内の一致limit(int, default 50, max 200)offset(int, default 0)
Response 200
{
"items": [
{
"public_id": "sub_01HZMX9ABCDE00000000XH015",
"expt_id": "XHRO-04",
"hid": "XH015",
"cohort_tags": ["XHRO-04", "XH015"]
}
],
"total": 12,
"limit": 50,
"offset": 0
} 2. Recordings & Availability
指定 subject の Recording 一覧を返す。Postgres mirror から読む (BQ には触らない、FR-X1)。
Path
public_id— `sub_` prefix ULID
Response 200
[
{
"id": "rec_01HZMX...",
"source_key": "xhro_04.eeg",
"expt_id": "XHRO-04",
"start_ms": 1699578000000,
"end_ms": 1699839878000,
"duration_hours": 72.8,
"signal_families": ["ecg", "acc", "opt", "vital"]
}
] 10 秒 bucket の family 別データ密度を返す。`start_ms` / `end_ms` は必須。
Query
start_ms(int, required) — 区間開始 epoch msend_ms(int, required) — 区間終了 epoch ms (`end_ms - start_ms ≤ 7 日`)families(csv, optional) — `ecg,acc,opt,vital` の絞り込み
Response 200
{
"recording_id": "rec_01HZMX...",
"bucket_ms": 10000,
"buckets": [
{
"ts_ms": 1699578000000,
"ecg": { "rows": 2500, "leadoff_ratio": 0.0, "motion_high": false },
"acc": { "rows": 250 },
"opt": { "rows": 500 },
"vital": { "rows": 10, "wearing_ratio": 1.0 }
},
{ "ts_ms": 1699578010000, ... }
]
} 3. Preview & Estimate
pyramid L3 (10 秒集約) の概観波形を返す。未生成時は空 rows で graceful placeholder (404 にはしない)。
Query
start_ms/end_ms(int, required)lead(string, optional, default `ch1_minus_ch2`)
Response 200
{
"recording_id": "rec_01HZMX...",
"lead": "ch1_minus_ch2",
"rows": [
{ "ts_ms": 1699578000000, "min": -120.4, "max": 145.2, "mean": 3.1 },
{ "ts_ms": 1699578010000, "min": -118.0, "max": 142.5, "mean": 2.8 }
]
} Window と family から所要時間・コスト・予算状態の見積もりを返す。BQ dry-run と人間語サマリの両方を返す。3 秒以内目標 (FR-X5)。
Request
{
"source_key": "xhro_04.eeg",
"subject_public_id": "sub_01HZMX9ABCDE00000000XH015",
"windows": [
{ "start_ms": 1699578000000, "end_ms": 1699578300000 },
{ "start_ms": 1699664400000, "end_ms": 1699664700000 }
],
"attached_signals": ["acc"],
"preset_id": "xhro-multiday"
} Response 200
{
"human_summary": {
"duration_text": "約 30 秒で出来上がります",
"cost_text": "コスト目安: ¥80 以内",
"cap_status": "予算内"
},
"details": {
"bytes": 12400000,
"rows": 1500000,
"cost_usd": 0.0023,
"windows_count": 2,
"caps": {
"ui_preview": { "limit_usd": 0.05, "used_usd": 0.0023, "ok": true },
"preview_window": { "limit_usd": 0.50, "used_usd": 0.0023, "ok": true },
"windows_per_bundle_max": { "limit": 12, "used": 2, "ok": true }
},
"dry_run_id": "dr_01HZMX..."
}
} 4. Bundles
Bundle build を開始。canonical_hash が既存と一致する場合は 200 OK + 既存 ID を返し、新規 Build は走らない (FR-X6 idempotency)。新規 Build の場合は 202 Accepted + job_id。
Request
{
"source_key": "xhro_04.eeg",
"subject_public_id": "sub_01HZMX9ABCDE00000000XH015",
"windows": [
{ "start_ms": 1699578000000, "end_ms": 1699578300000, "label": "rest-1" },
{ "start_ms": 1699664400000, "end_ms": 1699664700000, "label": "rest-2" }
],
"attached_signals": ["acc"],
"preset_id": "xhro-multiday",
"ecg_params": {
"detector": "pan_tompkins_v1",
"rr_min_ms": 200,
"rr_max_ms": 2000,
"hrv_window_sec": 300,
"leads": ["ch1", "ch2"]
},
"schema_version": "1.0",
"client_idempotency_key": "a1b2c3d4-..."
} Response 200 (既存 Bundle 一致)
{
"bundle_id": "bdl_01HZMX9P2QR3S4T5U6V7W8X9Y",
"status": "ready",
"canonical_hash": "0123456789ab...",
"manifest_uri": "gs://sn-bundles/bundles/bdl_01HZMX.../manifest.json",
"reused": true
} Response 202 (新規 Build)
{
"bundle_id": null,
"job_id": "job_01HZMXJOB...",
"canonical_hash": "0123456789ab...",
"events_url": "/v1/jobs/job_01HZMXJOB.../events",
"estimated_seconds": 32
} Bundle 一覧。`subject_public_id`, `status`, `limit`, `offset` で絞り込み。
Query
subject_public_id(optional)status(optional, csv) — `ready` / `building` / `failed` / `archived`parent_bundle_id(optional) — 派生子のみlimit/offset
Response 200
{
"items": [
{
"bundle_id": "bdl_01HZMX9P2QR3S4T5U6V7W8X9Y",
"status": "ready",
"subject_public_id": "sub_01HZMX9ABCDE00000000XH015",
"signal_family": "ecg",
"windows_count": 2,
"bytes_total": 12345678,
"cost_usd": 0.0023,
"parent_bundle_id": null,
"derive_kind": null,
"built_at": "2026-05-25T12:34:56.789Z"
}
],
"total": 5,
"limit": 50,
"offset": 0
} Bundle summary を取得。
Response 200
{
"bundle_id": "bdl_01HZMX9P2QR3S4T5U6V7W8X9Y",
"status": "ready",
"canonical_hash": "0123456789ab...",
"schema_version": "1.0",
"subject_public_id": "sub_01HZMX9ABCDE00000000XH015",
"signal_family": "ecg",
"sample_rate_hz": 250,
"leads": ["ch1", "ch2"],
"preset_id": "xhro-multiday",
"attached_signals": ["acc"],
"windows": [
{ "ordinal": 0, "label": "rest-1", "start_ms": 1699578000000, "end_ms": 1699578300000 },
{ "ordinal": 1, "label": "rest-2", "start_ms": 1699664400000, "end_ms": 1699664700000 }
],
"lineage": { "parent_bundle_id": null, "derive_kind": null, "...": "..." },
"built_at": "2026-05-25T12:34:56.789Z"
} ready Bundle の manifest を返す。未 ready は 409 Conflict。
Response 200
application/json で bundle § manifest 完全例 と同じ構造を返す。
Response 409 (未 ready)
{
"type": "https://errors.suntory-nedo.io/bundle-not-ready",
"title": "Bundle not ready",
"status": 409,
"detail": "Bundle bdl_01H... is still building (phase=rpeak)."
} Bundle ファイルの署名付き URL へ 302 redirect。ブラウザは Studio API を経由せず GCS から直接ダウンロードする。署名 URL の TTL は storage.signed_url_ttl_sec (既定 900 秒)。
Path
path— `manifest.json` / `windows/win_0001.parquet` / `derived/r_peaks.parquet` などの相対パス
Response 302
HTTP/1.1 302 Found Location: https://storage.googleapis.com/sn-bundles/bundles/bdl_01H.../windows/win_0001.parquet?X-Goog-Signature=...&X-Goog-Expires=900 Cache-Control: private, max-age=900
5. Jobs (Build 制御 + SSE)
ジョブ状態を取得。
Response 200
{
"id": "job_01HZMXJOB...",
"kind": "bundle_build",
"status": "running",
"phase": "detect_rpeaks",
"progress": 0.62,
"canonical_hash": "0123456789ab...",
"bundle_id": null,
"checkpoint_uri": "gs://sn-bundles/_staging/job_01HZMXJOB.../rpeak/",
"created_at": "2026-05-25T12:34:00.000Z",
"started_at": "2026-05-25T12:34:01.123Z"
} Job 進捗を Server-Sent Events で配信。`Accept: text/event-stream` で接続。reconnect は最新 phase からリプレイされる。
イベント形式
event: progress
data: {"phase":"detect_rpeaks","fraction":0.62,"label":"R ピークを探しています…"}
event: progress
data: {"phase":"pyramid","fraction":0.85,"label":"概観波形を作っています…"}
event: done
data: {"bundle_id":"bdl_01HZMX9P2QR3S4T5U6V7W8X9Y","cost_usd":0.0023,"duration_sec":31.4}
event: error
data: {"code":"bq_cost_cap_exceeded","message":"BQ scan exceeded ui_preview cap","retry_after_sec":3600} phase の値
extract— BQ → GCS Parquetclean— HPF / notch / 体動 / lead-offdetect_rpeaks— Pan-Tompkins v1hrv— SDNN / RMSSD / pNN50 / LF/HFpyramid— L0..L3 生成attach— 付帯信号同期manifest— integrity hash + manifest 組み立てregister— GCS commit + Postgres + `_READY`
Locale 翻訳: label は API 側で `Accept-Language` から `ja` / `en` を選択して翻訳済みで送る。フロントは `fraction` でバー描画。
次の phase boundary で一時停止を要求。status: paused へ遷移。
Response 200
{ "status": "paused", "pause_at_phase": "pyramid" } checkpoint から再開。新しい job_id は作らない (同 job の続行)。同 canonical_hash の Bundle が既に ready なら再実行せず done を返す。
Response 200
{ "status": "running", "resumed_from_phase": "rpeak" } queued は即時 cancel、running は canceling にして boundary で停止。GCS の中間ファイルは TTL ライフサイクル (7 日) で自動削除。
Response 200
{ "status": "canceled", "canceled_at_phase": "pyramid" } 6. Drafts & Recipes
Wizard 中断状態を保存。24h TTL (FR-X13)。`PUT /v1/drafts/{id}` で上書き。
Request
{
"payload": {
"step": 3,
"source_key": "xhro_04.eeg",
"subject_public_id": "sub_01HZMX9...",
"windows": [{ "start_ms": ..., "end_ms": ..., "label": "rest-1" }],
"attached_signals": ["acc"],
"preset_id": "xhro-multiday"
}
} Response 200
{
"id": "drf_01HZMX...",
"expires_at": "2026-05-26T12:34:00.000Z",
"updated_at": "2026-05-25T12:34:00.000Z"
} Recipe の一覧 / 作成 / 取得 / 更新 / 削除 / 適用 (FR-X11)。Apply は build ではなく draft 反映 (Recipe を draft に展開 → ユーザーが Build ボタンを押す)。
Recipe スキーマ
{
"id": "rcp_01HZMX...",
"scope": "project",
"scope_id": "prj_01HZMX...",
"display_name": "夜間 5 分 × 3 (Holter)",
"description": "00:00-04:00 から lead-off 少ない 5 分 × 3 を自動抽出",
"spec": {
"applies_to": { "signal_family": "ecg", "min_duration_hours": 6 },
"window_picker": {
"strategy": "auto_quality_top_k",
"k": 3,
"duration_sec": 300,
"time_range": "00:00-04:00",
"exclude": ["lead_off", "motion_high"]
},
"attached": ["acc"],
"preset_id": "holter-overnight"
},
"created_at": "2026-05-25T12:34:00.000Z"
} Apply
POST /v1/recipes/{id}/apply
{
"draft_id": "drf_01HZMX...",
"subject_public_id": "sub_01HZMX...",
"recording_id": "rec_01HZMX..."
}
→ 200 { "draft_id": "drf_01HZMX...", "applied_windows_count": 3 } 7. Settings
DefaultRegistry の全エントリを返す (フロントが自動フォームを描く元データ)。
Response 200
{
"entries": [
{
"key": "bq.cost_cap_usd.ui_preview",
"type": "float",
"min": 0, "max": 100,
"default": 0.05,
"scope_max": "tenant",
"overridable_at": ["tenant"],
"hot_reload": true,
"secret": false,
"description": "UI プレビュー dry-run 1 回あたりの USD 上限",
"affects": ["Studio Step4", "BigQueryAccessService.dry_run"],
"ui": { "component": "money", "currency": "USD" }
},
{ "key": "llm.api_key", "type": "secret", "scope_max": "tenant", "secret": true, "...": "..." }
]
} 指定 scope の全設定値を取得。継承解決後の値 (effective value)。
Response 200
{
"scope": "tenant",
"scope_id": "tnt_01HZMX...",
"values": {
"bq.cost_cap_usd.ui_preview": { "value": 0.05, "source": "tenant" },
"bq.cost_cap_usd.preview_window": { "value": 0.50, "source": "default" },
"llm.api_key": { "value": "sk-•••••abc", "source": "tenant", "secret": true }
}
} 設定値の更新。schema validation を通す。機密値 (secret=true) は KMS 暗号化付きで保存。`settings_audit` に who / when / old_hash / new_hash が残る (機密は hash のみ、非機密は raw)。Hot reload 対象は Redis `settings:changed` チャネルへ即配信。
Request
{ "value": 0.10 } # 非機密
{ "value": "sk-ant-..." } # 機密 (secret=true のキー) Response 200
{
"scope": "tenant",
"key": "bq.cost_cap_usd.ui_preview",
"old_value": 0.05,
"new_value": 0.10,
"hot_reload": true,
"audit_id": 12345
} Settings 変更通知の購読。他者が編集した値に即追従するため。
イベント形式
event: changed
data: {"scope":"tenant","scope_id":"tnt_01HZMX...","key":"bq.cost_cap_usd.ui_preview","new_value":0.10,"changed_by":"usr_01HZMX..."}
event: secret_changed
data: {"scope":"tenant","scope_id":"tnt_01HZMX...","key":"llm.api_key","new_hash":"abc123..."} 8. Copilot
Studio Copilot の会話 1 ターン。Phase 1 は in-memory thread (永続化なし)。
Request
{
"thread_id": "thr_01HZMX...",
"message": "夜間で lead-off が 5% 未満の 5 分 Window を 3 つ選んで",
"context": {
"recording_id": "rec_01HZMX...",
"subject_public_id": "sub_01HZMX..."
}
} Response 200
{
"thread_id": "thr_01HZMX...",
"messages": [
{ "role": "assistant", "content": "夜間 (00:00-04:00 JST) の Window を 3 件提案します。" }
],
"tool_calls": [
{
"tool": "studio.suggest_windows",
"input": { "recording_id": "rec_01HZMX...", "intent": "夜間 5 分 × 3", "constraints": {...} },
"output": { "windows": [{...}, {...}, {...}] }
}
],
"cost_usd": 0.0042,
"tokens_used": 1842
} Window / Recipe / Preset 候補を返す。`build-bundle` は 常に 501 Not Implemented (Agent からの直接 Build を禁止、人間承認必須)。
使えるツール
propose-windows— 自然言語 + recording から Window 候補propose-recipe— 直近操作から Recipe 候補propose-preset— Window 特性から preset 候補propose-build-bundle— 501 を返す (HUMAN_ONLY)
Response 501 (build-bundle のみ)
{
"type": "https://errors.suntory-nedo.io/human-approval-required",
"title": "Human approval required",
"status": 501,
"detail": "Bundle build cannot be invoked from Copilot. Use the Studio UI [エクスポート] button."
} OpenAPI スペック
正本の OpenAPI は packages/api-client/openapi.json に生成されます (Pydantic v2 起点)。TS クライアントは openapi-typescript で同 package に生成。CLI / E2E テストでも共有可能です。
# OpenAPI 取得
curl http://localhost:8080/openapi.json > apps/studio-api/openapi.json
# TS 型生成
pnpm --filter @suntory-nedo/api-client gen
# 利用例 (Next.js)
import createClient from "openapi-fetch";
import type { paths } from "@suntory-nedo/api-client";
const api = createClient<paths>({ baseUrl: "/api" });
const { data, error } = await api.GET("/v1/bundles", { params: { query: { status: "ready" } } });