Webhook
款項一有動靜,立即知道。
Webhook 會在事件發生時即時推送到您的伺服器,您無需輪詢。每個事件都經過簽章,在您確認接收前會持續重試,且可安全地重複處理。
◷ 沙盒將優先開放給候補名單中的開發者。正式金鑰將在 TujuPay 取得牌照後提供。
設定端點
- 1
新增網址
在商家後台開啟「開發者」,再進入「Webhook」,新增您伺服器上的公開 HTTPS 網址。
- 2
選擇事件
挑選您需要的事件,或在開發期間訂閱全部事件。
- 3
複製簽章密鑰
每個端點都有各自的密鑰,以 whsec_ 開頭。請像密碼一樣妥善保管。
驗證簽章
每個請求都帶有 TujuPay-Signature 標頭,內含時間戳記,以及對時間戳記和原始請求內容計算的 HMAC-SHA256 簽章。請用您的密鑰計算相同的簽章並進行比對。拒絕超過五分鐘的事件,以防範重放攻擊。
標頭格式
TujuPay-Signature: t=1791536400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Node.js
import crypto from "node:crypto";
export function verifyWebhook(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const age = Date.now() / 1000 - Number(parts.t);
if (age > 300) throw new Error("Event too old");
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
if (!ok) throw new Error("Bad signature");
return JSON.parse(rawBody);
}PHP
<?php
function verify_webhook(string $rawBody, string $header, string $secret): array {
parse_str(str_replace(',', '&', $header), $parts);
if (time() - (int) $parts['t'] > 300) {
throw new Exception('Event too old');
}
$expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
if (!hash_equals($expected, $parts['v1'])) {
throw new Exception('Bad signature');
}
return json_decode($rawBody, true);
}重試機制
如果您的伺服器未在 10 秒內回傳 2xx,我們會以逐漸拉長的間隔重試,最長持續三天。您也可以在商家後台重新發送任何事件。
- 11 分鐘
- 25 分鐘
- 330 分鐘
- 42 小時
- 56 小時
- 6之後每 12 小時一次,最長 3 天
事件
| 事件 | 觸發時機 |
|---|---|
| payment.succeeded | 顧客已付款,款項已確認 |
| payment.failed | 銀行拒絕,或顧客放棄結帳 |
| payment.expired | 付款未在 30 分鐘內完成 |
| refund.succeeded | 退款已退回給顧客 |
| refund.failed | 退款無法完成 |
| payout.scheduled | 今日結算已計算完成,即將發出 |
| payout.paid | 結算款項已到帳 |
| payout.held | 結算已暫停,事件內容會說明原因 |
最佳做法
- 立即回傳 200,將寄送電子郵件等耗時工作交給背景任務處理。
- 儲存每個事件 ID,略過已處理過的事件。重試可能導致同一事件送達兩次。
- 務必驗證簽章。切勿信任重新導向網址中的金額或狀態。
- 如需最新狀態,請透過 API 查詢付款;事件可能不按順序送達。