Tại sao cần đối soát?
Webhook gửi giao dịch theo thời gian thực, nhưng đôi lúc vẫn có trường hợp webhook không đến được server của bạn:
- Server tạm ngưng (deploy, restart, downtime)
- Lỗi mạng giữa SePay và server
- Webhook bị timeout vì server xử lý quá lâu
- Đã hết số lần retry (tối đa 7 lần trong 33 phút)
Để không bỏ sót giao dịch, bạn nên chạy đối soát định kỳ: gọi SePay API lấy danh sách giao dịch, so khớp với database của bạn, rồi bổ sung những bản ghi còn thiếu.
Các bước
1. Lấy danh sách giao dịch từ SePay
Gọi API lấy giao dịch trong khoảng thời gian cần đối soát:
https://userapi.sepay.vn/v2/transactionsYYYY-MM-DD HH:mm:ssYYYY-MM-DD HH:mm:ssDanh sách đầy đủ tham số: SePay API - Danh sách giao dịch.
Ví dụ: lấy giao dịch ngày 01/03/2026
curl -X GET "https://userapi.sepay.vn/v2/transactions?transaction_date_from=2026-03-01%2000:00:00&transaction_date_to=2026-03-01%2023:59:59&per_page=100" \-H "Content-Type: application/json" \-H "Authorization: Bearer YOUR_API_TOKEN"
2. So khớp với database
So sánh giao dịch từ SePay với database của bạn. Dùng trường id (UUID) hoặc reference_number để xác định giao dịch nào bị thiếu.
3. Bổ sung giao dịch thiếu
Giao dịch có trên SePay nhưng không có trong database thì lưu bổ sung và xử lý logic (cập nhật đơn hàng, ghi nhận thanh toán...).
Code mẫu
<?php$token = getenv('SEPAY_API_TOKEN');$pdo = new PDO('mysql:host=localhost;dbname=db_name;charset=utf8mb4', 'db_user', 'db_pass',[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);$dateFrom = date('Y-m-d H:i:s', strtotime('-24 hours'));$dateTo = date('Y-m-d H:i:s');// 1. Lấy giao dịch 24h gần nhất từ SePay$url = 'https://userapi.sepay.vn/v2/transactions?' . http_build_query(['transaction_date_from' => $dateFrom,'transaction_date_to' => $dateTo,'per_page' => 100,]);$ctx = stream_context_create(['http' => ['header' => "Authorization: Bearer $token\r\nContent-Type: application/json",]]);$result = json_decode(file_get_contents($url, false, $ctx), true);$transactions = $result['data'] ?? [];printf("SePay: %d giao dich\n", count($transactions));// 2. Đã lưu những giao dịch nào?$stmt = $pdo->prepare('SELECT reference_number FROM tb_transactions WHERE created_at >= ?');$stmt->execute([$dateFrom]);$existing = array_flip($stmt->fetchAll(PDO::FETCH_COLUMN));// 3. Bổ sung phần còn thiếu. UNIQUE(sepay_id) đảm bảo không trùng.$insert = $pdo->prepare('INSERT IGNORE INTO tb_transactions(sepay_id, gateway, transaction_date, account_number, sub_account,amount_in, amount_out, accumulated, code, transaction_content, reference_number)VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');$missing = 0;foreach ($transactions as $tx) {if (isset($existing[$tx['reference_number']])) continue;$insert->execute([$tx['id'], $tx['bank_brand_name'], $tx['transaction_date'],$tx['account_number'], $tx['va'] ?? '',$tx['amount_in'] ?? 0, $tx['amount_out'] ?? 0, $tx['accumulated'] ?? 0,$tx['code'], $tx['transaction_content'], $tx['reference_number'],]);$missing++;}printf("Xong. Bo sung %d giao dich.\n", $missing);
Nên đặt cron job chạy đối soát tự động, ví dụ mỗi giờ:
0 * * * * node /path/to/reconcile.js >> /var/log/reconcile.log 2>&1
Chiến lược đối soát
Chọn một trong hai cách tuỳ vào tần suất chạy đối soát:
- Rate limit (giới hạn tốc độ gọi): API chỉ cho phép tối đa 3 request/giây. Vượt quá sẽ trả HTTP 429.
- Chống trùng: Luôn kiểm tra
id(UUID) hoặcreference_numbertrước khi lưu. - Phân trang: Dùng
pagevàper_page(tối đa 100). Khi dữ liệu lớn, chia nhỏ khoảng thời gian hoặc dùngsince_id. - Tài liệu API: Xem chi tiết tại SePay API - Danh sách giao dịch.
Tiếp theo
- Tạo QR và trang thanh toán: code mẫu trang thanh toán đầy đủ
- Sự cố: xem giao dịch bị mất từ webhook lỗi và phát lại
- Tích hợp webhook: chống trùng khi webhook và đối soát chạy song song