Developer Portal
REAL-TIME

Webhooks

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à gì?

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.

1

Giao dịch thay đổi

Payout được tạo, xử lý, hoặc hoàn tất trên cổng gốc.

2

Pay27 gửi webhook

HTTP POST đến endpoint của bạn với payload JSON đầy đủ.

3

Bạn xử lý

Verify chữ ký, cập nhật đơn hàng, gửi email, ghi log...

Cấu trúc webhook payload

json
{
  "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"
}
FieldKiểuMô tả
eventstringLoại sự kiện: payout.created, payout.succeeded, payout.failed...
transaction_idstringID giao dịch trong hệ thống Pay27
gatewaystringCổng thanh toán (paypal, stripe, wise...)
amountnumberSố tiền giao dịch
currencystringMã tiền tệ (USD, EUR, VND, USDC...)
recipientstringĐịa chỉ người nhận
statusstringTrạng thái: pending, processing, completed, failed
feenumberPhí giao dịch
net_amountnumberSố tiền thực nhận sau phí
timestampstringThời gian sự kiện (ISO 8601)
webhook_idstringID duy nhất của webhook event
metadataobjectMetadata tùy chỉnh từ request gốc

HTTP Headers quan trọng

X-Pay27-Signature — Chữ ký HMAC-SHA256 để bạn verify
X-Pay27-Timestamp — Unix timestamp lúc tạo chữ ký
X-Pay27-Webhook-Id — ID duy nhất, dùng để deduplicate

Các loại event

payout.created

Payout vừa được tạo và gửi đến cổng.

payout.processing

Cổng gốc đang xử lý giao dịch.

payout.succeeded

Giao dịch thành công. Tiền đã về ví người nhận.

payout.failed

Giao dịch thất bại. Kiểm tra lỗi từ cổng.

payout.cancelled

Payout đã bị hủy.

connection.expired

Kết nối cổng hết hạn, cần refresh.

Xác thực chữ ký (Signature)

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.

Node.js

javascript
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));
}

Python

python
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.

Webhook Retry

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.

RetryThời gian chờSau sự kiện
Lần 10 giâyNgay lập tức
Lần 230 giây~30s
Lần 32 phút~2m30s
Lần 48 phút~10m30s
Lần 530 phút~40m30s

Test webhook

Từ Dashboard Pay27 → Webhooks → chọn endpoint → bấm "Gửi test event" để mô phỏng.

bash
# 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}"

Code mẫu nhận webhook

Node.js Express.js

javascript
// 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

python
# 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 Vanilla

php
<?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]);

Checklist trước khi go live

Dùng HTTPS cho endpoint
Lưu webhook secret trong biến môi trường
Verify signature HMAC-SHA256
Chống replay (timestamp < 5 phút)
Deduplicate bằng webhook_id
Trả về 200 trong 5 giây
Log đầy đủ để debug
IP whitelist của Pay27