Skip to content

📚 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

POST
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ướcThời gianHành động
1️⃣ Gửi ngayLập tứcGửi dữ liệu đến endpoint
2️⃣ Retry 1+60 giâyGửi lại nếu không nhận 200 OK
3️⃣ Retry 2-5+60 giây mỗi lầnTiếp tục retry (tối đa 5 lần)
4️⃣ DừngSau lần 6Ngừ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:

  1. HTTP status: 200 OK hoặc
  2. 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

  1. Xác thực ngay: Kiểm tra token trước khi xử lý
  2. Validate dữ liệu: Kiểm tra transactions array trước khi loop
  3. Response nhanh: Trả về 200 ngay, offload xử lý vào queue/background job
  4. Idempotent: Xử lý webhook nhiều lần mà không bị lỗi (kiểm tra ID trước insert)
  5. Logging: Log tất cả webhook request/response để debug
  6. Error handling: Không trả về 200 nếu xử lý thất bại
  7. Timeout: Đặt timeout vì Pay2S sẽ retry sau 60 giây
  8. Database transaction: Dùng DB transaction để tránh data inconsistency
  9. Monitoring: Alert nếu webhook failed nhiều lần
  10. 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