開發者
以 API 提供的 Euphona 引擎
透過 api.euphonaai.com/v1 提供的單一 REST 介面規格。非同步工作搭配即時 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
}
}設計
它是如何設計的
四項沒有商量餘地的決定,因為每一項都決定了一個整合能不能撐過第一次服務中斷。
一切都是工作
功能端點會回傳 202 與一個工作資源。沒有同步處理的端點,以後也絕不會有:以母帶後製品質處理一首三分鐘的歌,本來就不是一次請求/回應就能完成的事,而假裝可以的 API,只會讓每個整合都寫錯。
音訊繞過 API,而不是穿過 API
POST /v1/files 會在 files.euphonaai.com 上回傳一個簽章、可續傳的上傳目標。結果會以有時效的簽章下載 URL 傳回。API 本身是一個 JSON 控制平面,從不傳送任何一個位元組的音訊。
這個 API 本身就是產品,不是附屬品
Euphona Studio 使用的是另一套第一方 API。這是刻意的:兩者是共用同一個引擎的獨立產品,所以 Studio 想要的改動影響不到這份介面規格,而這份規格 12 個月的支援期,也不是 Studio 必須付出的代價。坦白說,這樣的結果是 Studio 可能做得到這個 API 做不到的事:如果這對你很重要,請告訴我們,它就會成為產品藍圖上的一個項目,而不是讓你意外的事。
文件本身就是介面規格
OpenAPI 3.1 文件是唯一的依據。伺服器路由與型別都由它產生,而一項把關部署的測試會確保實際提供的路由與宣告的操作完全一致:因此,文件與實際提供的內容一旦出現落差,建置就會直接失敗,而不是要靠我們承諾會留意。從同一份文件產生的 SDK,會隨開發者預覽版一起推出。
逐步說明
一個整合的樣貌
建立檔案、把位元組送到簽章目標、提交工作,然後透過 SSE 看它完成。四個步驟,其中只有第三步會因功能而不同。
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
# 重新連線,不會遺失任何內容。參考
API 介面
每項功能共用的平台介面。每項功能只會新增一個建立端點;它回傳的工作會流經以下所有環節:相同的生命週期、相同的事件、相同的報告。
| 方法 | 路徑 | 用途 |
|---|---|---|
| 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 | 取消;在處理(process)階段開始前不收費。 |
| POST | /v1/jobs/{id}/input | 回答處於 awaiting_input 狀態的工作所提出的問題,並讓它繼續。不計費。 |
| GET | /v1/jobs/{id}/events | 在工作的整個生命週期內提供伺服器推送事件(SSE)。 |
| POST | /v1/projects | 將檔案與工作分組;每個專案各有一份 Track Graph。 |
| GET | /v1/projects/{id} | 單一專案,以及其檔案與工作 ID。 |
| GET | /v1/usage | 額度狀態,以及兩個計量額度池。 |
| GET | /v1/webhook-endpoints | 這把金鑰所屬模式下的 Webhook 端點。 |
| POST | /v1/webhook-endpoints | 註冊一個 https URL;在儲存任何內容之前,它要先回應一次簽章挑戰。 |
| GET | /v1/webhook-endpoints/{id} | 單一端點。它的密鑰不會再次顯示。 |
| PATCH | /v1/webhook-endpoints/{id} | 變更事件、描述或 URL;新的 URL 會重新驗證。 |
| DELETE | /v1/webhook-endpoints/{id} | 刪除;要傳給它但尚未傳送的事件會被捨棄。 |
| POST | /v1/webhook-endpoints/{id}/rotate-secret | 新的簽章密鑰;舊密鑰在過渡期內繼續簽章。 |
| GET | /v1/events | 三十天內的事件,以及每次傳送的結果。 |
| GET | /v1/events/{id} | 依傳送原樣呈現的單一事件,以及它的傳送紀錄。 |
Webhook
在工作完成時收到通知
工作完成或等待你回答時送出的簽章通知,整合不必一直保持串流連線,也不必輪詢。
- 事件
job.completed、job.failed、job.canceled與job.awaiting_input。每一個都是事件,其data就是輪詢傳回的那個工作資源,完全相同。- 工作區事件
- 當一個已儲存的上傳檔案被定下可能被移除的日期時傳送
file.expiring,data為該檔案;當某個用量池達到其上限的 80% 或 100% 時傳送usage.threshold。兩者都會傳到任一模式的端點,每個檔案一次,或每個門檻與週期一次。 - 註冊
- 需要具有
webhooks:manage權限範圍的金鑰,新金鑰預設沒有這項權限。URL 必須使用 https,且只能解析為公開位址;在儲存任何內容之前,它必須以回傳挑戰值的 2xx 回應來回答簽章挑戰。傳送只透過 IPv4 進行,因此該主機必須有 IPv4 位址。 - 傳送
- 至少傳送一次,所以請依
Euphona-Event-Id去除重複。10 秒內未傳回 2xx 的傳送會以指數退避重試 24 小時,之後轉為無法傳送,並保留在GET /v1/events中。重新導向一律不跟隨,順序也不保證:請以工作本身為準進行核對。 - 簽章
Euphona-Signature包含一個時間戳記,以及以端點密鑰為金鑰、針對時間戳記、一個句點與原始主體計算的 HMAC-SHA256。請在剖析前驗證,以固定時間比較,並拒絕與你的時鐘相差超過五分鐘的時間戳記。- 來源位址
- 傳送會從
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);
});
}營運
凌晨三點會出事的那些地方
這六項行為,值得在你寫整合之前就讀,而不是等它把你吵醒之後才讀。
- 金鑰
eu_live_…與eu_test_…,歸屬於某個工作區,只在建立時顯示一次,並以雜湊形式儲存。輪替時可設定最長 30 天的重疊期,所以輪替不會造成服務中斷。測試金鑰在自己的每月上限內免費,並在獨立的排程等級中執行;它們回傳的是預覽而不是母帶,所以你不必購買任何東西,就能從頭到尾完成整合。- 錯誤
- 一律使用 RFC 9457
application/problem+json,並附上穩定、機器可讀的code、給人看的detail,以及請求 ID。請依code分支處理,絕不要依說明文字:說明文字會在地化,而 code 是介面規格的一部分。 - 冪等性
- 每個 POST 都使用
Idempotency-Key。相同的鍵搭配相同的主體,只要它建立的資源還存在,就會重播原本的回應;相同的鍵搭配不同的主體則會被拒絕。因此,逾時後的重試會回傳原本的工作,而不是再開始一個要向你收費的新工作。 - 限制
- 每個金鑰採用滑動時間窗,並提供標準的
RateLimit-*標頭;隨用隨付為每分鐘 60 個請求,承諾用量合約則更高。限制器只管控請求頻率:處理量由你的額度與佇列決定。 - 兩個計量,絕不合一
- API 金鑰的工作計入隨用隨付額度池;Studio 工作階段的工作則扣用方案分鐘數。兩個額度池誰都無法耗盡另一個,而
GET /v1/usage會同時回報兩者。支出上限預設為開啟。儲存空間只有一個額度,而不是兩個:用金鑰上傳的檔案和 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_ | 確認是你本人 |