本文へ移動
ドキュメント · Webhook

Webhook

Usherは、環境で起きたことを、起きたその時にサーバーへ知らせることができます。誰かが登録した、サインインした、情報を変更した、退会した、といったことです。各イベントは、指定したアドレスへの署名付きPOSTとして届き、サーバーが応答しなければ再試行されます。

エンドポイントを追加する

Usher Consoleで環境を開き、Webhook → 新規エンドポイントを選びます。自分のhttps://のアドレスを指定し、受け取るイベントを選んでください。Consoleは、エンドポイントの署名シークレット(whsec_…)を一度だけ表示します。サーバーのほかのシークレットと一緒に保管してください。

イベントタイミング
user.created誰かがアカウントを作成した
user.updatedアカウントが変更された:メールアドレス、名前、確認状態、利用停止(SCIMを介した、組織のディレクトリによる利用停止を含む)
user.deletedアカウントが削除された
session.created誰かがサインインした
session.endedセッションが終了した:サインアウト、失効、または早期の期限切れ

テストを送信を使うと、購読しているイベントにかかわらず、そのエンドポイントだけにwebhook.testイベントが送られます。

届くもの

リクエスト
POST /usher/webhooks HTTP/1.1
content-type: application/json
user-agent: Usher-Webhooks/1
webhook-id: msg_2fk7x…
webhook-timestamp: 1793643116
webhook-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
ボディ
{
  "type": "user.created",
  "timestamp": "2026-11-02T18:11:56.412Z",
  "data": {
    "user": {
      "id": "Qz3m…",
      "email": "lena@acme.com",
      "emailVerified": false,
      "name": "Lena Live",
      "image": null,
      "banned": false,
      "createdAt": "2026-11-02T18:11:56.380Z",
      "updatedAt": "2026-11-02T18:11:56.380Z"
    }
  }
}

イベントが表すのはユーザーとセッションであり、シークレットは決して含まれません。トークンも、パスワードのハッシュも、ロールも、利用停止の理由も含まれません。

すべての配信を確認する

UsherはStandard Webhooksの仕様に沿って署名するため、その仕様に対応したライブラリなら、どれでも署名を確認できます。検証に失敗したものは、すべて拒否してください。これらのライブラリは、5分以上ずれたタイムスタンプも拒否するため、古い配信を再送する攻撃は成立しません。

app/usher/webhooks/route.ts
// a route that receives Usher's webhooks (Node, the standardwebhooks package)
import { Webhook } from 'standardwebhooks'

const wh = new Webhook(process.env.USHER_WEBHOOK_SECRET!) // whsec_…, from Usher Console

export async function POST(request: Request) {
  const body = await request.text() // the raw body: the signature covers it byte for byte
  let event: { type: string; timestamp: string; data: Record<string, unknown> }
  try {
    event = wh.verify(body, Object.fromEntries(request.headers)) as typeof event
  } catch {
    return new Response('bad signature', { status: 400 })
  }
  // webhook-id is the same on every attempt: skip one you have handled already
  if (await alreadyHandled(request.headers.get('webhook-id')!)) return new Response(null, { status: 204 })
  await queue(event) // answer quickly; do the work afterwards
  return new Response(null, { status: 204 })
}
  • 15秒以内に応答する:任意の2xxで応答してください。それ以外の応答は、リダイレクトも含めて失敗とみなされます。
  • 重複を想定する:メッセージは、どの試行でも同じwebhook-idを持ちます。処理済みのものはスキップしてください。
  • 順序に頼らない:再試行が、新しいイベントよりあとに届くことがあります。

サーバーが応答しないとき

Usherは、5秒後、5分後、30分後、2時間後、5時間後、10時間後、さらに10時間後に再試行し(合計で約1日半)、それでも失敗した場合は諦めます。Consoleの配信ログには、各メッセージの試行と、サーバーの最後の応答が表示されます。再送信を使うと、配信を最初からやり直せます。

新しいシークレット

Consoleの新しいシークレットを使うと、別のシークレットが作成され、一度だけ表示されます。その後24時間は、各配信にシークレットごとに1つずつ、計2つの署名が付くため、何も取りこぼすことなく新しいシークレットをデプロイできます。それ以降は、新しいシークレットだけが署名に使われます。

配信元

WebhookはUsherのエグレスプロキシを通じて送信され、このプロキシはプライベートなアドレスや内部のアドレスには接続しません。エンドポイントは、公開された名前を使うhttps://のアドレスでなければなりません。IPアドレス、プライベートな名前、Usher自身のドメイン上のアドレスは使えません。