Nhận thông báo giao dịch real-time về server của bạn. Pay27 gửi HTTP POST đến endpoint của bạn mỗi khi có sự kiện thay đổi trạng thái giao dịch.
Webhook là cơ chế push: thay vì bạn phải gọi API liên tục để kiểm tra trạng thái (polling), Pay27 sẽ tự động gửi HTTP POST về server của bạn ngay khi có sự kiện.
Payout được tạo, xử lý, hoặc hoàn tất trên cổng gốc.
HTTP POST đến endpoint của bạn với payload JSON đầy đủ.
Verify chữ ký, cập nhật đơn hàng, gửi email, ghi log...
{
"event": "payout.succeeded",
"transaction_id": "txn_abc123def456",
"gateway": "paypal",
"amount": 150.00,
"currency": "USD",
"recipient": "recipient@example.com",
"status": "completed",
"external_id": "PAYPAL_TXN_789",
"fee": 4.50,
"net_amount": 145.50,
"metadata": { "order_id": "ORD-2026-0422" },
"timestamp": "2026-06-02T08:00:00Z",
"webhook_id": "wh_evt_8k9j7h6g5"
}| Field | Kiểu | Mô tả |
|---|---|---|
| event | string | Loại sự kiện: payout.created, payout.succeeded, payout.failed... |
| transaction_id | string | ID giao dịch trong hệ thống Pay27 |
| gateway | string | Cổng thanh toán (paypal, stripe, wise...) |
| amount | number | Số tiền giao dịch |
| currency | string | Mã tiền tệ (USD, EUR, VND, USDC...) |
| recipient | string | Địa chỉ người nhận |
| status | string | Trạng thái: pending, processing, completed, failed |
| fee | number | Phí giao dịch |
| net_amount | number | Số tiền thực nhận sau phí |
| timestamp | string | Thời gian sự kiện (ISO 8601) |
| webhook_id | string | ID duy nhất của webhook event |
| metadata | object | Metadata tùy chỉnh từ request gốc |
X-Pay27-Signature — Chữ ký HMAC-SHA256 để bạn verifyX-Pay27-Timestamp — Unix timestamp lúc tạo chữ kýX-Pay27-Webhook-Id — ID duy nhất, dùng để deduplicatepayout.createdPayout vừa được tạo và gửi đến cổng.
payout.processingCổng gốc đang xử lý giao dịch.
payout.succeededGiao dịch thành công. Tiền đã về ví người nhận.
payout.failedGiao dịch thất bại. Kiểm tra lỗi từ cổng.
payout.cancelledPayout đã bị hủy.
connection.expiredKết nối cổng hết hạn, cần refresh.
Pay27 ký mỗi webhook bằng HMAC-SHA256. Bạn phải verify chữ ký để đảm bảo request đến từ Pay27, không phải kẻ tấn công.
const crypto = require("crypto");
function verifyPay27Signature(rawBody, headers) {
const signature = headers["x-pay27-signature"];
const timestamp = headers["x-pay27-timestamp"];
if (!signature || !timestamp) return false;
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) return false; // 5 phút
const payload = timestamp + "." + rawBody;
const expected = crypto.createHmac("sha256", SECRET).update(payload).digest("hex");
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}import hmac, hashlib, time
def verify_pay27_signature(raw_body: str, headers: dict) -> bool:
signature = headers.get("X-Pay27-Signature", "")
timestamp = headers.get("X-Pay27-Timestamp", "")
if not signature or not timestamp:
return False
if abs(int(time.time()) - int(timestamp)) > 300:
return False
payload = timestamp + "." + raw_body
expected = hmac.new(
SECRET.encode(), payload.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)Luôn verify signature! Không verify = hacker có thể gửi webhook giả, đánh dấu đơn hàng đã thanh toán khi chưa nhận được tiền.
Nếu server của bạn không trả về HTTP 200 trong 5 giây, Pay27 sẽ tự động retry với exponential backoff.
| Retry | Thời gian chờ | Sau sự kiện |
|---|---|---|
| Lần 1 | 0 giây | Ngay lập tức |
| Lần 2 | 30 giây | ~30s |
| Lần 3 | 2 phút | ~2m30s |
| Lần 4 | 8 phút | ~10m30s |
| Lần 5 | 30 phút | ~40m30s |
Từ Dashboard Pay27 → Webhooks → chọn endpoint → bấm "Gửi test event" để mô phỏng.
# Gửi test webhook từ terminal
SECRET="whsec_your_webhook_secret_here"
TIMESTAMP=$(date +%s)
BODY='{"event":"payout.succeeded","transaction_id":"txn_test_9k8j7h6g5f","gateway":"paypal","amount":100,"currency":"USD"}'
SIGNATURE=$(echo -n "${TIMESTAMP}.${BODY}" | openssl dgst -sha256 -hmac "${SECRET}" | cut -d' ' -f2)
curl -X POST https://your-server.com/webhooks/pay27 \
-H "Content-Type: application/json" \
-H "X-Pay27-Signature: ${SIGNATURE}" \
-H "X-Pay27-Timestamp: ${TIMESTAMP}" \
-d "${BODY}"// Express.js — Endpoint nhận Webhook Pay27 đầy đủ
const express = require("express");
const crypto = require("crypto");
const app = express();
app.use(
express.json({
verify: (req, res, buf) => { req.rawBody = buf.toString(); },
})
);
const SECRET = process.env.PAY27_WEBHOOK_SECRET;
app.post("/webhooks/pay27", (req, res) => {
const signature = req.headers["x-pay27-signature"];
const timestamp = req.headers["x-pay27-timestamp"];
if (!signature || !timestamp) return res.status(401).json({ error: "Missing headers" });
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) return res.status(401).json({ error: "Expired" });
const expected = crypto.createHmac("sha256", SECRET).update(timestamp + "." + req.rawBody).digest("hex");
try {
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)))
return res.status(401).json({ error: "Invalid signature" });
} catch { return res.status(401).json({ error: "Invalid format" }); }
const { event, transaction_id, gateway, amount, currency } = req.body;
console.log("[Webhook] " + event + " — " + transaction_id + " (" + gateway + ")");
switch (event) {
case "payout.succeeded":
console.log("Payout " + transaction_id + " OK: " + amount + " " + currency);
break;
case "payout.failed":
console.error("Payout " + transaction_id + " FAILED: " + amount + " " + currency);
break;
}
res.json({ received: true });
});
app.listen(process.env.PORT || 3000, () => console.log("Webhook server running"));# Python Flask — Endpoint nhận Webhook Pay27
from flask import Flask, request, jsonify
import hmac, hashlib, os, time, logging
app = Flask(__name__)
SECRET = os.environ["PAY27_WEBHOOK_SECRET"]
def verify():
sig = request.headers.get("X-Pay27-Signature", "")
ts = request.headers.get("X-Pay27-Timestamp", "")
if not sig or not ts: return False
if abs(int(time.time()) - int(ts)) > 300: return False
payload = ts + "." + request.get_data(as_text=True)
expected = hmac.new(SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(sig, expected)
@app.route("/webhooks/pay27", methods=["POST"])
def webhook():
if not verify(): return jsonify({"error":"Invalid"}), 401
body = request.json
event = body.get("event")
tx_id = body.get("transaction_id")
if event == "payout.succeeded":
app.logger.info(f"Payout OK: {tx_id}")
return jsonify({"received":True}), 200<?php
// PHP — Endpoint nhận Webhook Pay27
$secret = getenv("PAY27_WEBHOOK_SECRET");
$headers = getallheaders();
$sig = $headers["X-Pay27-Signature"] ?? "";
$ts = $headers["X-Pay27-Timestamp"] ?? "";
if (!$sig || !$ts) { http_response_code(401); exit; }
if (abs(time() - (int)$ts) > 300) { http_response_code(401); exit; }
$raw = file_get_contents("php://input");
$expected = hash_hmac("sha256", $ts . "." . $raw, $secret);
if (!hash_equals($expected, $sig)) { http_response_code(401); exit; }
$body = json_decode($raw, true);
$event = $body["event"] ?? "";
error_log("[Webhook] $event — {$body["transaction_id"]}");
switch ($event) {
case "payout.succeeded":
// Cập nhật đơn hàng
break;
case "payout.failed":
// Thông báo thất bại
break;
}
echo json_encode(["received" => true]);