Nhận Webhook
Webhook
Cập nhật: 23/03/2026
Nhận Webhook
ThueAPI gửi HTTP POST request đến URL webhook của bạn khi có giao dịch mới.
Method & Endpoint
POST https://your-domain.com/your-webhook-path
Content-Type: application/json
X-Webhook-Signature: <hmac-sha256-signature>
Headers
| Header | Mô tả |
|---|---|
Content-Type | application/json |
X-Webhook-Signature | HMAC-SHA256 chữ ký toàn bộ payload dùng Secret Key |
Payload
ThueAPI gửi HTTP POST với Content-Type application/json:
{
"transactions": [
{
"id": "3521",
"gateway": "ACB",
"transactionDate": "2025-10-14 10:59:30",
"transactionNumber": "FT253146879VN",
"accountNumber": "19354957",
"content": "NAPTIEN MOMO 963869789",
"transferType": "IN",
"transferAmount": 200000,
"checksum": "a1b2c3d4e5f6..."
}
]
}
Mô tả các trường
| Trường | Kiểu | Mô tả |
|---|---|---|
| id | string | ID giao dịch trong hệ thống ThueAPI |
| gateway | string | Mã ngân hàng: ACB, VCB, TCB, v.v. |
| transactionDate | string | Thời gian giao dịch (Y-m-d H:i:s) |
| transactionNumber | string | Mã giao dịch duy nhất từ ngân hàng |
| accountNumber | string | Số tài khoản nhận/gửi |
| content | string | Nội dung chuyển khoản |
| transferType | string | IN (tiền vào) hoặc OUT (tiền ra) |
| transferAmount | integer | Số tiền (đơn vị: VND) |
| checksum | string | Mã duy nhất theo giao dịch — dùng để xác định giao dịch là duy nhất, tránh xử lý trùng lặp. |
Headers
POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
X-Webhook-Signature: sha256=abc123def456...
Code mẫu xử lý Webhook
PHP (Laravel)
<?php
// routes/api.php
Route::post('/webhook/thueapi', [WebhookController::class, 'handle']);
// app/Http/Controllers/WebhookController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use App\Models\Transaction;
class WebhookController extends Controller
{
public function handle(Request $request)
{
// Bước 1: Xác thực chữ ký HMAC
$secret = config('services.thueapi.webhook_secret');
$payload = $request->getContent();
$signature = $request->header('X-Webhook-Signature');
$expected = hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expected, $signature)) {
Log::warning('ThueAPI webhook: chữ ký không hợp lệ');
return response()->json(['error' => 'Invalid signature'], 401);
}
// Bước 2: Verify checksum per-transaction (tuỳ chọn)
// $txChecksum = $tx['checksum']; unset($tx['checksum']); ksort($tx);
// $valid = hash_hmac('sha256', json_encode($tx), $secret) === $txChecksum;
// Bước 3: Xử lý từng giao dịch
$transactions = $request->input('transactions', []);
foreach ($transactions as $tx) {
// Kiểm tra trùng lặp theo transactionNumber
$exists = Transaction::where('transaction_number', $tx['transactionNumber'])->exists();
if ($exists) {
Log::info('ThueAPI: bỏ qua giao dịch trùng #' . $tx['transactionNumber']);
continue;
}
// Lưu vào database
Transaction::create([
'transaction_number' => $tx['transactionNumber'],
'gateway' => $tx['gateway'],
'account_number' => $tx['accountNumber'],
'transfer_type' => $tx['transferType'],
'transfer_amount' => $tx['transferAmount'],
'content' => $tx['content'],
'transaction_date' => $tx['transactionDate'],
]);
}
// Bước 3: Phản hồi thành công — BẮT BUỘC
return response()->json(['success' => true]);
}
}
PHP (Vanilla)
<?php
// webhook.php — Xử lý webhook từ ThueAPI.VN
$webhookSecret = 'YOUR_WEBHOOK_SECRET';
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
// Xác thực chữ ký
$expected = hash_hmac('sha256', $payload, $webhookSecret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
$data = json_decode($payload, true);
if (!isset($data['transactions'])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid payload']);
exit;
}
// Kết nối database
$pdo = new PDO('mysql:host=localhost;dbname=your_db', 'user', 'pass');
$stmt = $pdo->prepare('
INSERT IGNORE INTO transactions
(transaction_number, gateway, account_number, transfer_type, transfer_amount, content, transaction_date)
VALUES (?, ?, ?, ?, ?, ?, ?)
');
foreach ($data['transactions'] as $tx) {
$stmt->execute([
$tx['transactionNumber'],
$tx['gateway'],
$tx['accountNumber'],
$tx['transferType'],
$tx['transferAmount'],
$tx['content'],
$tx['transactionDate'],
]);
}
http_response_code(200);
echo json_encode(['success' => true]);
Node.js (Express)
// webhook-handler.js — Express webhook receiver
const express = require('express');
const crypto = require('crypto');
const app = express();
const SECRET = 'YOUR_WEBHOOK_SECRET';
// Dùng raw body để tính HMAC chính xác
app.use('/webhook', express.raw({ type: 'application/json' }));
app.post('/webhook/thueapi', (req, res) => {
const payload = req.body; // Buffer
const signature = req.headers['x-webhook-signature'];
// Xác thực chữ ký HMAC-SHA256
const expected = crypto
.createHmac('sha256', SECRET)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).json({ error: 'Invalid signature' });
}
const data = JSON.parse(payload.toString());
const { transactions } = data;
// Xử lý bất đồng bộ — không chặn response
setImmediate(async () => {
for (const tx of transactions) {
console.log(`[${tx.transactionDate}] ${tx.transferType} ${tx.transferAmount} VND`);
console.log(`Nội dung: ${tx.content}`);
// TODO: Lưu database, gửi thông báo, cập nhật đơn hàng
}
});
// Phản hồi ngay lập tức
res.json({ success: true });
});
app.listen(3000, () => console.log('Webhook server đang chạy trên port 3000'));
Python (Flask)
# webhook_handler.py — Flask webhook receiver
import hmac
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET = b'YOUR_WEBHOOK_SECRET'
@app.route('/webhook/thueapi', methods=['POST'])
def handle_webhook():
# Lấy raw body để tính HMAC chính xác
payload = request.get_data()
signature = request.headers.get('X-Webhook-Signature', '')
# Xác thực chữ ký HMAC-SHA256
expected = hmac.new(SECRET, payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
return jsonify({'error': 'Invalid signature'}), 401
data = request.get_json(force=True)
for tx in data.get('transactions', []):
print(f"[{tx['transactionDate']}] {tx['transferType']} "
f"{tx['transferAmount']:,} VND - {tx['content']}")
# TODO: Lưu database
return jsonify({'success': True})
if __name__ == '__main__':
app.run(port=5000)
Go (net/http)
// main.go — Go webhook receiver
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
)
const webhookSecret = "YOUR_WEBHOOK_SECRET"
type Transaction struct {
ID string `json:"id"`
Gateway string `json:"gateway"`
TransactionDate string `json:"transactionDate"`
TransactionNumber string `json:"transactionNumber"`
AccountNumber string `json:"accountNumber"`
Content string `json:"content"`
TransferType string `json:"transferType"`
TransferAmount int64 `json:"transferAmount"`
}
type WebhookPayload struct {
Transactions []Transaction `json:"transactions"`
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, `{"error":"cannot read body"}`, http.StatusBadRequest)
return
}
// Xác thực HMAC-SHA256
signature := r.Header.Get("X-Webhook-Signature")
mac := hmac.New(sha256.New, []byte(webhookSecret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(signature)) {
http.Error(w, `{"error":"invalid signature"}`, http.StatusUnauthorized)
return
}
var payload WebhookPayload
if err := json.Unmarshal(body, &payload); err != nil {
http.Error(w, `{"error":"invalid json"}`, http.StatusBadRequest)
return
}
for _, tx := range payload.Transactions {
fmt.Printf("[%s] %s %d VND - %s\n",
tx.TransactionDate, tx.TransferType, tx.TransferAmount, tx.Content)
// TODO: Lưu database
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"success":true}`))
}
func main() {
http.HandleFunc("/webhook/thueapi", webhookHandler)
fmt.Println("Webhook server chạy trên :8080")
http.ListenAndServe(":8080", nil)
}
C# (ASP.NET)
// WebhookController.cs — ASP.NET Core
using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
[ApiController]
[Route("webhook")]
public class WebhookController : ControllerBase
{
private const string WebhookSecret = "YOUR_WEBHOOK_SECRET";
[HttpPost("thueapi")]
public async Task<IActionResult> Handle()
{
// Đọc raw body
using var reader = new StreamReader(Request.Body, Encoding.UTF8);
var payload = await reader.ReadToEndAsync();
var signature = Request.Headers["X-Webhook-Signature"].ToString();
// Xác thực HMAC-SHA256
var key = Encoding.UTF8.GetBytes(WebhookSecret);
using var hmac = new HMACSHA256(key);
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
var expected = Convert.ToHexString(hash).ToLower();
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(signature)))
{
return Unauthorized(new { error = "Invalid signature" });
}
var data = JsonSerializer.Deserialize<WebhookPayload>(payload);
foreach (var tx in data?.Transactions ?? [])
{
Console.WriteLine($"[{tx.TransactionDate}] {tx.TransferType} {tx.TransferAmount} VND");
// TODO: Lưu database
}
return Ok(new { success = true });
}
}
public record WebhookPayload(List<Transaction> Transactions);
public record Transaction(
string Id, string Gateway, string TransactionDate,
string TransactionNumber, string AccountNumber,
string Content, string TransferType, long TransferAmount);
Java (Spring Boot)
// WebhookController.java — Spring Boot
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/webhook")
public class WebhookController {
private static final String WEBHOOK_SECRET = "YOUR_WEBHOOK_SECRET";
@PostMapping("/thueapi")
public ResponseEntity<Map<String, Boolean>> handle(
@RequestBody String payload,
@RequestHeader("X-Webhook-Signature") String signature) throws Exception {
// Xác thực HMAC-SHA256
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(WEBHOOK_SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
String expected = HexFormat.of().formatHex(hash);
if (!expected.equals(signature)) {
return ResponseEntity.status(401).body(Map.of("success", false));
}
// Parse JSON (dùng Jackson/Gson)
// ObjectMapper mapper = new ObjectMapper();
// WebhookPayload data = mapper.readValue(payload, WebhookPayload.class);
System.out.println("Webhook hợp lệ từ ThueAPI: " + payload);
return ResponseEntity.ok(Map.of("success", true));
}
}