Skip to content

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-Signaturet=<UNIX秒>,v1=<HMAC-SHA256(HEX)>

Webhook URL を知る第三者は、あなたのエンドポイントに偽の配信結果を POST できます。 受信側では必ず署名を検証してください。

署名は ${t}.${リクエストボディ} を、署名シークレットを鍵とする HMAC-SHA256 で計算した値です。

  1. X-Todoke-Signature から t と v1 を取り出す
  2. ${t}.${生のリクエストボディ} を組み立てる(パース前の生のボディを使う — JSON を再シリアライズすると空白やキー順が変わり署名が一致しません)
  3. 署名シークレットを鍵として HMAC-SHA256 を計算し、v1 とタイミングセーフに比較する
  4. t が現在時刻から離れすぎていないか確認する(リプレイ攻撃対策。5 分程度を推奨)
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() を先に通すと生のボディが失われます。

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;
}
  • 配信は 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 日の保持期間が適用されます。アプリまたはアカウントを削除すると、配信ログも連鎖削除されます。