Webhook
This content is not available in your language yet.
配信結果 Webhook を設定すると、通知の配信が完了したタイミングで todoke が指定の URL に POST します。Pro プランの機能です。
ダッシュボードのアプリ詳細画面で Webhook URL を保存します。保存すると署名シークレット(whsec_ で始まる文字列)が発行され、その画面でのみ表示されます。再表示はできないため、必ず控えてください(紛失した場合は URL を保存し直すと再発行されます)。
URL は https:// のみ設定できます。内部ホスト(localhost・プライベート IP・リンクローカル)は拒否されます。
{ "event": "notification.delivered", "app_id": "8f14e45f-ceea-467a-9575-1b1e0f0d1f1a", "notification_id": "b3a1c2d4-...", "sent_count": 128, "failed_count": 2, "timestamp": 1752345678}| ヘッダー | 内容 |
|---|---|
X-Todoke-Event | イベント名(現在は notification.delivered のみ) |
X-Todoke-Signature | t=<UNIX秒>,v1=<HMAC-SHA256(HEX)> |
署名の検証(必須)
Section titled “署名の検証(必須)”Webhook URL を知る第三者は、あなたのエンドポイントに偽の配信結果を POST できます。 受信側では必ず署名を検証してください。
署名は ${t}.${リクエストボディ} を、署名シークレットを鍵とする HMAC-SHA256 で計算した値です。
X-Todoke-Signatureからtとv1を取り出す${t}.${生のリクエストボディ}を組み立てる(パース前の生のボディを使う — JSON を再シリアライズすると空白やキー順が変わり署名が一致しません)- 署名シークレットを鍵として HMAC-SHA256 を計算し、
v1とタイミングセーフに比較する tが現在時刻から離れすぎていないか確認する(リプレイ攻撃対策。5 分程度を推奨)
Node.js
Section titled “Node.js”import crypto from "node:crypto";
const TOLERANCE_SEC = 300;
export function verifyTodokeSignature(rawBody, header, secret) { const t = Number(header.match(/t=(\d+)/)?.[1]); const v1 = header.match(/v1=([0-9a-f]{64})/)?.[1]; if (!t || !v1) return false;
// 古いリクエストの再送(リプレイ)を拒否する if (Math.abs(Math.floor(Date.now() / 1000) - t) > TOLERANCE_SEC) return false;
const expected = crypto .createHmac("sha256", secret) .update(`${t}.${rawBody}`) .digest("hex");
// 文字列の === 比較は先頭一致の度合いで処理時間が変わり、署名を推測されうる return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));}Express なら express.raw({ type: "application/json" }) で生のボディを受け取ってから検証してください。express.json() を先に通すと生のボディが失われます。
Cloudflare Workers
Section titled “Cloudflare Workers”const enc = new TextEncoder();
async function verify(rawBody, header, secret) { const t = Number(header.match(/t=(\d+)/)?.[1]); const v1 = header.match(/v1=([0-9a-f]{64})/)?.[1]; if (!t || !v1) return false; if (Math.abs(Math.floor(Date.now() / 1000) - t) > 300) return false;
const key = await crypto.subtle.importKey( "raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"], ); const sig = await crypto.subtle.sign("HMAC", key, enc.encode(`${t}.${rawBody}`)); const expected = Array.from(new Uint8Array(sig), (b) => b.toString(16).padStart(2, "0")).join(""); return expected === v1;}再送とタイムアウト
Section titled “再送とタイムアウト”- 配信は best-effort です。2xx 以外が返った場合は 1 回だけ即時リトライします
- 5 秒でタイムアウトします
- リダイレクトは追従しません(3xx は失敗として扱われます)
- Webhook の失敗は通知の配信自体には影響しません(Push は既に送信済みです)
受信側は同じ notification_id が複数回届きうる前提で、冪等に処理してください。
Webhook URL を設定すると、アプリ詳細画面の Webhook セクションに直近 10 件の最終試行結果が表示されます。各行では日時、成功または失敗、HTTP status、エラー種別、send_id の先頭 8 文字を確認できます。
API から取得する場合は GET /api/v1/apps/:id/webhook-deliveries を使います。limit は 1〜100(既定値 20)、before は前ページの next_before を指定する時刻 cursor です。レスポンスには id、send_id、notification_id、status_code、error、attempts、attempted_at が含まれます。
成功時は error が null になります。失敗時の error は timeout、network_error、redirect、または http_<status> のいずれかです。HTTP 応答を観測できなかった場合の status_code は null です。配信先 URL やレスポンス本文はログに保存されません。
配信ログは現在のプランに応じて、Free では 7 日、Pro では 90 日保持されます。Pro から Free へ変更した場合も、次回の定期削除から 7 日の保持期間が適用されます。アプリまたはアカウントを削除すると、配信ログも連鎖削除されます。