Transaction reconciliation

Reconcile transactions between your system and SePay using the API and dashboard so dropped or duplicate webhook deliveries never go unnoticed.

||

Why reconcile?

Webhooks deliver transactions in real time, but sometimes they don't reach your server:

  • Server is briefly down (deploy, restart, downtime)
  • Network error between SePay and your server
  • Webhook timed out because your server was too slow
  • Out of retry attempts (max 7 within 33 minutes)

To never miss a transaction, run periodic reconciliation against SePay API.

Steps

1. Fetch transactions from SePay

Call the API to get transactions in the period to reconcile:

GET
https://userapi.sepay.vn/v2/transactions
transaction_date_fromstring
Start date (inclusive), format YYYY-MM-DD HH:mm:ss
transaction_date_tostring
End date (inclusive), format YYYY-MM-DD HH:mm:ss
bank_account_idstring
Filter by bank account UUID (optional)
per_pageinteger
Transactions per page, max 100 (default 20)
pageinteger
Page number (default 1)
since_idstring
UUID of the last processed transaction; returns transactions newer than this

Full parameter list: SePay API - Transactions list.

Example: fetch transactions for 01/03/2026

Bash
1
2
3
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. Compare with your database

Compare SePay's transactions against your database. Use the id (UUID) or reference_number field to identify missing transactions.

3. Backfill missing transactions

Transactions present at SePay but missing in your database: insert and run business logic (update orders, mark payments, ...).

Code samples

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?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. Fetch last 24h transactions from 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 transactions\n", count($transactions));
 
// 2. What's already in DB?
$stmt = $pdo->prepare('SELECT reference_number FROM tb_transactions WHERE created_at >= ?');
$stmt->execute([$dateFrom]);
$existing = array_flip($stmt->fetchAll(PDO::FETCH_COLUMN));
 
// 3. Backfill missing. UNIQUE(sepay_id) prevents duplicates.
$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("Done. Backfilled %d transactions.\n", $missing);
Tip

Set up a cron job to reconcile automatically, e.g. every hour:

Bash
1
0 * * * * node /path/to/reconcile.js >> /var/log/reconcile.log 2>&1

Reconciliation strategies

Pick one depending on how often your reconciler runs:

Notes
  • Rate limit: API limited to 3 requests/second. Exceeding gets HTTP 429.
  • Deduplication: Always check id (UUID) or reference_number before inserting.
  • Pagination: Use page and per_page (max 100). For large data, narrow the time range or use since_id.
  • API docs: SePay API - Transactions list.

Next