POST /api/partner/v2/webhooks
GET /api/partner/v2/webhooks
GET /api/partner/v2/webhooks/:id
PUT /api/partner/v2/webhooks
PUT /api/partner/v2/webhooks/:id/secret
DELETE /api/partner/v2/webhooks/:id# POST /api/partner/v2/webhooks
{
"name": "SEEK Pass Webhook", // Optional
"description": "SEEK Pass Webhook", // Optional
"url": "https://example.com/webhook" // Required
}{
"id": "03a7c560-93cd-4579-a26a-a3b98efa8a49",
"name": "SEEK Pass Webhook",
"description": "SEEK Pass Webhook",
"url": "https://example.com/webhook",
"secret": "23a8db7b1b368dd0e30eff95b048b994e892000e69a4fbb3ca78ff92e13ad1f7"
}{
"event_id": "5c4ac58b-5cf9-40a0-b60a-28c0137663ed",
"request_id": "7cbc7e51-82f5-43ec-8d63-b7cdf1170425",
"type": "vwsp_request.verified",
"created_at": "2025-04-15T02:14:01.307391Z"
}| Header | คำอธิบาย |
|---|---|
| X-Signature | Hex digest ของ HMAC-SHA256 ของข้อความที่ลงนาม |
| X-Timestamp | Unix timestamp เมื่อส่งเหตุการณ์ |
require "openssl"
require "active_support/security_utils"
TOLERANCE_SECONDS = 900
def verify_webhook(raw_body, secret, given_hmac, timestamp, tolerance_seconds = TOLERANCE_SECONDS)
# Step 1: Reject stale requests to prevent replay attacks
age = Time.now.to_i - timestamp.to_i
return false if age.abs > tolerance_seconds
# Step 2: Reconstruct the signed message
signed_message = "#{timestamp}.#{raw_body}"
# Step 3: Recompute the HMAC-SHA256
calculated_hmac = OpenSSL::HMAC.hexdigest("SHA256", secret, signed_message)
# Step 4: Constant-time comparison to prevent timing attacks
return false unless calculated_hmac.bytesize == given_hmac.bytesize
ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, given_hmac)
end
def handle_event(raw_body, secret, given_hmac, timestamp)
return if !verify_webhook(raw_body, secret, given_hmac, timestamp)
event = JSON.parse(raw_body)
# Step 5: Idempotency check — discard duplicate events within the tolerance window
return if event_already_processed?(event["event_id"])
mark_event_as_processed(event["event_id"])
# ... handle event
endconst crypto = require("crypto");
const TOLERANCE_SECONDS = 900;
function verifyWebhook(rawBody, secret, givenHmac, timestamp, toleranceSeconds = TOLERANCE_SECONDS) {
// Step 1: Reject stale requests to prevent replay attacks
const age = Math.abs(Math.floor(Date.now() / 1000) - parseInt(timestamp, 10));
if (age > toleranceSeconds) return false;
// Step 2: Reconstruct the signed message
const signedMessage = `${timestamp}.${rawBody}`;
// Step 3: Recompute the HMAC-SHA256
const calculatedHmac = crypto
.createHmac("sha256", secret)
.update(signedMessage, "utf8")
.digest(); // Returns a Buffer
const givenHmacBuffer = Buffer.from(givenHmac, "hex");
// Step 4: Constant-time comparison to prevent timing attacks
if (calculatedHmac.length !== givenHmacBuffer.length) return false;
return crypto.timingSafeEqual(calculatedHmac, givenHmacBuffer);
}
async function handleEvent(rawBody, secret, givenHmac, timestamp) {
if (!verifyWebhook(rawBody, secret, givenHmac, timestamp)) return;
const event = JSON.parse(rawBody);
// Step 5: Idempotency check — discard duplicate events within the tolerance window
if (await isEventAlreadyProcessed(event.event_id)) return;
await markEventAsProcessed(event.event_id);
// ... handle event
}{
"id": "03a7c560-93cd-4579-a26a-a3b98efa8a49",
"name": "SEEK Pass Webhook",
"description": "SEEK Pass Webhook",
"url": "https://example.com/webhook",
"secret": "NEW_SECRET"
}| สภาพแวดล้อม | ที่อยู่ IP |
|---|---|
| Staging |
|
| Production |
|