Xero の Webhook 配信の仕組み

Xero は Webhook を有効にする前に、5 秒以内にボディなしの 200 を、不正な署名には 401 を返すよう求めます。検証済みの署名形式と Node の検証コードを掲載しています。

配信の挙動

プロバイダーの公式ドキュメントから読み取っています(最終確認 2026-08-06)。プロバイダーが数値を示していない項目には、この表も数値を載せていません。

応答期限5 秒

5 秒以内に、ボディも Cookie もない 200 を返す。署名が正しくなければ 401

ハンドラーが期限内に応答するか確認する

リトライプロバイダーは未公表
打ち切りプロバイダーは未公表
出典https://developer.xero.com/documentation/guides/webhooks/configuring-your-server/

署名

ドキュメントを読んだだけではなく、テストスイートで裏付けた動作する検証コードで確認しています。最終検証日は 2026-08-20 です。

アルゴリズムHMAC-SHA256
署名対象raw body
エンコードbase64
ヘッダーx-xero-signature = base64
許容範囲適用なし

検証する

JSON としてパースする前の生のリクエストボディを、バイト単位でそのまま使います。フレームワークがペイロードを再シリアライズした時点で、このページのどの方式も検証に失敗します。

const crypto = require("node:crypto");

// base64(HMAC-SHA256(webhooks key, raw body))
const expected = crypto
  .createHmac("sha256", webhooksKey) // developer.xero.com → My Apps → Webhooks
  .update(rawBody)
  .digest("base64");
const received = Buffer.from(req.headers["x-xero-signature"] ?? "");
const wanted = Buffer.from(expected);
// timingSafeEqual throws on a length mismatch, so check that first.
const valid =
  received.length === wanted.length && crypto.timingSafeEqual(received, wanted);
// Intent to Receive needs both answers: 200 (empty body) when valid, 401 when not.

落とし穴

Intent to Receive に通るまで、Webhook はオフのままです。Xero は正しく署名した配信と、わざと誤った署名を付けた配信を送り、前者には 200、後者には 401 を求めます。何にでも 200 を返すエンドポイントは、いつまでも有効になりません。Xero の送信元と Webhook キーを設定しておけば、AnyHook が両方に自分で応答し、どちらも保存しません。

配信に含まれるのはレコードではなく、ポインターのバッチです。各イベントにはカテゴリー、タイプ、認証付きの呼び出しで取得する resourceUrl が入っています。AnyHook はバッチの最初のイベントからイベント名(invoice.update)を付けるため、フィルターでもイベントログでも同じ名前で扱えます。

あわせて読む

AnyHook は、Xero から受信するエンドポイントの手前に入ります。約 1 秒で応答し、サーバーがダウンしていれば独自のスケジュールでリトライします。すべてのイベントは再送できる状態で保存されます。 変えるのは URL 1 つだけで、コードはそのままです。

仕組みを見る →