Appearance
📚 Tài liệu kỹ thuật Webhook
🔄 Cơ chế hoạt động
Khi một giao dịch xảy ra, Pay2S sẽ gửi dữ liệu đến endpoint của bạn qua HTTP POST.
🛠️ Request từ Pay2S
Method
POSTHeader
json
{
"Content-Type": "application/json",
"Authorization": "Bearer <YOUR_SECRET_KEY>"
}Body (POST Parameters)
json
{
"transactions": [
{
"id": "1788052",
"gateway": "ACB",
"transactionDate": "2025-04-01 00:02:18",
"transactionNumber": "10418",
"accountNumber": "12805521",
"content": "SHOPVPS12537 GD 789604-040125 00:05:38",
"transferType": "IN",
"transferAmount": 50000,
"checksum": "7e2b3bbc03d1083017e3d2a96d3b8e01"
}
]
}Response từ Endpoint của bạn
json
{
"success": true
}🔁 Cơ chế Retry
Pay2S sẽ thực hiện các bước sau:
| Bước | Thời gian | Hành động |
|---|---|---|
| 1️⃣ Gửi ngay | Lập tức | Gửi dữ liệu đến endpoint |
| 2️⃣ Retry 1 | +60 giây | Gửi lại nếu không nhận 200 OK |
| 3️⃣ Retry 2-5 | +60 giây mỗi lần | Tiếp tục retry (tối đa 5 lần) |
| 4️⃣ Dừng | Sau lần 6 | Ngừng gửi |
✅ Điều kiện dừng gửi lại
Pay2S sẽ dừng gửi khi một trong hai điều kiện được thỏa:
- HTTP status:
200 OKhoặc - Response body chứa:
json
{
"success": true
}💡 Tip: Nếu endpoint xử lý lâu, hãy trả về 200 ngay và offload xử lý vào background job
💻 Code mẫu
php
<?php
// Token hợp lệ của bạn
$expectedToken = 'your_expected_token_here';
// Lấy Authorization header
$headers = getallheaders();
$authHeader = $headers['Authorization'] ?? '';
// Kiểm tra token
if (!preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
http_response_code(401);
echo json_encode(['success' => false]);
exit;
}
$receivedToken = $matches[1];
if ($receivedToken !== $expectedToken) {
http_response_code(403);
echo json_encode(['success' => false]);
exit;
}
// Lấy dữ liệu JSON
$data = json_decode(file_get_contents('php://input'), true);
if (!$data || !isset($data['transactions'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Invalid payload']);
exit;
}
// Xử lý từng giao dịch
foreach ($data['transactions'] as $transaction) {
$id = $transaction['id'];
$amount = $transaction['transferAmount'];
$content = $transaction['content'];
// Lưu vào database
// INSERT INTO transactions (id, amount, content) VALUES ($id, $amount, $content);
}
// Trả về 200
http_response_code(200);
echo json_encode(['success' => true]);
?>javascript
const express = require('express');
const app = express();
app.use(express.json());
const SECRET_KEY = 'your_expected_token_here';
app.post('/webhook', (req, res) => {
// Kiểm tra Authorization
const authHeader = req.headers['authorization'] || '';
const token = authHeader.replace('Bearer ', '');
if (token !== SECRET_KEY) {
return res.status(403).json({ success: false });
}
const { transactions } = req.body;
if (!transactions || !Array.isArray(transactions)) {
return res.status(400).json({ success: false });
}
// Xử lý từng giao dịch
transactions.forEach(tx => {
const { id, transferAmount, content } = tx;
// await Transaction.create({ id, transferAmount, content });
});
return res.status(200).json({ success: true });
});
app.listen(3000, () => console.log('Webhook running on :3000'));python
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET_KEY = 'your_expected_token_here'
@app.route('/webhook', methods=['POST'])
def webhook():
# Kiểm tra Authorization
auth_header = request.headers.get('Authorization', '')
token = auth_header.replace('Bearer ', '')
if token != SECRET_KEY:
return jsonify({'success': False}), 403
data = request.get_json()
if not data or 'transactions' not in data:
return jsonify({'success': False}), 400
# Xử lý từng giao dịch
for tx in data['transactions']:
tx_id = tx.get('id')
amount = tx.get('transferAmount')
content = tx.get('content')
# db.transaction.insert_one({ 'id': tx_id, 'amount': amount, 'content': content })
return jsonify({'success': True}), 200
if __name__ == '__main__':
app.run(port=5000)java
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import com.google.gson.Gson;
import java.util.Map;
import java.util.List;
@RestController
@RequestMapping("/webhook")
public class WebhookController {
private static final String SECRET_KEY = "your_expected_token_here";
@PostMapping
public ResponseEntity<?> handleWebhook(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@RequestBody Map<String, Object> data) {
// Kiểm tra Authorization
if (authHeader == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("success", false));
}
String token = authHeader.replace("Bearer ", "");
if (!token.equals(SECRET_KEY)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("success", false));
}
// Lấy transactions
List<Map<String, Object>> transactions = (List<Map<String, Object>>) data.get("transactions");
if (transactions == null) {
return ResponseEntity.badRequest().body(Map.of("success", false));
}
// Xử lý từng giao dịch
for (Map<String, Object> tx : transactions) {
String id = (String) tx.get("id");
Long amount = ((Number) tx.get("transferAmount")).longValue();
String content = (String) tx.get("content");
// transactionService.save(new Transaction(id, amount, content));
}
return ResponseEntity.ok(Map.of("success", true));
}
}go
package main
import (
"encoding/json"
"log"
"net/http"
"strings"
)
const SECRET_KEY = "your_expected_token_here"
type Transaction struct {
ID string `json:"id"`
TransferAmount int64 `json:"transferAmount"`
Content string `json:"content"`
}
type WebhookRequest struct {
Transactions []Transaction `json:"transactions"`
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
// Kiểm tra Authorization
authHeader := r.Header.Get("Authorization")
token := strings.TrimPrefix(authHeader, "Bearer ")
if token != SECRET_KEY {
w.WriteHeader(http.StatusForbidden)
json.NewEncoder(w).Encode(map[string]bool{"success": false})
return
}
// Parse JSON body
var webhook WebhookRequest
if err := json.NewDecoder(r.Body).Decode(&webhook); err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]bool{"success": false})
return
}
// Xử lý từng giao dịch
for _, tx := range webhook.Transactions {
log.Printf("Processing transaction: %s, Amount: %d", tx.ID, tx.TransferAmount)
// db.SaveTransaction(tx)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
func main() {
http.HandleFunc("/webhook", webhookHandler)
log.Println("Webhook server running on :8080")
http.ListenAndServe(":8080", nil)
}ruby
require 'sinatra'
require 'json'
SECRET_KEY = 'your_expected_token_here'
post '/webhook' do
# Kiểm tra Authorization
auth_header = request.headers['Authorization'] || ''
token = auth_header.sub('Bearer ', '')
if token != SECRET_KEY
status 403
return { success: false }.to_json
end
# Lấy dữ liệu
data = JSON.parse(request.body.read)
transactions = data['transactions']
unless transactions.is_a?(Array)
status 400
return { success: false }.to_json
end
# Xử lý từng giao dịch
transactions.each do |tx|
id = tx['id']
amount = tx['transferAmount']
content = tx['content']
# Transaction.create(id: id, amount: amount, content: content)
end
status 200
{ success: true }.to_json
end🔧 Troubleshooting
⚠️ Authorization header không được nhận
Nếu server của bạn không nhận được header Authorization, thêm rule này vào .htaccess:
.htaccess
RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]⚡ Best Practices
- Xác thực ngay: Kiểm tra token trước khi xử lý
- Validate dữ liệu: Kiểm tra
transactionsarray trước khi loop - Response nhanh: Trả về 200 ngay, offload xử lý vào queue/background job
- Idempotent: Xử lý webhook nhiều lần mà không bị lỗi (kiểm tra ID trước insert)
- Logging: Log tất cả webhook request/response để debug
- Error handling: Không trả về 200 nếu xử lý thất bại
- Timeout: Đặt timeout vì Pay2S sẽ retry sau 60 giây
- Database transaction: Dùng DB transaction để tránh data inconsistency
- Monitoring: Alert nếu webhook failed nhiều lần
- Versioning: Hỗ trợ multiple webhook versions để dễ update
📌 Checklist trước deploy
- [ ] Authorization header được kiểm tra
- [ ] JSON validation đầy đủ
- [ ] Response 200 được trả về luôn
- [ ] Logging được cấu hình
- [ ] Database transaction được dùng
- [ ] Error handling cho timeout/network error
- [ ] Endpoint có HTTPS (nếu production)
- [ ] Rate limiting được setup
- [ ] Webhook được test với Postman/curl
- [ ] Alert/monitoring được cấu hình
