開発者向け
Euphona のエンジンを API で
api.euphonaai.com/v1で提供する、1つのREST API仕様。SSEでリアルタイムに進捗を確認できる非同期ジョブ、署名付きのファイル転送、プレフィックス付きのキー、無料のテストモード。独自に12か月のサポート期間を設けた、ファーストパーティの製品です。
キーはStudioの設定で作成します。eu_test_…はすぐに無料で、eu_live_…はカードを登録すれば同じ画面から作成でき、最初のジョブから課金されます。テストモードでは、仕様のすべてが動作します。
すべてはジョブ
curl -X POST https://api.euphonaai.com/v1/masters \
-H "Authorization: Bearer $EUPHONA_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"file_id": "file_8Kd2mQTz3XaV7pLw"}'
HTTP/2 202
{
"id": "job_7Vd2mHs5Yq8Tz3Xa",
"object": "job",
"type": "mastering",
"status": "queued",
"stage": null,
"metering": {
"pool": "payg",
"charged_minutes": 0,
"test_mode": false
}
}設計
設計の考え方
譲れない4つの決定です。どれも、最初の障害を乗り越えられる連携と、そうでない連携とを分けるものだからです。
すべてはジョブ
オーディオはAPIを通らず、迂回する
このAPIは製品そのもの。副産物ではない
ドキュメントが仕様そのもの
手順
連携の全体像
ファイルを作成し、署名付きのアップロード先にデータを送り、ジョブを送信して、SSEで完了まで見届けます。4つのステップのうち、機能ごとに異なるのは3つ目だけです。
1:ファイルを作成し、アップロード先を取得する
curl -X POST https://api.euphonaai.com/v1/files \
-H "Authorization: Bearer $EUPHONA_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"filename":"take-07.wav","bytes":58210044,"content_type":"audio/wav"}'
{
"id": "file_8Kd2mQ...",
"object": "file",
"status": "awaiting_upload",
"upload": { "protocol": "...", "url": "https://files.euphonaai.com/...", "expires_at": "..." }
}2:データは私たちではなく、files.に送る
# upload.protocolを読み取ってください。決め打ちはしないでください。
# 再開可能なアップロードの仕組みは、仕様の範囲内での実装上の選択であり、
# メジャーバージョンを上げずに変わることがあります。だからこそ、ドキュメントではなくフィールドなのです。3:ジョブを見守る
curl -N https://api.euphonaai.com/v1/jobs/job_4Rb.../events \
-H "Authorization: Bearer $EUPHONA_API_KEY"
event: job.progress
data: {"status":"analyzing","stage":"analyze","progress":{"fraction":0.4}}
event: job.completed
data: {"status":"succeeded","result":{...},"metering":{"pool":"payg","charged_minutes":3.4}}4:ジョブ自体と突き合わせる
curl https://api.euphonaai.com/v1/jobs/job_4Rb... \
-H "Authorization: Bearer $EUPHONA_API_KEY"
{"id":"job_4Rb...","status":"succeeded","result":{...},
"metering":{"pool":"payg","charged_minutes":3.4}}
# ジョブリソースは、ステートマシンの現時点での正しい状態です。ストリームで何が見えても、
# 何を見逃しても、行動する前に読み直してください。切断されたSSE接続は
# Last-Event-IDで再接続し、何も失いません。リファレンス
共通エンドポイント
すべての機能が共有するプラットフォームのエンドポイントです。機能ごとに作成用のエンドポイントが1つ加わり、そこから返されるジョブは、以下のすべてを通ります。ライフサイクルも、イベントも、レポートも同じです。
| メソッド | パス | 内容 |
|---|---|---|
| POST | /v1/files | ファイルを作成し、署名付きで再開可能なアップロード先を取得します。 |
| GET | /v1/files/{id} | 取り込みの状態、測定した長さ、サンプルレート、チャンネル数、フォーマット。 |
| DELETE | /v1/files/{id} | ファイルと、そこから作られたものを削除します。ジョブが使用中の間は拒否されます。 |
| GET | /v1/files/{id}/download | 有効期限付きの署名付きダウンロードURL。 |
| GET | /v1/jobs | 統合された読み取り用エンドポイント。カーソルでページ分割されます。 |
| GET | /v1/jobs/{id} | 状態、ステージ、進捗、課金、測定レポート。 |
| POST | /v1/jobs/{id}/cancel | キャンセルします。処理ステージが始まる前なら課金されません。 |
| POST | /v1/jobs/{id}/input | awaiting_inputのジョブからの問いに答えて、再開します。課金対象外です。 |
| GET | /v1/jobs/{id}/events | ジョブが終わるまでのサーバー送信イベント。 |
| POST | /v1/projects | ファイルとジョブをまとめます。Track Graphはプロジェクトごとです。 |
| GET | /v1/projects/{id} | 1つのプロジェクトと、そのファイルIDとジョブID。 |
| GET | /v1/usage | 利用枠の状態と、2つの課金プール。 |
| GET | /v1/webhook-endpoints | このキーのモードのWebhookエンドポイント。 |
| POST | /v1/webhook-endpoints | https のURLを登録します。何かが保存される前に、署名付きチャレンジに応答します。 |
| GET | /v1/webhook-endpoints/{id} | エンドポイント1件。シークレットは二度と表示されません。 |
| PATCH | /v1/webhook-endpoints/{id} | イベント、説明、URLを変更します。新しいURLは再度検証されます。 |
| DELETE | /v1/webhook-endpoints/{id} | 削除します。未配信のイベントは破棄されます。 |
| POST | /v1/webhook-endpoints/{id}/rotate-secret | 新しい署名シークレット。古いものも移行期間中は署名を続けます。 |
| GET | /v1/events | 30日分のイベントと、各配信の結果。 |
| GET | /v1/events/{id} | 配信されたとおりのイベント1件と、その配信状況。 |
Webhook
ジョブの完了を受け取る
ジョブが完了したとき、またはあなたの回答を待っているときに届く署名付きの通知です。ストリームを開いたままにしたり、ポーリングしたりする必要はありません。
- イベント
job.completed、job.failed、job.canceled、job.awaiting_input。いずれもイベントで、そのdataはポーリングで返るものとまったく同じジョブリソースです。- ワークスペースのイベント
file.expiringは、保存されているアップロードに削除されうる日付が付いたときに送られ、dataはそのファイルです。usage.thresholdは、利用枠が上限の80%または100%に達したときに送られます。どちらもテスト・本番どちらのモードのエンドポイントにも届き、ファイルごとに1回、またはしきい値と期間ごとに1回です。- 登録
webhooks:manageスコープを持つキーが必要です。新しいキーにはこのスコープはありません。URLはhttpsを使い、公開アドレスにのみ名前解決される必要があります。また、何かが保存される前に、署名付きチャレンジをそのまま返す2xxレスポンスで応答する必要があります。配信はIPv4でのみ送られるため、ホストにはIPv4アドレスが必要です。- 配信
- 少なくとも1回届くため、
Euphona-Event-Idで重複を除いてください。10秒以内に2xx以外が返った場合は、指数バックオフで24時間再試行し、その後はデッドレターとしてGET /v1/eventsに残します。リダイレクトには従わず、順序も保証されません。ジョブと突き合わせてください。 - 署名
Euphona-Signatureには、タイムスタンプと、タイムスタンプ・ドット・未加工のボディに対するHMAC-SHA256(キーはエンドポイントのシークレット)が含まれます。パースする前に検証し、定数時間で比較し、手元の時計と5分以上ずれたタイムスタンプは拒否してください。- 送信元アドレス
- 配信は
65.21.69.162から送られます。ファイアウォールで受信トラフィックを絞っている場合はこれらを許可してください。それでも署名は必ず検証してください。リクエストの送信元アドレスは、誰が送ったかの証明にはなりません。
配信を検証する(Node.js)
import { createHmac, timingSafeEqual } from "node:crypto";
// 届いたとおりの未加工のボディを渡してください。パースして再シリアライズした
// JSONは、署名されたメッセージではありません。
export function verifyEuphonaSignature(rawBody, header, secret, now = Date.now() / 1000) {
const parts = header.split(",").map((part) => part.trim().split("="));
const t = Number(parts.find(([key]) => key === "t")?.[1]);
const signatures = parts.filter(([key]) => key === "v1").map(([, value]) => value);
if (!Number.isFinite(t) || Math.abs(now - t) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
return signatures.some((value) => {
const given = Buffer.from(value, "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
});
}運用
深夜3時に困ること
連携を書く前に読んでおくべき6つの挙動です。夜中に起こされてからでは遅すぎます。
- キー
eu_live_…とeu_test_…。ワークスペースが所有し、作成時に1回だけ表示され、保存時はハッシュ化されます。最大30日間の重複期間を設けてローテーションできるので、ローテーションで障害が起きることはありません。テスト用のキーは専用の月間上限の範囲内で無料で、専用のスケジューリングクラスで実行されます。マスターではなくプレビューを返すため、何も購入せずに連携を最初から最後まで構築できます。- エラー
- 常にRFC 9457の
application/problem+jsonで、機械可読で安定したcode、人が読むためのdetail、リクエストIDを含みます。分岐は文章ではなく、必ずcodeで行ってください。文章はローカライズされますが、コードは仕様の一部です。 - 冪等性
- すべてのPOSTに
Idempotency-Keyを付けます。同じキーと同じボディなら、作成したリソースが存在する限り元のレスポンスを再送します。同じキーで異なるボディは拒否されます。そのため、タイムアウト後に再試行しても、課金される2つ目のジョブが始まるのではなく、元のジョブが返されます。 - 制限
- キーごとのスライディングウィンドウで、標準の
RateLimit-*ヘッダーを返します。従量課金では1分あたり60リクエスト、利用量コミットメント契約ではそれ以上です。リミッターが制御するのはリクエストの頻度だけで、スループットは利用枠とキューによって決まります。 - メーターは1つではなく2つ
- APIキーによるジョブは従量課金のプールに計上され、Studioのセッションによるジョブはプランの処理時間を消費します。どちらのプールももう一方を減らすことはなく、
GET /v1/usageは両方を報告します。利用上限額はデフォルトでオンです。ストレージの利用枠は2つではなく1つです。キーでアップロードしたファイルとAPIのジョブの結果は、Studioのすべてのものと合わせて、ワークスペースのプランのストレージの利用枠(無料プランでは2 GB)に計上され、収まらないアップロードやジョブはstorage_fullで拒否されます。 - Request-Id
- 成功でも失敗でも、すべてのレスポンスに付きます。サポートへの問い合わせでお伝えいただければ、ジョブから各ステージまで、実際に何が起きたかを端から端まで追跡できます。
code | title |
|---|---|
| unauthenticated | 認証が必要 |
| invalid_ | 無効なAPIキー |
| key_ | 取り消されたAPIキー |
| insufficient_ | スコープの不足 |
| email_ | メールアドレスの確認が必要 |
| malformed_ | 不正な形式のリクエスト |
| validation_ | リクエストの検証に失敗 |
| unsupported_ | 非対応のメディアタイプ |
| idempotency_ | 別のボディでの冪等キーの再利用 |
| not_ | 見つかりません |
| job_ | キャンセルできないジョブ |
| job_ | 入力待ちではないジョブ |
| file_ | 準備ができていないファイル |
| file_ | 有効期限切れのファイル |
| file_ | 使用中のファイル |
| file_ | 法的な理由で利用できないファイル |
| unsupported_ | 非対応のオーディオ形式 |
| unsupported_ | 非対応のサンプルレート |
| unsupported_ | 非対応のチャンネル数 |
| file_ | 長すぎるオーディオ |
| file_ | 短すぎるオーディオ |
| file_ | 大きすぎるファイル |
| corrupt_ | デコードできないオーディオ |
| rate_ | レート制限の超過 |
| too_ | 実行中のジョブ数の超過 |
| quota_ | 利用枠の超過 |
| spend_ | 利用上限額に到達 |
| test_ | テストモードの上限に到達 |
| storage_ | ストレージの空き不足 |
| purchase_ | 購入が必要 |
| payment_ | 支払い方法が必要 |
| format_ | このプランに含まれない形式 |
| webhook_ | Webhook URLはHTTPSが必須 |
| webhook_ | Webhook URLは公開アドレスが必須 |
| webhook_ | Webhookの所有権確認に失敗 |
| internal_ | 内部エラー |
| service_ | サービス利用不可 |
| processing_ | 処理の失敗 |
| capacity_ | 処理能力の不足 |
| capability_ | 一時停止中 |
| workspace_ | ワークスペースの停止 |
| uploads_ | アップロードの一時停止 |
| reauthentication_ | 本人確認が必要 |