Webhook

プロジェクトページで 1 つ以上の Webhook URL を設定します。有効な Webhook は、各イベントごとに HMAC-SHA256 で署名された JSON を POST で受け取ります。配信失敗 (5xx / 408 / 429 / ネットワークエラー) は指数バックオフで再試行 (30 秒 → 2 分 → 10 分 → 1 時間、最大 5 回)。再試行時は X-Nesh-Attempt ヘッダに試行回数が入ります。

各 Webhook はイベント種別の一部だけにフィルタできます。デフォルト (全チェックボックス選択) は全イベントを受信します。

イベント

notification.sent通知の送信完了後 (ダッシュボード / REST / cron)。
subscription.created新規エンドポイントの購読 (再購読は発火しません)。
subscription.removed購読が削除された — クライアントの DELETE か、push サービスが 404/410 を返した場合。

notification.sent

POST <your-webhook-url>
Content-Type: application/json
X-Nesh-Event: notification.sent
X-Nesh-Signature: sha256=<hex hmac of body>

{
  "type": "notification.sent",
  "notification": {
    "id":              "uuid",
    "project_id":      "uuid",
    "title":           "Hello",
    "body":            "From Nesh",
    "url":             "https://example.com",
    "delivered":       11,
    "removed":         1,
    "failed":          0,
    "target_user_ids": ["alice"],
    "sent_at":         "2026-05-05T18:00:00.000Z"
  }
}

subscription.created

X-Nesh-Event: subscription.created

{
  "type": "subscription.created",
  "subscription": {
    "id":               "uuid",
    "project_id":       "uuid",
    "endpoint":         "https://fcm.googleapis.com/...",
    "external_user_id": "alice",
    "created_at":       "2026-05-05T18:00:00.000Z"
  }
}

subscription.removed

reason: "client" は SDK / ダッシュボードからの DELETE、reason: "expired" は送信時に push サービスが 404/410 を返し、Nesh が失効した購読を自動で削除したことを示します。

X-Nesh-Event: subscription.removed

{
  "type": "subscription.removed",
  "subscription": {
    "project_id":       "uuid",
    "endpoint":         "https://fcm.googleapis.com/...",
    "external_user_id": "alice",
    "reason":           "client",
    "removed_at":       "2026-05-05T18:00:00.000Z"
  }
}

署名を検証する (Node.js)

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(body, signatureHeader, secret) {
  const expected = "sha256=" + createHmac("sha256", secret).update(body).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader);
  return a.length === b.length && timingSafeEqual(a, b);
}

本文は生バイトで比較してください (JSON パースの前にリクエストボディを読む)。Webhook 配信は 5 秒タイムアウト、非 2xx レスポンスは最終配信エラーとして記録されダッシュボードに表示されます。