webhook-setup について
Zavu の Webhook を設定し、メッセージ、配信、テンプレートのイベントを受け取る開発者向けスキルです。Webhook の種類、イベントペイロード、送信者設定、複数チャネルの受信処理を説明します。
主な機能
- 01送信者およびプロジェクトの Webhook 設定
- 02メッセージや配信状況イベントの選択
- 03共通イベント形式とペイロードの解釈
- 04複数チャネルの受信メッセージ処理
- 05送信者 Webhook シークレットの再生成
- 06メール添付ファイルのメタデータとリンク取得
活用シーン
- 01顧客からの受信メッセージの処理
- 02配信状況や既読状況の追跡
- 03テンプレート承認やカテゴリ変更への対応
- 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は送信者です。
| フィールド | 説明 |
|---|---|
messageId | ZavuのメッセージIDです。 |
conversationId | 受信トレイのスレッドIDです。メッセージがスレッドに入った後にWebhookが送信されるため、常に設定されています。新しいスレッドの最初のメッセージには、conversation.newと同じIDが格納されます。リアクションでは省略されます。下記の「受信トレイへのディープリンク」を参照してください。 |
contactId | Zavuの連絡先です。fromとは異なり、WhatsAppの連絡先の識別子がBSUIDと電話番号の間で切り替わっても変わりません。fromではなく、この値をキーとして使用してください。例外は、1人の連絡先2件が統合された場合です。このときcontact.identity_linkedには、旧IDがmergedContactIdとして格納されます。リアクションでは省略されます。 |
whatsapp | BSUIDが判明している場合のみ、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)。 |
messageType | text、image、videoなどです。location_requestへの返信は、新しいタイプではなくlocationとして届き、content.replyToMessageIdにリクエストが設定されます。これを照合に使用してください。 |
text | テキスト本文またはメディアのキャプションです。存在する場合に設定されます。 |
providerTimestamp | WhatsApp、Telegram、Instagram、Messengerでは、プロバイダーが受信した元の時刻(Unix ms)です。SMSとメールではnullです。トップレベルのtimestampと比較して、配信の遅延を検知できます。 |
referral | クリックしてWhatsAppでチャットする広告のアトリビューションです。WhatsAppのみで、広告から開始されたスレッドの最初のメッセージにのみ設定されます。下記の「クリックしてWhatsAppでチャットする広告のアトリビューション」を参照してください。 |
telegramChatId | Telegramのみ:返信先のチャットIDです。プレフィックスは含まれません。 |
instagramSenderId | Instagramのみ:返信先の送信者IDです。プレフィックスは含まれません。 |
messengerSenderId | Messengerのみ:返信先の送信者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 のみで、会話の最初のメッセージにのみ届き、その後のメッセージには含まれません。再送はないため、届いたら保存してください。オーガニックな会話には含まれません。
| フィールド | 説明 |
|---|---|
ctwaClid | Click-to-WhatsApp のクリック識別子です。Meta の Conversions API で、広告にコンバージョンを紐付ける際に必要です。ad ソースに含まれ、post には含まれません。 |
sourceId | クリックにつながった広告または投稿の識別子です。 |
sourceType | ad または 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 には次の内容が含まれます。
| フィールド | 説明 |
|---|---|
invitationId | Zavu の招待 ID です。 |
clientName / clientEmail | 招待の作成時に渡した値、または null です。 |
connectionType | クライアントが接続する対象です。whatsapp_waba または messenger です。 |
previousStatus / currentStatus | ステータスの遷移です。 |
senderId | completed の場合に存在します。プロジェクトに作成された sender です。 |
wabaAccountId | WhatsApp 招待が completed の場合に存在します。WhatsApp Business Account ID です。 |
connectedAccount | completed の場合に存在します。{ channel, id, name } — リンクされた WhatsApp 番号または Facebook ページです。 |
failureReason | failed の場合に存在します。安定したコードです。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>]
| 部分 | 対象 |
|---|---|
t | Unix タイムスタンプ(秒) |
v1 | HMAC_SHA256(secret, body) |
v2 | HMAC_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 回再試行した後、配信は失敗としてマークされます。
ベストプラクティス
- すばやく 200 を返す - 30 秒以内に応答し、非同期で処理してください
- 署名を検証する - 本番環境では必ず検証してください
- 冪等なハンドラーにする -
event.idを確認して重複をスキップしてください - 生の本文を使う - 署名は解析済み JSON ではなく、生の本文から計算されます
- ngrok でテストする - 開発時にローカルサーバーを外部に公開してください