Payment result webhook
When a payment succeeds via a static or dynamic QR code, the TINGEE system automatically sends a webhook notification to the URL you registered in your configuration.
- Accounts linked by original account number (
isNotifyAccountNumber = true): A transfer to the original account number triggers a balance notification from the bank to Tingee → Tingee sends a webhook to the partner. - Accounts that do NOT support original account number linking (VA only,
isNotifyAccountNumber = false):- The payer must transfer to the issued virtual account (VA) number.
- If the money is transferred directly to the original account number, the bank does not send the transaction notification to Tingee → Tingee does not receive the transaction from the bank → No webhook.
- See the list of supported accounts in the Original account number linking support table.
Push notification of transaction data from Tingee to your system as soon as money arrives in the account.
Headers sent
These headers help you identify the request and verify the integrity of the received data.
| Header | Description |
|---|---|
Content-Type | Always application/json. |
x-request-id | Unique identifier of each webhook request (UUID). |
x-request-timestamp | Time the notification is sent (format: yyyyMMddHHmmssSSS, UTC+7). |
x-signature | HMAC SHA512 signature used to verify the data. |
Body Parameter
The transaction details are sent in the body.
| Field | Type | Description |
|---|---|---|
clientId | string | Your Client ID. |
transactionCode | string | System transaction code. |
amount | number | Actual amount paid. |
content | string | Transfer description or note. |
bank | string | Receiving bank name. |
accountNumber | string | Receiving bank account number. |
vaAccountNumber | string | Virtual account number (if any). |
transactionDate | string | Transaction time (format: yyyyMMddHHmmss). |
type | string | Transaction type - credit: Credit transaction - debit: Debit transaction If not sent, defaults to Credit. |
additionalData | array | Additional information (e.g. billId for dynamic QR). |
VietinBank does not return the payment description when a transaction occurs, so the webhook from Tingee to the partner will not include the content field either.
Webhook authentication
You must verify the x-signature signature to make sure the webhook really comes from Tingee.
The signature is generated with: HMAC_SHA512(timestamp + ":" + json_body, secretKey).
Verification code samples
- NestJS
- C#
- Java
- PHP
import { createHmac } from 'crypto';
function verifyWebhook(headers: any, body: any, secretKey: string): boolean {
const signature = headers['x-signature'];
const timestamp = headers['x-request-timestamp'];
const message = `${timestamp}:${JSON.stringify(body)}`;
const computedSig = createHmac('sha512', secretKey).update(message).digest('hex');
return computedSig === signature;
}
using System;
using System.Security.Cryptography;
using System.Text;
public class WebhookVerifier
{
public static bool VerifyWebhook(string body, string timestamp, string signature, string secretKey)
{
var message = $"{timestamp}:{body}";
var keyBytes = Encoding.UTF8.GetBytes(secretKey);
var messageBytes = Encoding.UTF8.GetBytes(message);
using (var hmac = new HMACSHA512(keyBytes))
{
var hash = hmac.ComputeHash(messageBytes);
var computedSig = BitConverter.ToString(hash).Replace("-", "").ToLower();
return computedSig == signature;
}
}
}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
public class WebhookVerifier {
public static boolean verifySignature(String body, String timestamp, String signature, String secretKey) throws Exception {
String message = timestamp + ":" + body;
Mac hmac = Mac.getInstance("HmacSHA512");
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA512");
hmac.init(keySpec);
byte[] hash = hmac.doFinal(message.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : hash) {
sb.append(String.format("%02x", b));
}
return sb.toString().equals(signature);
}
}
function verifyWebhook($headers, $body, $secretKey) {
$timestamp = $headers['x-request-timestamp'] ?? '';
$signature = $headers['x-signature'] ?? '';
$message = $timestamp . ':' . $body;
$computedSig = hash_hmac('sha512', $message, $secretKey);
return hash_equals($computedSig, $signature);
}
Responding to the request
After processing successfully, your server must respond with HTTP 200 OK and the following JSON:
{
"code": "00",
"message": "Success"
}
Retry & Logging
Tingee always makes every effort to ensure you receive notifications:
- Retry: If your server responds with an error or does not respond (timeout), Tingee automatically resends the webhook up to 5 times, 1 minute apart.
- Idempotency: Use
transactionCodeto check whether a transaction has already been processed, to avoid duplicate processing when you receive a webhook retry. - Audit Log: We recommend logging the full webhook payload and headers to make investigation easier when issues arise.
⚠️ Note — Dynamic QR transactions
For payments via dynamic QR, the payer may change the amount when scanning the code, so the amount actually received differs from the amount embedded in the QR code.
📌 Most banks now do not allow the amount to be edited when paying via dynamic QR. However, some banks have not yet applied this restriction, so the risk still exists.
In this case, TINGEE still records the transaction and sends the webhook to you as usual.
You must actively reconcile and verify the transaction using additional sources such as:
- The customer's transaction screenshot
- The statement of the bank account that actually received the money
Only confirm the transaction as complete after checking that the actual amount matches the invoice amount.
Never expose your secretKey in source code, and only process transactions when the x-signature signature is fully valid.