ThunderPhone 2.0 เปิดให้ใช้งานแล้วเริ่มใช้งานได้ด้วยตัวเอง ราคาเริ่มต้น 2 เซนต์ต่อนาที.อ่านประกาศเปิดตัว

Operations

ตรวจสอบลายเซ็น webhook

webhook และคำขอเครื่องมือทุกรายการที่ ThunderPhone ส่งจะมีลายเซ็นพร้อมการประทับเวลา ตรวจสอบด้วยตัวอย่างหรือฟังก์ชันช่วยของ SDK ที่นี่ และปฏิเสธคำขอที่ถูกส่งซ้ำซึ่งเก่ากว่าห้านาที

ทุกคำขอที่เราส่งไปยังเซิร์ฟเวอร์ของคุณ (การส่ง webhook, webhook แบบ URL เดียวรุ่นเดิม และการเรียก tool endpoint) มีส่วนหัวลายเซ็นสองรายการ ทั้งสองใช้ HMAC-SHA256 ด้วย secret เดียวกัน

ส่วนหัวลงลายเซ็นให้การป้องกันการส่งซ้ำ
X-ThunderPhone-Signature-V2 (แนะนำ)การประทับเวลา เมธอด พาธ query และเนื้อหาปฏิเสธคำขอที่เก่ากว่า 5 นาที
X-ThunderPhone-Signatureเฉพาะเนื้อหาไม่มี: คำขอที่ถูกดักจับจะตรวจสอบได้ตลอดไป

ตรวจสอบ X-ThunderPhone-Signature-V2 ในการผสานรวมใหม่ และย้ายการผสานรวมที่มีอยู่มาใช้รูปแบบนี้ X-ThunderPhone-Signature ยังคงถูกส่งไปพร้อมทุกคำขอโดยไม่มีการเปลี่ยนแปลง ดังนั้นตัวตรวจสอบที่สร้างบนรูปแบบนี้จะยังทำงานต่อไป เราจะแจ้งวันที่ล่วงหน้าก่อนหยุดส่งส่วนหัวนี้

ลายเซ็น V2

X-ThunderPhone-Signature-V2: t=1767225600,v1=03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e
  • t คือเวลา Unix หน่วยวินาที ณ เวลาที่เราส่งคำขอ การส่งซ้ำจะได้รับการลงลายเซ็นใหม่เมื่อมีการส่ง โดยมี t ของตัวเอง
  • v1 คือค่า HMAC-SHA256 แบบเลขฐานสิบหกของสตริงที่ใช้ลงลายเซ็นด้านล่าง โดยใช้ซีเคร็ตของคุณเป็นคีย์

ค่านี้คือรายการคู่ key=value ที่คั่นด้วยเครื่องหมายจุลภาค ให้ละเว้นคีย์ที่คุณไม่รู้จัก และยอมรับคำขอหากมี v1 ใดตรงกัน เนื่องจากเราอาจเพิ่มลายเซ็นที่สองระหว่างการเปลี่ยนแปลงในอนาคต

สตริงที่ใช้ลงลายเซ็น

มีสี่ฟิลด์ โดยแต่ละฟิลด์ตามด้วยบรรทัดใหม่หนึ่งบรรทัด (\n, ไบต์ 0x0A) ยกเว้นฟิลด์สุดท้าย:

<t>\n<METHOD>\n<path and query>\n<body>
ฟิลด์ค่า
tค่า t จากเฮดเดอร์
METHODเมธอด HTTP เป็นตัวพิมพ์ใหญ่: POST, GET, PUT, PATCH หรือ DELETE
path and queryเป้าหมายคำขอจากบรรทัดคำขอ HTTP: พาธ ตามด้วย ? และสตริงคิวรีเมื่อมี ไม่รวมสคีม โฮสต์ หรือแฟรกเมนต์ ใช้ตามที่ได้รับมาโดยตรง โดยไม่ถอดรหัสหรือเรียงลำดับพารามิเตอร์ใหม่
bodyเนื้อหาคำขอดิบ การเรียกใช้เครื่องมือ GET และ DELETE ไม่มีเนื้อหา ดังนั้นสตริงที่ใช้ลงลายเซ็นจึงสิ้นสุดด้วยบรรทัดใหม่หลังพาธ

วิธีตรวจสอบ:

  1. ก่อนทำการแยกวิเคราะห์ใดๆ ให้อ่านเฮดเดอร์ เมธอด พาธและคิวรีดิบ และเนื้อหาดิบ
  2. ดึง t และ v1 ทุกค่าจากเฮดเดอร์
  3. ปฏิเสธคำขอหาก t อยู่ก่อนหรือหลังเวลาของคุณเกิน 300 วินาที
  4. คำนวณ HMAC-SHA256 แบบเลขฐานสิบหกของสตริงที่ใช้ลงลายเซ็นด้วยซีเคร็ตของคุณ
  5. เปรียบเทียบกับ v1 แบบเวลาคงที่ ตอบกลับ 401 หากไม่มีค่าใดตรงกัน

ตัวอย่างการทำงาน

ซีเคร็ต whsec_example_4c1f2a9b เราส่งคำขอนี้ที่ t = 1767225600 (2026-01-01 00:00:00 UTC):

POST /tools/lookup?region=us HTTP/1.1
Host: api.example.com
Content-Type: application/json
X-ThunderPhone-Signature-V2: t=1767225600,v1=03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e
 
{"customer_id":"cust_123"}

สตริงที่ใช้ลงลายเซ็นคือสี่บรรทัดนี้ โดยไม่มีบรรทัดใหม่หลังเนื้อหา:

1767225600
POST
/tools/lookup?region=us
{"customer_id":"cust_123"}

ค่า HMAC-SHA256 คือ 03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e ตรวจสอบการติดตั้งใช้งานของคุณเทียบกับค่านี้ หรือสร้างซ้ำด้วย openssl:

printf '1767225600\nPOST\n/tools/lookup?region=us\n{"customer_id":"cust_123"}' \
  | openssl dgst -sha256 -hmac 'whsec_example_4c1f2a9b'

การเรียกใช้เครื่องมือ GET ไปยัง /tools/lookup?customer_id=cust_123 ในวินาทีเดียวกันมีเนื้อหาว่าง สตริงที่ใช้ลงลายเซ็นคือ 1767225600\nGET\n/tools/lookup?customer_id=cust_123\n และลายเซ็นคือ 3ce214cb0b8f2bf39b1d19d4e7265df9dbaff5a5f838d3cc6c753a8f7a4ad280

ใช้กับ SDK

SDK สำหรับ Python และ TypeScript มีฟังก์ชันช่วย (thunderphone 0.2.0 หรือใหม่กว่า, @thunderphone/sdk 0.2.0 หรือใหม่กว่า) ซึ่งตรวจสอบช่วงเวลา 5 นาที เปรียบเทียบแบบเวลาคงที่ และยอมรับรายการซีเคร็ตเพื่อให้คุณหมุนเวียนซีเคร็ตได้โดยไม่เกิดช่วงหยุดให้บริการ

Python
from thunderphone import verify_signature
 
ok = verify_signature(
    header,            # the X-ThunderPhone-Signature-V2 value
    secret=SECRET,     # or [NEW_SECRET, OLD_SECRET] while rotating
    method=method,     # "POST"
    path=path,         # "/tools/lookup?region=us", exactly as received
    body=raw_body,     # bytes, before any JSON parsing
    tolerance=300,     # optional; seconds
)
Node.js
import { verifySignature } from "@thunderphone/sdk";
 
const ok = verifySignature({
  header,               // the X-ThunderPhone-Signature-V2 value
  secret: SECRET,       // or [NEW_SECRET, OLD_SECRET] while rotating
  method,               // "POST"
  path,                 // "/tools/lookup?region=us", exactly as received
  body: rawBody,        // Buffer or string, before any JSON parsing
  toleranceSeconds: 300, // optional
});

โดยไม่ใช้ SDK

Python
import hashlib
import hmac
import time
 
TOLERANCE_SECONDS = 300
 
 
def verify_v2(header: str, secret: str, method: str, path: str, body: bytes) -> bool:
    """path is the path plus ?query exactly as received; body is the raw body."""
    timestamp, signatures = None, []
    for item in (header or "").split(","):
        key, _, value = item.strip().partition("=")
        if key == "t" and value.isdigit() and len(value) <= 15:
            timestamp = int(value)
        elif key == "v1":
            signatures.append(value)
    if timestamp is None or abs(time.time() - timestamp) > TOLERANCE_SECONDS:
        return False
    signed = f"{timestamp}\n{method.upper()}\n{path}\n".encode() + body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected.encode(), s.encode()) for s in signatures)
Node.js
import crypto from "node:crypto";
 
const TOLERANCE_SECONDS = 300;
 
// path is the path plus ?query exactly as received; body is the raw Buffer.
export function verifyV2(header, secret, method, path, body) {
  let timestamp;
  const signatures = [];
  for (const item of (header || "").split(",")) {
    const [key, value = ""] = item.trim().split("=", 2);
    if (key === "t" && /^\d+$/.test(value)) timestamp = Number(value);
    else if (key === "v1") signatures.push(value);
  }
  if (timestamp === undefined) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
  const expected = Buffer.from(
    crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}\n${method.toUpperCase()}\n${path}\n`)
      .update(body)
      .digest("hex"),
  );
  return signatures.some((sig) => {
    const candidate = Buffer.from(sig);
    return (
      candidate.length === expected.length &&
      crypto.timingSafeEqual(candidate, expected)
    );
  });
}

การเชื่อมต่อกับเฟรมเวิร์ก

แต่ละตัวอย่างจะอ่านพาธและคิวรีดิบ รวมถึงบอดีดิบ จากนั้นเรียกใช้ตัวช่วยของ SDK

FastAPI
import json
 
from fastapi import FastAPI, HTTPException, Request
from thunderphone import verify_signature
 
app = FastAPI()
 
 
def raw_path_and_query(request: Request) -> str:
    path = (request.scope.get("raw_path") or request.url.path.encode()).decode("latin-1")
    query = request.scope.get("query_string", b"").decode("latin-1")
    return f"{path}?{query}" if query else path
 
 
@app.post("/thunderphone-webhook")
async def hook(request: Request):
    body = await request.body()  # raw bytes, NOT request.json()
    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=SECRET,
        method=request.method,
        path=raw_path_and_query(request),
        body=body,
    ):
        raise HTTPException(status_code=401)
    event = json.loads(body)
    # … dispatch on event["type"] …
    return {"ok": True}
Express
import express from "express";
import { verifySignature } from "@thunderphone/sdk";
 
const app = express();
 
app.post(
  "/thunderphone-webhook",
  // IMPORTANT: parse as raw; do NOT use express.json() here.
  express.raw({ type: "application/json" }),
  (req, res) => {
    const ok = verifySignature({
      header: req.header("X-ThunderPhone-Signature-V2"),
      secret: process.env.WEBHOOK_SECRET,
      method: req.method,
      path: req.originalUrl, // path and query as received
      body: req.body,
    });
    if (!ok) return res.sendStatus(401);
    const event = JSON.parse(req.body.toString("utf8"));
    // … dispatch on event.type …
    res.sendStatus(204);
  },
);
Django
import json
 
from django.http import HttpResponseForbidden, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from thunderphone import verify_signature
 
 
@csrf_exempt
@require_POST
def hook(request):
    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=SECRET,
        method=request.method,
        path=request.get_full_path(),
        body=request.body,  # raw bytes
    ):
        return HttpResponseForbidden("invalid signature")
    event = json.loads(request.body)
    # … dispatch on event["type"] …
    return JsonResponse({"ok": True})

req.originalUrl (Express) รวมถึง raw_path และ query_string ของ ASGI (FastAPI) คือเป้าหมายตามที่ได้รับมาอย่างแม่นยำ Django get_full_path() และ request.path ของ Flask จะถอดรหัสพาธก่อน ซึ่งให้ สตริงเดียวกันสำหรับพาธทั่วไป หากพาธของเอนด์พอยต์มีอักขระที่เข้ารหัสเปอร์เซ็นต์หรืออักขระสงวน เช่น %2F, ; หรือ = ให้ใช้ เป้าหมายดิบที่เซิร์ฟเวอร์ของคุณมีให้แทน (ตัวอย่างเช่น request.META["RAW_URI"] บน Gunicorn)

ใช้ secret ใด

ความลับเดียวกันใช้ลงลายเซ็นสำหรับทั้งสองเฮดเดอร์

แหล่งที่มาSecret
Webhook endpoint (/v1/developer/webhook-endpoints)secret เฉพาะ endpoint (อักขระ hex 48 ตัว) ที่ส่งกลับเพียงครั้งเดียวเมื่อสร้าง
Webhook URL เดียวแบบเดิมsecret เฉพาะองค์กรที่ส่งกลับจาก GET /v1/webhook
การเรียกใช้ Tool endpoint (เรียกไปยัง endpoint.url ของคุณโดยตรง)webhook secret ระดับองค์กร (ค่าเดียวกับ webhook URL เดียวแบบเดิม) — ไม่ใช่ secret เฉพาะ endpoint

จัดเก็บ secret ไว้ในตัวจัดการ secret หรือตัวแปรสภาพแวดล้อม — ห้าม commit โดยเด็ดขาด

การตรวจสอบการเรียกใช้เครื่องมือ

เมื่อเอเจนต์เรียกใช้ เครื่องมือฟังก์ชัน รายการใดรายการหนึ่งโดยตรง (เครื่องมือนั้นมี endpoint) คำขอจะมีส่วนหัว ThunderPhone เหล่านี้พร้อมกับ endpoint.headers ที่คุณกำหนดค่าไว้:

  • X-ThunderPhone-Call-ID — รหัสตัวเลขของสายที่กำลังดำเนินอยู่
  • X-ThunderPhone-Signature-V2 — ลายเซ็นที่มีการประทับเวลาข้างต้น ซึ่งใช้ ข้อมูลลับเว็บฮุกระดับองค์กร ของคุณ
  • X-ThunderPhone-Signature — ลายเซ็นแบบเดิมที่ครอบคลุมเฉพาะบอดี โดยใช้คีย์เดียวกัน

มีข้อแตกต่างจากเว็บฮุกอยู่สองประการ:

  1. เครื่องมือ GET / DELETE ไม่มีบอดี อาร์กิวเมนต์จะส่งเป็นพารามิเตอร์คิวรี ลายเซ็น V2 ครอบคลุมอาร์กิวเมนต์เหล่านั้นผ่านพาธและคิวรี และฟิลด์บอดีจะว่างเปล่า ตรวจสอบด้วยสตริงคิวรีดิบ: การแยกวิเคราะห์ พารามิเตอร์และสร้างสตริงคิวรีใหม่อาจเปลี่ยนลำดับ หรือการเข้ารหัสได้ ส่วนหัวแบบเดิมจะลงนามสตริงไบต์ว่าง
  2. องค์กรที่ไม่ได้กำหนดค่าเว็บฮุกแบบเดิมจะไม่มีข้อมูลลับระดับองค์กร ใน กรณีนั้น การเรียกใช้เครื่องมือจะมีเพียง X-ThunderPhone-Call-ID และไม่มี ส่วนหัวลายเซ็น กำหนดค่าเว็บฮุกแบบเดิม (PUT /v1/webhook) เพื่อรับข้อมูลลับสำหรับการลงนาม หรือยืนยันตัวตนการเรียกใช้ เครื่องมือด้วยส่วนหัวของคุณเองผ่าน endpoint.headers
@app.api_route("/tools/search-appointments", methods=["GET", "POST"])
async def tool(request: Request):
    body = await request.body()  # b"" for GET/DELETE tools
    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=ORG_WEBHOOK_SECRET,
        method=request.method,
        path=raw_path_and_query(request),
        body=body,
    ):
        raise HTTPException(status_code=401)
    call_id = request.headers.get("X-ThunderPhone-Call-ID", "")
    args = json.loads(body) if body else dict(request.query_params)
    ...

ใน Express ตัวแยกวิเคราะห์บอดีจะปล่อยให้ req.body เป็นวัตถุว่างสำหรับ GET ดังนั้นให้ส่ง Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0) ผ่านไป

การส่งต่อเครื่องมือในโหมดเว็บฮุก (เครื่องมือที่ไม่มี endpoint ซึ่งส่งไปยัง เว็บฮุกขององค์กรคุณเป็น telephony.tool / web.tool) คือเว็บฮุกที่ลงนามตามปกติ — ใช้ขั้นตอนมาตรฐานข้างต้น ดู เครื่องมือฟังก์ชัน สำหรับรูปแบบคำขอทั้งสองแบบ

ลายเซ็นแบบเดิม: X-ThunderPhone-Signature

เฮดเดอร์ดั้งเดิมคือค่า hex ของ HMAC-SHA256 สำหรับ raw body เพียงอย่างเดียว โดยใช้ secret เดียวกัน ไม่มี timestamp ดังนั้นผู้ที่ดักจับคำขอที่ลงลายเซ็นไว้หนึ่งรายการ สามารถส่งซ้ำได้ ระบบจะยังคงส่งเฮดเดอร์นี้ในทุกคำขอระหว่างที่อินทิเกรชันย้ายไปยัง V2

  1. อ่านเนื้อหาคำขอ ดิบ — ไบต์ที่ตรงกับที่เราส่งให้คุณทุกประการ
  2. คำนวณ hmac_sha256(secret, body).hexdigest()
  3. เปรียบเทียบกับ X-ThunderPhone-Signature โดยใช้ เวลาคงที่

เราลงลายเซ็นไบต์ที่เราส่งจริงทุกประการ ไบต์เหล่านั้นยังเป็น การซีเรียลไลซ์ JSON แบบมาตรฐาน ของ payload ด้วย — เรียงคีย์ตามลำดับ ตัวอักษร ใช้ตัวคั่นแบบกระชับ (, และ : โดยไม่มีช่องว่าง) และ UTF-8 หากเฟรมเวิร์กของคุณแสดงเฉพาะ JSON ที่แยกวิเคราะห์แล้ว ให้ซีเรียลไลซ์ใหม่แบบมาตรฐานและ คำนวณ HMAC กับข้อมูลนั้น:

# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")

ควรใช้ raw body เพราะมีขั้นตอนน้อยกว่า และไม่รับผลกระทบจากความแตกต่างในการแปลงกลับไปกลับมาของตัวเลข JSON ในบางภาษา หลักการเดียวกันนี้ใช้กับฟิลด์ body ของ V2 ด้วย

Python
import hashlib
import hmac
 
 
def verify(body: bytes, signature: str, secret: str) -> bool:
    """Constant-time HMAC-SHA256 verification."""
    expected = hmac.new(
        secret.encode("utf-8"),
        body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature or "")
Node.js
import crypto from "node:crypto";
 
export function verify(body, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  if (!signature || expected.length !== signature.length) return false;
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature),
  );
}
Go
package webhook
 
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)
 
func Verify(body []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}
Ruby
require "openssl"
 
def verify(body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
  Rack::Utils.secure_compare(expected, signature.to_s)
end

ข้อผิดพลาดที่พบบ่อย

ความคลาดเคลื่อนของนาฬิกา

ช่วงเวลา 5 นาทีจะวัดเทียบกับนาฬิกาของเซิร์ฟเวอร์คุณ ควรซิงโครไนซ์ กับ NTP นาฬิกาที่คลาดเคลื่อนเกิน 5 นาทีจะปฏิเสธทุก คำขอ

พร็อกซีเขียนพาธใหม่

พาธที่ลงลายเซ็นคือพาธที่เราร้องขอ ได้แก่พาธของ URL ที่คุณ กำหนดค่าใน ThunderPhone หากรีเวิร์สพร็อกซีหรือเกตเวย์ API ลบ หรือเพิ่มคำนำหน้าก่อนที่แอปพลิเคชันของคุณจะเห็นคำขอ ให้สร้าง พาธที่เราส่งใหม่ (เช่น เพิ่มคำนำหน้ากลับเข้าไป) ก่อนตรวจสอบ

ซีเรียลไลซ์ใหม่ด้วยรูปแบบเริ่มต้น

การแยกวิเคราะห์บอดีแล้วดัมป์อีกครั้งด้วยค่าเริ่มต้นของ ไลบรารี JSON ของคุณ (เว้นวรรคหลัง , / :, คีย์เรียงตามลำดับการแทรก) จะสร้าง ไบต์ที่แตกต่างและทำให้ HMAC ใช้ไม่ได้ ตรวจสอบบอดีดิบ หรือหาก จำเป็นต้องซีเรียลไลซ์ใหม่ ให้ตรงกับรูปแบบมาตรฐานของเราอย่างแม่นยำ: เรียง คีย์ ตัวคั่นแบบกระชับ UTF-8

เฟรมเวิร์กแยกวิเคราะห์ JSON อัตโนมัติ

มิดเดิลแวร์ express.json() ของ Express จะใช้สตรีมเนื้อหา ทำให้คุณสูญเสียไบต์ดิบ ใช้ express.raw() กับเส้นทาง webhook โดยเฉพาะ หรือบัฟเฟอร์เนื้อหาดิบในมิดเดิลแวร์ก่อนหน้า NestJS / Koa ก็เช่นกัน — ตรวจสอบเอกสารเกี่ยวกับเนื้อหาดิบของแต่ละระบบ

การเปรียบเทียบที่ไม่ปลอดภัยต่อการจับเวลา

expected === signature ใน JS หรือ expected == signature ใน Python เป็นการเปรียบเทียบที่ใช้เวลาไม่คงที่ ให้ใช้ crypto.timingSafeEqual หรือ hmac.compare_digest ตามลำดับ ความแตกต่างด้านประสิทธิภาพ ไม่มีนัยสำคัญ

ซีเคร็ตไม่ถูกต้องสำหรับเอนด์พอยต์เครื่องมือ

การเรียกเอนด์พอยต์เครื่องมือโดยตรงจะลงลายเซ็นด้วย ซีเคร็ต webhook ระดับองค์กร (GET /v1/webhook) ไม่ใช่ซีเคร็ตเฉพาะเอนด์พอยต์ จาก /v1/developer/webhook-endpoints ใช้ตัวช่วยเดิมซ้ำได้ แต่ต้องแน่ใจว่าได้ส่งซีเคร็ตระดับองค์กรให้กับตัวช่วยนั้นบนเส้นทางเครื่องมือ

สตริงคิวรีบนเครื่องมือ GET/DELETE

V2 ลงลายเซ็นสตริงคิวรีตรงตามที่ส่ง ส่วนหัวแบบเดิมจะลงลายเซ็น บอดีว่างและจะไม่ลงลายเซ็นสตริงคิวรี ดังนั้นการแฮช URL สำหรับ ส่วนหัวแบบเดิมจะไม่ตรงกัน

ข้อมูลซ้ำภายในช่วงเวลา

การประทับเวลาป้องกันการเล่นซ้ำที่เก่ากว่า 5 นาที ไม่ใช่สำเนาที่สอง ภายในช่วงเวลานั้น การส่งไปยังเอนด์พอยต์เป็นแบบอย่างน้อยหนึ่งครั้ง ดังนั้น การลองใหม่ของเหตุการณ์เดียวกันก็อาจมาถึงได้ ให้กำจัดข้อมูลซ้ำโดยอิงจาก event_id ของซองข้อมูล

ไม่ส่งคืน 401 เมื่อไม่ตรงกัน

การส่งคืน 200 เมื่อการตรวจสอบล้มเหลวทำให้ตัวจัดการเป็นเป้าหมายของการเล่นซ้ำ ให้ตอบกลับด้วยสถานะที่ไม่ใช่ 2xx เสมอหากการตรวจสอบล้มเหลว


ขั้นตอนถัดไป