Deep Link transaction result redirect
After the user completes the steps in the banking app, Tingee automatically redirects the user back to the partner's app or website along with the transaction result.
Tingee appends the transaction result directly to redirectUrl as a query string.
Query Parameters
The parameters appended to the redirect URL.
| Parameter | Type | Description |
|---|---|---|
amount | number | Amount in the transaction's QR code. |
bankName | string | Receiving bank code (e.g. MBB). |
bankBin | string | Receiving bank BIN. |
accountNumber | string | Recipient account number. |
content | string | Transfer description. |
deepLinkStatus | string | Status: success, pending (processing), failed. |
transactionDate | number | Redirect time (timestamp in ms). |
clientId | string | Partner client ID. |
signature | string | HMAC SHA512 signature that ensures the data has not been tampered with. |
Verification rules
For security, the partner must verify the received signature. The signature is generated as follows:
- Take all URL parameters (except
signature), withamountandtransactionDateas numbers and the remaining parameters as strings. - Sort the keys alphabetically and serialize them to a JSON string (no whitespace, no escaping of unicode characters or
/). Example:{"accountNumber":"0123456789","amount":100000,"bankBin":"970422","bankName":"MBB","clientId":"YOUR_CLIENT_ID","content":"Thanh toan don hang","deepLinkStatus":"success","transactionDate":1736930449000}. - Sign that JSON string with HMAC SHA512 using the partner's
secretKey; the result is hex-encoded.
Verification code samples
- NestJS
- C#
- Java
- PHP
import { createHmac } from 'crypto';
function verifySignature(query: Record<string, string>, secretKey: string): boolean {
const { signature, ...data } = query;
// Sort keys alphabetically; amount and transactionDate are numbers
const sorted: Record<string, any> = {};
for (const key of Object.keys(data).sort()) {
sorted[key] = key === 'amount' || key === 'transactionDate' ? Number(data[key]) : data[key];
}
const computedSig = createHmac('sha512', secretKey)
.update(JSON.stringify(sorted))
.digest('hex');
return computedSig === signature;
}
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
public static bool VerifySignature(IDictionary<string, string> query, string secretKey)
{
if (!query.ContainsKey("signature")) return false;
var signature = query["signature"];
// Sort keys alphabetically; amount and transactionDate are numbers
var data = new SortedDictionary<string, object>(StringComparer.Ordinal);
foreach (var item in query)
{
if (item.Key == "signature") continue;
if (item.Key == "amount" || item.Key == "transactionDate")
data[item.Key] = long.Parse(item.Value);
else
data[item.Key] = item.Value;
}
var canonicalString = JsonSerializer.Serialize(data, new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
var keyBytes = Encoding.UTF8.GetBytes(secretKey);
var messageBytes = Encoding.UTF8.GetBytes(canonicalString);
using (var hmac = new HMACSHA512(keyBytes))
{
var hash = hmac.ComputeHash(messageBytes);
var computedSig = BitConverter.ToString(hash).Replace("-", "").ToLower();
return computedSig == signature;
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class SignatureUtil {
public static boolean verifySignature(Map<String, String> query, String secretKey) throws Exception {
String signature = query.get("signature");
if (signature == null) return false;
// Sort keys alphabetically; amount and transactionDate are numbers
Map<String, Object> data = new TreeMap<>();
for (Map.Entry<String, String> entry : query.entrySet()) {
String key = entry.getKey();
if (key.equals("signature")) continue;
if (key.equals("amount") || key.equals("transactionDate")) {
data.put(key, Long.parseLong(entry.getValue()));
} else {
data.put(key, entry.getValue());
}
}
String canonicalString = new ObjectMapper().writeValueAsString(data);
Mac sha512_HMAC = Mac.getInstance("HmacSHA512");
SecretKeySpec secret_key = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA512");
sha512_HMAC.init(secret_key);
byte[] hash = sha512_HMAC.doFinal(canonicalString.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : hash) sb.append(String.format("%02x", b));
return sb.toString().equals(signature);
}
}
function verifySignature($query, $secretKey) {
$signature = $query['signature'] ?? '';
unset($query['signature']);
// amount and transactionDate are numbers
foreach (['amount', 'transactionDate'] as $numberKey) {
if (isset($query[$numberKey])) {
$query[$numberKey] = (int) $query[$numberKey];
}
}
// Sort keys alphabetically
ksort($query, SORT_STRING);
$canonicalString = json_encode($query, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$computedSig = hash_hmac('sha512', $canonicalString, $secretKey);
return hash_equals($computedSig, $signature);
}
Sample redirect URL
Tingee redirects the user to the following URL:
https://partner.vn/callback?deepLinkStatus=success&transactionDate=1736930449000&clientId=YOUR_CLIENT_ID&amount=100000&bankName=MBB&bankBin=970422&accountNumber=0123456789&content=Thanh%20toan%20don%20hang&signature=91a2a8b7d...
Important notes
- If your original
redirectUrlalready contains a query string, Tingee automatically adds&so that your existing URL structure is not broken. - Always check the
deepLinkStatusfield to know whether the transaction succeeded or failed before continuing your business logic. - We recommend logging the full callback URL for auditing when needed.