webhook は、アカウントで何かが起きたときに、Sakuranbo AI から自分のサーバーに HTTP リクエストを送る仕組みです。webhook を使うと、ポーリングせずに画像や動画のジョブの完了を知り、入金が届いたときに記録し、残高が少なくなったときに警告を受け取れます。
配信は Standard Webhooks の仕様に従い、すべて署名されています。サーバーは、リクエストを信頼する前に署名を検証する必要があります。署名を検証するを参照してください。
エンドポイントは、次のイベントを購読できます: job.succeeded, job.failed, job.cancelled, deposit.credited, balance.low。webhook.test も送られますが、テストイベントを依頼したときだけで、購読はできません。
job.succeeded: 画像または動画のジョブがcompletedになりました。dataは、GET /v1/jobs/{id}が返すものと同じジョブで、outputsに署名付き URL が入ります。job.failed: ジョブがfailedまたはexpiredになりました。dataはそのジョブで、data.statusがどちらかを示します。failedのジョブでは、data.errorに理由が入ります。expiredのジョブでは、data.errorはnullです。モデルの提供元が内容を理由に拒否したジョブも、このイベントで届き、data.error.codeはcontent_policy_violationです。job.cancelled: ジョブがcancelledになりました。dataはそのジョブです。deposit.credited: 入金が残高に反映されました。dataには、金額、日付、残高が入ります。balance.low: 利用可能な残高が、ダッシュボードの設定で指定したアラートの基準を下回りました。dataには、利用可能な残高とアラートの基準が入ります。webhook.test: 依頼したテストイベントです。
ジョブのステータスはジョブを、残高は残高とキーの利用上限を参照してください。
イベントの本文
Section titled “イベントの本文”すべての配信の本文は、次の 4 つのフィールドを持つ JSON です。
id: イベントの ID です。webhook-idヘッダの値にもなります。type: イベントの種類です。timestamp: イベントが起きた時刻で、UTC の ISO 8601 形式です。data: イベントの内容で、種類によって異なります。
知らないイベントの種類やフィールドは無視してください。Sakuranbo AI は、予告なく新しいイベントやフィールドを追加することがあります。
低残高アラート
Section titled “低残高アラート”アラートの基準は、ダッシュボードの設定で指定します。balance.low イベントは、利用可能な残高が基準以上から基準未満になるたびに送られます。次のイベントが送られるのは、残高が基準以上に戻ったあとだけです。課金、入金した残高の失効、残高を減らす調整が、きっかけになります。金額の確保だけでは、きっかけになりません。基準を指定していない間は、何も送られません。
エンドポイントを作成する
Section titled “エンドポイントを作成する”エンドポイントは、イベントを受け取る URL です。ダッシュボードでも API でも作成できます。
ダッシュボードでは、Webhookページを開きます。エンドポイントの作成と編集、配信の停止と再開、テストイベントの送信、秘密のローテーション、配信ログの確認ができます。
API では、POST /v1/webhook-endpoints を呼び出します。
curl -X POST "https://api.sakuranbo.ai/v1/webhook-endpoints" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $SAKURANBO_API_KEY" \ -d '{ "url": "https://example.com/webhooks/cherry", "events": [ "job.succeeded", "job.failed", "job.cancelled" ]}'const response = await fetch("https://api.sakuranbo.ai/v1/webhook-endpoints", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.SAKURANBO_API_KEY}`, }, body: JSON.stringify({ "url": "https://example.com/webhooks/cherry", "events": [ "job.succeeded", "job.failed", "job.cancelled" ] }),});const data = await response.json();console.log(data);import os
import requests
response = requests.post( "https://api.sakuranbo.ai/v1/webhook-endpoints", headers={ "Authorization": f"Bearer {os.environ['SAKURANBO_API_KEY']}", }, json={ "url": "https://example.com/webhooks/cherry", "events": [ "job.succeeded", "job.failed", "job.cancelled", ], },)print(response.json())リクエスト本文には、次のフィールドがあります。
| 名前 | 型 | 必須 | 既定値 | 制約 |
|---|---|---|---|---|
url | string | はい | 1〜2,048 文字 | |
events | string[] | いいえ | 次のいずれか: job.succeeded, job.failed, job.cancelled, deposit.credited, balance.low; 1 件以上 | |
description | string | いいえ | 最大 256 文字 |
events を省略すると、エンドポイントは、作成した時点にあるすべてのイベントの種類を購読します。あとで追加されたイベントの種類は、エンドポイントに追加されません。受け取るには、PATCH /v1/webhook-endpoints/{id} を呼び出し、新しい種類を含むイベントの種類の全一覧を events に入れます。events は、一覧全体を置き換えます。
レスポンスには、エンドポイントの ID と、署名の秘密 secret が入ります。秘密は whsec_ で始まります。秘密の全文が現れるのは、このリクエストのレスポンスと、秘密のローテーションのレスポンスだけです。あとから読み取ることはできないため、すぐに保存してください。紛失したときは、秘密をローテーションしてください。
URL は、次の規則に従う必要があります。
httpsと既定のポートを使います。- ホストは公開された DNS 名です。IP アドレス、
localhost、ドットのない名前、内部向けの名前は拒否されます。ユーザー名やパスワードを含む URL も拒否されます。 - パスとクエリ文字列を含められます。
規則に反する URL には、400 と invalid_request を返します。error.param は url で、error.details.reason にどの規則に反したかが入ります。自分のマシンでテストするときは、公開の HTTPS トンネルを使って、外部から届くようにしてください。
1 つのアカウントに作成できるエンドポイントは、10 件までです。上限に達しているときは、既存のエンドポイントを削除してから追加してください。
配信の仕組み
Section titled “配信の仕組み”Sakuranbo AI は、各イベントを、JSON の本文と次のヘッダを持つ POST リクエストとして送ります。
webhook-id: イベントの ID です。webhook-timestamp: この試行の時刻で、Unix 秒です。webhook-signature: 署名です。署名を検証するを参照してください。
サーバーの応答は、次の規則で扱われます。
- 15 秒以内の
2xxは、イベントを受け取ったことを示します。時間のかかる処理は、応答してから行ってください。 - Sakuranbo AI は、リダイレクトに従いません。
3xxは失敗として扱われます。 410 Goneを返すと、エンドポイントが停止します。停止中に起きたイベントは溜められず、再開しても再び送られません。再開するには、enabledをtrueにしてPATCH /v1/webhook-endpoints/{id}を呼び出すか、ダッシュボードを使います。そのあとで、失敗した配信を手動で再送してください。- それ以外のステータス、タイムアウト、接続の失敗は、失敗として扱われます。Sakuranbo AI は再試行し、最初の試行を含めて合計で最大 8 回、約 31 時間にわたって送ります。最後の試行のあと、配信は
failedになります。
受信側は、次の点を踏まえて書いてください。
- イベントは、複数回届くことがあります。1 つのイベントのどの試行も、
webhook-idと本文が同じです。処理した ID を保存して、重複を読み飛ばしてください。 - イベントの順序は保証されません。
timestampと、dataの状態を使ってください。 - 処理しないイベントの種類にも
2xxを返してください。再試行されなくなります。
署名を検証する
Section titled “署名を検証する”各配信は、エンドポイントの秘密で署名されています。署名を検証すると、リクエストが Sakuranbo AI から送られたもので、誰も書き換えていないことを確かめられます。方式は Standard Webhooks のバージョン v1 で、HMAC-SHA256 を使います。
webhook-id、webhook-timestamp、webhook-signatureヘッダを読み取ります。1 つでも欠けていれば、リクエストを拒否します。- タイムスタンプが、現在時刻の前後 5 分以内であることを確かめます。これで、過去のリクエストの再利用(リプレイ)を防げます。
- 鍵を作ります。秘密から接頭辞
whsec_を取り除き、残りを base64 からデコードします。鍵は、デコードしたバイト列であり、秘密の文字列そのものではありません。 - 署名の対象を作ります。ID ヘッダの値、ドット、タイムスタンプヘッダの値、ドット、生のリクエスト本文を、この順につなげたものです。
- 鍵を使って、署名の対象の HMAC-SHA256 を計算し、結果を標準の base64 でエンコードします。
- 署名ヘッダには、スペースで区切られた 1 つ以上の項目が入ります。各項目は、バージョン、カンマ、署名の順です。バージョンが
v1でない項目は読み飛ばします。いずれかの項目が計算結果と一致すれば、リクエストは有効です。比較は、定数時間で行ってください。 - JSON は、署名が有効だと確かめてから解析します。
受け取ったバイト列そのものを検証してください。JSON を解析してから直列化し直す Web フレームワークでは、バイト列が変わり、署名が一致しなくなることがあります。パーサーが動く前に、生の本文を読み取ってください。
TypeScript:
import { createHmac, timingSafeEqual } from "node:crypto";
/** * Verifies a Sakuranbo AI webhook (Standard Webhooks) and returns the parsed event. * rawBody must be the request body exactly as received, before any JSON parsing. */export function verifyWebhook( secret: string, // "whsec_..." as shown when the endpoint was created or its secret rotated rawBody: string, headers: Record<string, string | undefined>, // header names in lower case toleranceSeconds = 300,) { const id = headers["webhook-id"]; const timestamp = headers["webhook-timestamp"]; const signatures = headers["webhook-signature"]; if (!id || !timestamp || !signatures) throw new Error("missing webhook headers");
const sentAt = Number(timestamp); const now = Math.floor(Date.now() / 1000); if (!Number.isInteger(sentAt) || Math.abs(now - sentAt) > toleranceSeconds) { throw new Error("timestamp outside the tolerance"); }
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest();
// During a secret rotation the header holds two signatures, separated by a space. for (const entry of signatures.split(" ")) { const [version, signature] = entry.split(","); if (version !== "v1" || !signature) continue; const received = Buffer.from(signature, "base64"); if (received.length === expected.length && timingSafeEqual(received, expected)) { return JSON.parse(rawBody); } } throw new Error("no matching signature");}Python:
import base64import hashlibimport hmacimport jsonimport time
def verify_webhook(secret: str, raw_body: bytes, headers: dict, tolerance_seconds: int = 300): """Verify a Sakuranbo AI webhook (Standard Webhooks) and return the parsed event.
raw_body must be the request body exactly as received, before any JSON parsing. """ headers = {name.lower(): value for name, value in headers.items()} message_id = headers.get("webhook-id") timestamp = headers.get("webhook-timestamp") signatures = headers.get("webhook-signature") if not (message_id and timestamp and signatures): raise ValueError("missing webhook headers")
try: sent_at = int(timestamp) except ValueError: raise ValueError("invalid timestamp") from None if abs(int(time.time()) - sent_at) > tolerance_seconds: raise ValueError("timestamp outside the tolerance")
if secret.startswith("whsec_"): secret = secret[len("whsec_"):] key = base64.b64decode(secret) signed = f"{message_id}.{timestamp}.".encode() + raw_body expected = hmac.new(key, signed, hashlib.sha256).digest()
# During a secret rotation the header holds two signatures, separated by a space. for entry in signatures.split(" "): version, _, signature = entry.partition(",") if version != "v1" or not signature: continue if hmac.compare_digest(base64.b64decode(signature), expected): return json.loads(raw_body) raise ValueError("no matching signature")Standard Webhooks のライブラリも、同じ検証を行います。自分で書くよりライブラリを使いたいときは、npm または PyPI の standardwebhooks パッケージを使ってください。
秘密をローテーションする
Section titled “秘密をローテーションする”秘密を紛失したときや、漏えいした可能性があるときは、秘密をローテーションします。ダッシュボードか POST /v1/webhook-endpoints/{id}/rotate-secret を使います。レスポンスに新しい秘密が入り、読み取れるのはこのときだけです。
ローテーションのあと 24 時間は、署名ヘッダに 2 つの署名が入り、1 つ目が新しい秘密、2 つ目が前の秘密のものです。その間は、どちらの秘密を持つ受信側でも、リクエストを検証できます。サーバーを新しい秘密に切り替えてください。その時間が過ぎると、前の秘密は使えなくなります。エンドポイントの previous_secret_expires_at に、その時刻が入ります。
その間にもう一度ローテーションすると、最初のローテーションより前の秘密は、すぐに使えなくなります。配信に署名するのは、最新の 2 つの秘密だけです。
テストイベントを送る
Section titled “テストイベントを送る”POST /v1/webhook-endpoints/{id}/test は、エンドポイントが購読しているイベントに関係なく、1
つのエンドポイントに webhook.test
イベントを送ります。エンドポイントが有効である必要があります。レスポンスは、ステータスが pending
の新しい配信です。ダッシュボードにも、同じ操作のボタンがあります。
curl -X POST "https://api.sakuranbo.ai/v1/webhook-endpoints/we_0123456789abcdef0123456789abcdef/test" \ -H "Authorization: Bearer $SAKURANBO_API_KEY"const response = await fetch("https://api.sakuranbo.ai/v1/webhook-endpoints/we_0123456789abcdef0123456789abcdef/test", { method: "POST", headers: { Authorization: `Bearer ${process.env.SAKURANBO_API_KEY}`, },});const data = await response.json();console.log(data);import os
import requests
response = requests.post( "https://api.sakuranbo.ai/v1/webhook-endpoints/we_0123456789abcdef0123456789abcdef/test", headers={ "Authorization": f"Bearer {os.environ['SAKURANBO_API_KEY']}", },)print(response.json())エンドポイントのすべての配信は、GET /v1/webhook-endpoints/{id}/deliveries とダッシュボードで新しい順に確認できます。各項目には、ステータス、試行回数、最後の HTTP ステータス、次の試行の時刻、サーバーからの応答の先頭部分が入ります。ログに、イベントの本文は含まれません。Sakuranbo AI は、ログを 30 日間保管します。
配信を再送するには、POST /v1/webhook-endpoints/{id}/deliveries/{deliveryId}/retry を呼び出すか、ダッシュボードを使います。配信は同じイベント ID と同じ本文で送られ、新しい一連の試行が始まります。再送できるのは、succeeded か failed で終わった配信だけで、エンドポイントが有効である必要があります。
管理 API
Section titled “管理 API”これらのエンドポイントはすべて、API キーが必要です。認証を参照してください。
POST /v1/webhook-endpoints: エンドポイントを作成します。GET /v1/webhook-endpoints: エンドポイントを一覧します。GET /v1/webhook-endpoints/{id}: エンドポイントを 1 件読み取ります。PATCH /v1/webhook-endpoints/{id}: URL、イベント、説明、エンドポイントが有効かどうかを変更します。DELETE /v1/webhook-endpoints/{id}: エンドポイントと配信ログを削除します。POST /v1/webhook-endpoints/{id}/test: テストイベントを送ります。POST /v1/webhook-endpoints/{id}/rotate-secret: 新しい秘密を作成します。GET /v1/webhook-endpoints/{id}/deliveries: 配信ログを読み取ります。POST /v1/webhook-endpoints/{id}/deliveries/{deliveryId}/retry: 配信を再送します。
| コード | HTTP ステータス | 種別 | 説明 |
|---|---|---|---|
invalid_request | 400 | invalid_request_error | リクエストの形式が不正か、値が範囲外です。有効な値が分かる場合は details.allowed に入ります。 |
unauthorized | 401 | authentication_error | API キーが指定されていない、形式が不正、存在しない、失効済み、または期限切れです。レスポンスからは、どれに当たるかは分かりません。 |
webhooks_unavailable | 503 | api_error | この環境では webhook をまだ使えません。時間をおいて再試行してください。 |
ほかのエンドポイントは、エンドポイントまたは配信がアカウントにないときに、404 と not_found を返します。停止中のエンドポイントへのテストイベントや再送には、400 と invalid_request を返します。すべてのコードは、エラーを参照してください。
価格はすべて USD 表示です。表示価格には当社のマージンを含みます。