mcpField
W
webhook-setup

webhook-setup

github.com/zavudev開発1239 回インストール2 日前に更新
リポジトリ

webhook-setup について

Zavu の Webhook を設定し、メッセージ、配信、テンプレートのイベントを受け取る開発者向けスキルです。Webhook の種類、イベントペイロード、送信者設定、複数チャネルの受信処理を説明します。

主な機能

  1. 01送信者およびプロジェクトの Webhook 設定
  2. 02メッセージや配信状況イベントの選択
  3. 03共通イベント形式とペイロードの解釈
  4. 04複数チャネルの受信メッセージ処理
  5. 05送信者 Webhook シークレットの再生成
  6. 06メール添付ファイルのメタデータとリンク取得

活用シーン

  1. 01顧客からの受信メッセージの処理
  2. 02配信状況や既読状況の追跡
  3. 03テンプレート承認やカテゴリ変更への対応
  4. 04受信メールの添付ファイル取得

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/zavudev/zavu-skills --skill webhook-setup

インストール後、Claude Code で/webhook-setupを入力して読み込まれたか確認します。

詳細

Webhook の設定

使用する場面

Zavu から受信メッセージ、配信状況の更新、テンプレート承認通知を受け取るために Webhook エンドポイントを設定するときに、このスキルを使用します。

Sender とアカウント(1 段落)

Sender は Zavu-Sender として渡す API ハンドルです。アカウント(WhatsApp Business Account、Facebook Page、Telegram ボット、電話番号)は Sender が接続する先であり、課金対象です。Sender は無料です。ダッシュボードでアカウントを接続すると、その Sender が自動的に作成されます。GET /v1/senders で確認し、送信可能な内容は channels 配列を基準にしてください。全体像については channel-setup スキルを参照してください。

Webhook の種類

  • Sender Webhook:メッセージイベント(受信、配信状況、テンプレート)。Sender ごとに設定します
  • プロジェクト Webhook:プロジェクトレベルのイベント(パートナーへの招待)。プロジェクトごとに 1 つです。親プロジェクトにはサブアカウントのイベントは届きません。各サブアカウント固有の API キーを使って Webhook を設定してください(POST /v1/invitations/webhook)。

利用可能なイベント

イベントカテゴリ説明
message.inbound受信顧客からメッセージが届きました
conversation.new受信新しい連絡先から最初のメッセージが届きました
message.unsupported受信サポートされていない種類のメッセージを受信しました
contact.identity_linked連絡先BSUID で識別されていた WhatsApp の連絡先が電話番号に紐付けられた場合、電話番号で識別されていた連絡先が BSUID を受け取った場合、または連絡先の電話番号が変更された場合に発生します。data には contactId、bsuid、phoneNumber、reason(provider_reported、contact_shared、phone_changed、bsuid_changed)が含まれます。phone_changed の場合は previousPhoneNumber も含まれ、WhatsApp から BSUID が一度も報告されていない場合、bsuid は null です。2 つの連絡先が contactId に統合された場合は mergedContactId が含まれます。保持している BSUID で連絡先を識別できなくなった場合は previousBsuid が含まれます。紐付けごとに 1 回送信されます
message.queued送信メッセージが配信待ちになりました
message.sent送信メッセージが通信事業者に送信されました
message.delivered送信メッセージが受信者に配信されました
message.read送信受信者がメッセージを読みました
message.failed送信メッセージの配信に失敗しました
broadcast.status_changedブロードキャストブロードキャストのステータスが変更されました
template.status_changedテンプレートWhatsApp テンプレートの承認ステータスが変更されたか、Meta によって再分類されました。data には templateId、name、previousStatus、currentStatus、rejectionReason、category が含まれます
invitation.status_changed招待パートナーへの招待ステータスが変更されました(in_progress、completed、failed、cancelled、または再送時に pending に戻る)。期限切れの場合は送信されません
domain.verifiedドメインカスタムメールドメインの検証に成功しました
domain.failedドメインカスタムメールドメインの検証に失敗しました

SDK で Webhook を設定する

TypeScript - Webhook を設定して Sender を作成する
const sender = await zavu.senders.create({
  name: "My Sender",
  phoneNumber: "+15551234567",
  webhookUrl: "https://your-app.com/webhooks/zavu",
  webhookEvents: ["message.inbound", "message.delivered", "message.failed"],
});
// Store sender.webhook.secret securely - only shown once!
Webhook を更新する
await zavu.senders.update({
  senderId: "snd_abc123",
  webhookUrl: "https://new-url.com/webhooks",
  webhookEvents: ["message.inbound"],
  webhookActive: true,
});
シークレットを再生成する
const result = await zavu.senders.webhookSecret.regenerate({
  senderId: "snd_abc123",
});
console.log(result.secret); // whsec_new_secret...

Webhook ペイロードの構造

トップレベルのエンベロープはすべてのイベントで共通です。イベント固有のフィールドは data に格納されます。timestamp は Zavu が Webhook を送信した時刻(Unix ミリ秒)です。

{
  "id": "evt_1705312200000_abc123",
  "type": "message.inbound",
  "timestamp": 1705312200000,
  "senderId": "snd_abc123",
  "projectId": "prj_xyz789",
  "data": { }
}

受信メッセージデータ(message.inbound)

message.inboundでは、dataにメッセージが格納されます。受信時、toは自分の番号(メッセージの宛先)、fromは送信者です。

フィールド説明
messageIdZavuのメッセージIDです。
conversationId受信トレイのスレッドIDです。メッセージがスレッドに入った後にWebhookが送信されるため、常に設定されています。新しいスレッドの最初のメッセージには、conversation.newと同じIDが格納されます。リアクションでは省略されます。下記の「受信トレイへのディープリンク」を参照してください。
contactIdZavuの連絡先です。fromとは異なり、WhatsAppの連絡先の識別子がBSUIDと電話番号の間で切り替わっても変わりません。fromではなく、この値をキーとして使用してください。例外は、1人の連絡先2件が統合された場合です。このときcontact.identity_linkedには、旧IDがmergedContactIdとして格納されます。リアクションでは省略されます。
whatsappBSUIDが判明している場合のみ、WhatsAppで設定されます:{ bsuid, username? }。
from送信者です。1対1のメッセージでは連絡先、グループメッセージでは参加者になります。通常はE.164形式の電話番号ですが、ユーザー名を設定して電話番号を非公開にしたWhatsAppの連絡先の場合は、ビジネススコープのユーザーID(BSUID、例:US.13491208655302741918)です。これは不透明な値として扱い、返信時にはtoにそのまま渡してください。WhatsAppがそのメッセージに電話番号を含めた場合に限り電話番号となるため、同じ相手でも電話番号とBSUIDの間で切り替わることがあります。
to自分の番号(メッセージの宛先)です。
channel配信チャネルです(sms、whatsapp、telegram、email、instagram、messenger、voice)。
messageTypetext、image、videoなどです。location_requestへの返信は、新しいタイプではなくlocationとして届き、content.replyToMessageIdにリクエストが設定されます。これを照合に使用してください。
textテキスト本文またはメディアのキャプションです。存在する場合に設定されます。
providerTimestampWhatsApp、Telegram、Instagram、Messengerでは、プロバイダーが受信した元の時刻(Unix ms)です。SMSとメールではnullです。トップレベルのtimestampと比較して、配信の遅延を検知できます。
referralクリックしてWhatsAppでチャットする広告のアトリビューションです。WhatsAppのみで、広告から開始されたスレッドの最初のメッセージにのみ設定されます。下記の「クリックしてWhatsAppでチャットする広告のアトリビューション」を参照してください。
telegramChatIdTelegramのみ:返信先のチャットIDです。プレフィックスは含まれません。
instagramSenderIdInstagramのみ:返信先の送信者IDです。プレフィックスは含まれません。
messengerSenderIdMessengerのみ:返信先の送信者IDです。プレフィックスは含まれません。
同じ送信者から返信する

POST /v1/messagesで返信を送信し、ペイロードのトップレベルにあるsenderIdをZavu-Senderヘッダーに渡すと、連絡先がメッセージを送った送信者から返信が送られます。toに設定する値はチャネルによって異なります。

  • SMS、WhatsApp、メール:data.from。
  • Telegram:data.telegramChatId。Instagram:data.instagramSenderId。Messenger:data.messengerSenderId。これらのチャネルでは、data.fromにtelegram:/instagram:/messenger:のプレフィックスが含まれますが、toでは受け付けられず、送信は400エラーで拒否されます。
const { senderId, data } = event;
const to =
  data.channel === "telegram" ? data.telegramChatId :
  data.channel === "instagram" ? data.instagramSenderId :
  data.channel === "messenger" ? data.messengerSenderId :
  data.from;
await zavu.messages.send({ to, channel: data.channel, text: "Thanks, we got it.", "Zavu-Sender": senderId });
受信トレイへのディープリンク

conversation.newとmessage.inboundのどちらも、受信トレイのスレッドIDであるconversationIdをdataに含みます。チームがZavuダッシュボードで会話を開けるよう、直接リンクを作成してください。

https://dashboard.zavu.dev/{locale}/inbox?conv={conversationId}

{locale}はダッシュボードのUI言語です(例:en、es)。

message.inboundではconversationIdは常に設定され、リアクションでは省略されます。

返信/引用のコンテキスト

連絡先が以前のメッセージに返信(引用)した場合、message.inbound の data.content 内に含まれます。

フィールド説明
replyToMessageId引用されたメッセージの Zavu メッセージ ID です。引用されたメッセージが Zavu に保存されていない場合は省略されます。
replyToProviderMessageId引用されたメッセージのプロバイダーメッセージ ID(WhatsApp WAMID)です。返信の場合は常に含まれます。
replyToFrom引用されたメッセージの送信者(E.164)です。
replyToText引用されたメッセージのテキストの短縮スニペットです(メディアの場合は空です)。
replyToMessageType引用されたメッセージの種類(text、image、...)です。

WhatsApp の場合のみ設定されます。受信した Telegram メッセージでは、連絡先が何を引用したかは通知されません。自分のメッセージに引用を付けて返信するには、event.senderId の送信者から content.replyToMessageId: event.data.messageId を送信してください(send-message スキルを参照)。

Click-to-WhatsApp のアトリビューション

連絡先が Click-to-WhatsApp(CTWA)広告または投稿をタップしてあなたに連絡した場合、data.referral に流入元が含まれます。これは WhatsApp のみで、会話の最初のメッセージにのみ届き、その後のメッセージには含まれません。再送はないため、届いたら保存してください。オーガニックな会話には含まれません。

フィールド説明
ctwaClidClick-to-WhatsApp のクリック識別子です。Meta の Conversions API で、広告にコンバージョンを紐付ける際に必要です。ad ソースに含まれ、post には含まれません。
sourceIdクリックにつながった広告または投稿の識別子です。
sourceTypead または post です。
sourceUrl広告または投稿への Meta パーマリンクです。
headline広告または投稿の見出しです。
body広告または投稿の本文です。
mediaType広告にメディアが含まれていた場合は、image または video です。
imageUrl / videoUrl / thumbnailUrl広告メディアです。mediaType に一致するもののみ含まれます。
{
  "type": "message.inbound",
  "data": {
    "messageId": "jd7x2k3m4n5p6q7r8s9t0",
    "from": "+56912345678",
    "channel": "whatsapp",
    "messageType": "text",
    "text": "Hi, I saw your ad",
    "referral": {
      "sourceType": "ad",
      "sourceId": "120210000000000000",
      "headline": "Free first consultation",
      "mediaType": "image",
      "ctwaClid": "ARIzZm9vYmFyY3R3YWNsaWQ"
    }
  }
}

該当しないフィールドは空の値で送信されるのではなく省略されるため、読み取る前に存在を確認してください。GET /v1/messages/{messageId} は、同じオブジェクトを content.referral の下に返します。

メールの添付ファイル

受信メールは channel: "email" および messageType: "text" の message.inbound として届きます。Webhook ペイロードには本文(text と、取得したメッセージに含まれる htmlBody)のみが含まれ、添付ファイルのデータは含まれません。添付ファイルは別途保存され、必要に応じて取得します。

取得するには、Webhook の messageId を指定して GET /v1/messages/{messageId}/attachments を呼び出します。各添付ファイルのメタデータと、有効期限の短い署名付き downloadUrl が返されます(リクエストごとに再生成されるため、すぐに取得し、URL をキャッシュしないでください)。これは、添付ファイル付きで送信した送信メールにも使えます。添付ファイルが保存されていないメッセージでは、空のリストが返されます。

curl https://api.zavu.dev/v1/messages/MESSAGE_ID/attachments \
  -H "Authorization: Bearer $ZAVU_API_KEY"
{
  "items": [
    {
      "id": "att_abc123",
      "filename": "invoice.pdf",
      "mimeType": "application/pdf",
      "size": 102400,
      "contentId": null,
      "isInline": false,
      "downloadUrl": "https://...signed-url...",
      "createdAt": "2024-01-15T10:00:00.000Z"
    }
  ]
}

HTML 本文に埋め込まれたインライン画像には isInline: true と contentId が設定され、htmlBody 内では cid:<contentId> として参照されます。

パートナー招待データ(invitation.status_changed)

パートナー招待の保存済みステータスが変更されるたびに発火し、招待を作成したプロジェクトのプロジェクト Webhook に配信されます。同じステータスへの変更では何も送信されません。有効期限切れは保存済みの変更ではありません。expiresAt を過ぎた pending の招待は expired として読み取られますが、イベントは発生しません。data には次の内容が含まれます。

フィールド説明
invitationIdZavu の招待 ID です。
clientName / clientEmail招待の作成時に渡した値、または null です。
connectionTypeクライアントが接続する対象です。whatsapp_waba または messenger です。
previousStatus / currentStatusステータスの遷移です。
senderIdcompleted の場合に存在します。プロジェクトに作成された sender です。
wabaAccountIdWhatsApp 招待が completed の場合に存在します。WhatsApp Business Account ID です。
connectedAccountcompleted の場合に存在します。{ channel, id, name } — リンクされた WhatsApp 番号または Facebook ページです。
failureReasonfailed の場合に存在します。安定したコードです。fb_cancelled、fb_not_authorized、signup_abandoned、meta_no_pages、internal_error などがあります。不明なコードは一般的な失敗として扱ってください。

failed の招待は終端状態ではありません。同じリンクは引き続き使用でき、クライアントが再試行すると in_progress に戻ります。プロビジョニングを行うのは completed の場合だけにしてください。

{
  "id": "evt_1736850000000_abc123",
  "type": "invitation.status_changed",
  "timestamp": 1736850000000,
  "projectId": "jx7xyz789ghi012",
  "data": {
    "invitationId": "jh7am5bng9p3v2x1k4r8",
    "clientName": "Acme Corp",
    "clientEmail": "[email protected]",
    "connectionType": "messenger",
    "previousStatus": "in_progress",
    "currentStatus": "completed",
    "senderId": "sender_12345",
    "connectedAccount": {
      "channel": "messenger",
      "id": "1077492835456839",
      "name": "Acme Store"
    }
  }
}

署名の検証

ヘッダー:X-Zavu-Signature: t=<unix_seconds>[,v1=<hex>][,v2=<hex>]

部分対象
tUnix タイムスタンプ(秒)
v1HMAC_SHA256(secret, body)
v2HMAC_SHA256(secret, "{t}.{body}") — 現在の方式

正しいペイロードをハッシュ化しないと、配信はすべて 401 になります。 v1 の対象は本文のみです。 {t}.{body} を対象とするのは v2 だけです。送信側の webhook.signatureVersion(GET /v1/senders/{senderId})を確認すると、受信側がどちらを受け取るかがわかります。新しい sender のデフォルトは v2 です。それ以前に作成された sender は、移行するまで v1 を使用します。

v2 があればそちらを優先し、なければ v1 にフォールバックしてください。そうすれば、移行の前・最中・後にわたって同じ実装を使用できます。

アルゴリズム
1. read the RAW body (no JSON parser in front)
2. parse the header into { t, v1?, v2? }
3. reject if |now - t| > 300
4. signed = v2 present ? `${t}.${body}` : body
   expected = HMAC_SHA256(secret, signed)
5. constant-time compare against v2 ?? v1
TypeScript(Express)
import crypto from "crypto";
import express from "express";

const app = express();
// Raw body. NOT express.json() — the signature covers the exact bytes sent.
app.use("/webhooks/zavu", express.raw({ type: "application/json" }));

function verifyZavuSignature(rawBody: string, header: string, secret: string): boolean {
  if (!header) return false;

  const parts: Record<string, string> = {};
  for (const piece of header.split(",")) {
    const i = piece.indexOf("=");
    if (i > 0) parts[piece.slice(0, i)] = piece.slice(i + 1);
  }

  const t = Number(parts.t);
  if (!Number.isFinite(t)) return false;

  const age = Math.floor(Date.now() / 1000) - t;
  if (age > 300 || age < -60) return false;

  const received = parts.v2 ?? parts.v1;
  if (!received) return false;

  const signed = parts.v2 ? `${t}.${rawBody}` : rawBody;
  const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex");

  // Length first: timingSafeEqual throws on a mismatch.
  if (expected.length !== received.length) return false;
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}

app.post("/webhooks/zavu", (req, res) => {
  const rawBody = req.body.toString("utf8");
  if (!verifyZavuSignature(rawBody, req.headers["x-zavu-signature"] as string, process.env.ZAVU_WEBHOOK_SECRET!)) {
    return res.status(401).send("Invalid signature");
  }

  // Answer fast, then work. Zavu retries non-2xx, so a slow handler turns one
  // event into five.
  res.status(200).send("OK");
  processEvent(JSON.parse(rawBody)).catch(console.error);
});
Python(Flask)
import hashlib, hmac, os, time
from flask import Flask, request

app = Flask(__name__)

def verify_zavu_signature(raw_body: bytes, header: str, secret: str) -> bool:
    if not header:
        return False

    parts = {}
    for piece in header.split(","):
        key, sep, value = piece.partition("=")
        if sep:
            parts[key] = value

    try:
        t = int(parts["t"])
    except (KeyError, ValueError):
        return False

    age = int(time.time()) - t
    if age > 300 or age < -60:
        return False

    received = parts.get("v2") or parts.get("v1")
    if not received:
        return False

    signed = f"{t}.".encode() + raw_body if "v2" in parts else raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, received)

@app.post("/webhooks/zavu")
def zavu_webhook():
    raw_body = request.get_data()  # bytes, before parsing
    header = request.headers.get("X-Zavu-Signature", "")
    if not verify_zavu_signature(raw_body, header, os.environ["ZAVU_WEBHOOK_SECRET"]):
        return "Invalid signature", 401
    enqueue(request.get_json())
    return "OK", 200
Go
func verifyZavuSignature(rawBody []byte, header, secret string) bool {
	if header == "" {
		return false
	}

	parts := map[string]string{}
	for _, piece := range strings.Split(header, ",") {
		if k, v, ok := strings.Cut(piece, "="); ok {
			parts[k] = v
		}
	}

	t, err := strconv.ParseInt(parts["t"], 10, 64)
	if err != nil {
		return false
	}
	age := time.Now().Unix() - t
	if age > 300 || age < -60 {
		return false
	}

	received, hasV2 := parts["v2"]
	if !hasV2 {
		received = parts["v1"]
	}
	if received == "" {
		return false
	}

	mac := hmac.New(sha256.New, []byte(secret))
	if hasV2 {
		mac.Write([]byte(strconv.FormatInt(t, 10) + "."))
	}
	mac.Write(rawBody)

	return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(received))
}
Ruby(Sinatra)
def verify_zavu_signature(raw_body, header, secret)
  return false if header.nil? || header.empty?

  parts = {}
  header.split(',').each do |piece|
    key, _, value = piece.partition('=')
    parts[key] = value unless value.empty?
  end

  t = Integer(parts['t'], exception: false)
  return false if t.nil?

  age = Time.now.to_i - t
  return false if age > 300 || age < -60

  received = parts['v2'] || parts['v1']
  return false if received.nil?

  signed = parts['v2'] ? "#{t}.#{raw_body}" : raw_body
  expected = OpenSSL::HMAC.hexdigest('SHA256', secret, signed)

  OpenSSL.secure_compare(expected, received)
end
PHP
function verifyZavuSignature(string $rawBody, ?string $header, string $secret): bool {
    if (!$header) return false;

    $parts = [];
    foreach (explode(',', $header) as $piece) {
        $i = strpos($piece, '=');
        if ($i !== false) $parts[substr($piece, 0, $i)] = substr($piece, $i + 1);
    }

    if (!isset($parts['t']) || !ctype_digit($parts['t'])) return false;
    $t = (int) $parts['t'];

    $age = time() - $t;
    if ($age > 300 || $age < -60) return false;

    $received = $parts['v2'] ?? $parts['v1'] ?? null;
    if ($received === null) return false;

    $signed = isset($parts['v2']) ? "{$t}.{$rawBody}" : $rawBody;
    return hash_equals(hash_hmac('sha256', $signed, $secret), $received);
}

受信側が使用している方式の確認

sender で確認します。Webhook が設定されている場合、webhook.signatureVersion は常に存在します。

npx zavudev senders signature $SENDER_ID
endpoint   https://api.example.com/webhooks/zavu
signature  v1

Next step:
  npx zavudev senders update snd_abc --signature-version v1+v2

That sends both signatures at once, so your current receiver is unaffected.

npx zavudev senders list の signature 列を見ると、移行が必要な受信側を最も簡単に確認できます。

API では GET /v1/senders/{senderId} -> webhook.signatureVersion で確認できます。

Webhook を v2 に移行する

# 1. Both signatures, one shared t. Your v1 receiver notices nothing.
npx zavudev senders update $SENDER_ID --signature-version v1+v2

# 2. Deploy the verifier above. Confirm real deliveries land in YOUR logs.

# 3. Drop v1.
npx zavudev senders update $SENDER_ID --signature-version v2

API でも同じ操作を行えます。

curl -X PATCH https://api.zavu.dev/v1/senders/$SENDER_ID \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY" \
  -d '{"webhookSignatureVersion": "v1+v2"}'

新規作成した sender のデフォルトは v2 です。すでに v1 を読み取っている古い sender が使用するエンドポイントを指定する場合は、両方の方式を有効にして作成してください。

npx zavudev senders create --name Support --phone +15551234567 \
  --webhook-url https://api.example.com/webhooks/zavu \
  --webhook-events message.inbound \
  --signature-version v1+v2

v1 から直接 v2 に変更すると 400 が返されます。最初に v1+v2 を設定してください。重要なのはステップ 2 です。検証前に 200 を返す受信側は、Zavu 側からは正常に動作しているものと区別できないため、テストリクエストが成功しても何も証明されません。受信側自身のログで確認してください。

完全なガイド:https://docs.zavu.dev/guides/receiving-messages/signature-migration

ツールの Webhook は異なる形式です

エージェントが webhookUrl を持つツールを呼び出すと、Zavu は同じ ヘッダー名を使い、形式の異なる POST リクエストを送信します。

X-Zavu-Signature: 2120e306a4ce...     bare hex, no t=, no v1=/v2=
X-Zavu-Timestamp: 1786113454812       separate header, MILLISECONDS
X-Zavu-Tool:      get_order_status

ダイジェストは HMAC_SHA256(secret, body) で、v1 と同じですが、エンベロープが 異なるため、上記の検証処理ではこの形式のリクエストを検証できません。別の処理を 追加するか、ヘッダーに = が含まれているかどうかで分岐してください。

知っておくべきことが 2 つあります。

  • すべてのツール呼び出しに署名が付与されます。 Zavu はツールの 作成時にシークレットを生成し、そのレスポンスでのみ返します。すでに シークレットをお持ちの場合は、--secret で指定してください。紛失した場合は、次のエンドポイントで ローテーションできます。 POST /v1/senders/{senderId}/agent/tools/{toolId}/webhook/secret。ヘッダーがない場合はリクエストを拒否してください。 署名がない場合に「検証をスキップする」ことは避けてください。
  • ペイロードには、署名対象の本文内に独自の timestamp が含まれています。 X-Zavu-Timestamp ヘッダーではなく、その値を使って有効期限を確認してください。

defineTool を使って Zavu Function 内で宣言したツールには、これらの処理は不要です。 ハンドラーは HTTP を介さずに関数内で実行されます。

再試行ポリシー

試行遅延
1 回目の再試行1 分
2 回目の再試行5 分
3 回目の再試行15 分
4 回目の再試行1 時間
5 回目の再試行4 時間

5 回再試行した後、配信は失敗としてマークされます。

ベストプラクティス

  1. すばやく 200 を返す - 30 秒以内に応答し、非同期で処理してください
  2. 署名を検証する - 本番環境では必ず検証してください
  3. 冪等なハンドラーにする - event.id を確認して重複をスキップしてください
  4. 生の本文を使う - 署名は解析済み JSON ではなく、生の本文から計算されます
  5. ngrok でテストする - 開発時にローカルサーバーを外部に公開してください

コメント