Create a virtual account (VA)
Use this API to create a new virtual account (VA) for a Merchant, so you can receive balance notifications and manage cash flows separately.
This API registers for credit notifications (money in). To receive debit notifications (money out) on an ACB account, use the Register for debit notifications API.
Try the full account linking and OTP verification flow with simulated data.
Start a request to create or link a virtual account with the bank. The result returns a confirmId to use in the next confirmation step.
Header Request
| Header | Required | Description |
|---|---|---|
Content-Type | ✓ | application/json |
x-client-id | ✓ | Partner identifier issued by TINGEE. |
x-signature | ✓ | HMAC SHA512 authentication signature. See How to generate the signature. |
x-request-timestamp | ✓ | Time the request is sent (format: yyyyMMddHHmmssSSS, UTC+7) |
For VIB (bankBin: "970441"), the customer must register for the payment speaker on VIB iBusiness before you call the create VA API. See the steps in VIB account linking requirements or the VIB tab in Sample Request Body.
Body Parameter
1. Bank & general account information
| Field | Type | Required | Description |
|---|---|---|---|
bankBin | string | ✓ | Bank BIN (6 digits). See Supported banks. Examples: 970418 (BIDV), 970436 (VCB), 970422 (MBB), 970415 (VietinBank)... |
accountType | string | ~ | Account type (BankAccountTypeEnum):- personal-account: personal account.- business-account: business account.- business-household-account: household business account.Required for all banks except BAOKIM. |
accountNumber | string | ~ | Original bank account number to link. Not required for: TPBank, VCB, MSB and BAOKIM. |
accountName | string | ~ | Bank account holder name (uppercase, without diacritics). Not required for TPBank, PGBank, BAOKIM and business accounts of BIDV, OCB, VIB, SHINHAN, VietinBank (CTG). |
identity | string | ~ | Customer's identity document (ID card / citizen ID or tax code). Not applicable to VCB, ACB and business accounts. |
mobile | string | ~ | Phone number registered to receive the OTP / for linking. Format: 10 digits, starting with 0.Required for most banks (including Vietcombank personal accounts and all ACB account types). Not required for: VPBank, VietinBank (CTG), VIB, BAOKIM, TPBank, Vietcombank (except personal accounts) and business accounts of BIDV, Shinhan, OCB. |
isNotifyAccountNumber | boolean | ✕ | Link type: - false (default): create a virtual account.- true: link the original account number (receive balance notifications for the original account number).For VIB, the system always sets this to false.Only applies to supported banks. See Important notes for details. |
appType | string | ~ | Integration app type; pass baas.Required for Vietcombank (VCB) personal accounts. |
2. Virtual account & shop information
| Field | Type | Required | Description |
|---|---|---|---|
vaPrefix | string | ✕ | Virtual account (VA) prefix. The VA number ( vaPrefix + vaSuffix) must not contain the string QRD. |
vaSuffix | string | ~ | Virtual account (VA) suffix. Required for BIDV business accounts (used to build the VA number ${vaPrefix}${vaSuffix}) and OCB business accounts (when isNotifyAccountNumber = false).For VietinBank (CTG) business accounts: pass vaSuffix to build the VA number.For personal accounts (e.g. BIDV, OCB, VietinBank), if left empty, the system generates the suffix automatically. |
vaAccountNumber | string | ~ | Existing virtual account number to link (at least 4 characters). Required for MSB: the VA number already registered at MSB for the Merchant's account. |
merchantName | string | ~ | Name displayed at payment (VA account holder). Required for BIDV business accounts and OCB business accounts (when isNotifyAccountNumber = false).For BIDV and VietinBank (CTG) business accounts, the name must start with the registered business name. For other banks, the value is used as the shop name when the system automatically creates a new shop. |
merchantId | number | ~ | ID of the sub-merchant to create the VA for (required if you connect with a Master Merchant account). |
shopId | number | ✕ | ID of the shop to assign the VA to. If you pass this parameter, the system assigns the created VA to that shop. If left empty, the system automatically creates a new shop when creating the VA and assigns the VA to it. (The shop ID is returned in the response). |
merchantAddress | string | ✕ | Shop/merchant address (used when a new shop is created automatically). |
name | string | ✕ | Cash flow name / cash flow identifier for internal management. |
taxCode | string | ✕ | Tax code of the business / household business. |
3. Bank-specific parameters
| Bank | Field | Type | Required | Description |
|---|---|---|---|---|
| Vietcombank (VCB) | redirectUrl | string | ~ | URL that takes the customer back to the partner's system after completing verification in the VCB Digibank app. Required for Vietcombank personal accounts. |
| Vietcombank (VCB) & TPBank | webhookUrl | string | ~ | URL that receives the account registration/cancellation result from the bank. Required for TPBank (all account types) or Vietcombank (personal, household business). See the data structure in Account link status webhook. |
| TPBank (TPB) | requestId | string | ~ | Unique request identifier from the partner. Required for TPBank. |
| ACB | acbUserId | string | ~ | Login user ID on ACB Online / Biz. Required for ACB business or household business accounts. |
| Bảo Kim (BAOKIM) | walletId | string | ~ | ID of the Bảo Kim e-wallet to create the virtual account number for. Required when bankBin = BAOKIM. |
isNotifyAccountNumber = true)- Banks that support linking the original account number:
- ACB, TPBank: Support all account types (personal, household business, business). (Note: for TPBank, only business accounts have a VA; personal and household business accounts do not.)
- VPBank: Supports business and personal accounts.
- OCB: Supports business and household business accounts.
- VietinBank: Supports personal and household business accounts.
- PGBank, Co-opBank: Only support linking the original account number (no VA; the system handles this automatically).
- How transaction webhooks work by account type:
- For accounts linked by original account number (
isNotifyAccountNumber = true): only transfers to the original account number trigger a webhook from Tingee. - For accounts that do not support original account linking (VA only,
isNotifyAccountNumber = false): transfers must go to the virtual account (VA) number. If money is transferred to the original account number, the bank does not send the transaction to Tingee → Tingee does not receive the transaction from the bank → no webhook.
- For accounts linked by original account number (
- For all other banks, the system only supports creating virtual accounts (VA) by default; set
isNotifyAccountNumber = falseor omit it. - See Original account number linking support table for details.
Sample Request Body by bank
- Common personal (MBB, OCB, Sacombank...)
- BIDV & VietinBank (business)
- Vietcombank (personal)
- TPBank
- ACB (business / household business)
- Bảo Kim
- VIB
Use this to link personal accounts at standard banks (MB Bank, Sacombank, OCB, Shinhan, BIDV personal...):
{
"bankBin": "970422",
"accountType": "personal-account",
"accountNumber": "0935926988",
"accountName": "NGUYEN THI HUYEN",
"identity": "040191014068",
"mobile": "0123456789",
"isNotifyAccountNumber": false
}
Applies to BIDV (970418) and VietinBank (970415) business accounts. The system builds the VA number as vaPrefix + vaSuffix:
{
"bankBin": "970418",
"accountType": "business-account",
"accountNumber": "1234567890",
"vaPrefix": "TINGEE",
"vaSuffix": "8888",
"merchantName": "CONG TY TNHH ABC CUA HANG 1",
"isNotifyAccountNumber": false
}
Note:
merchantNamemust start with the exact business name registered with the bank.
Applies to Vietcombank personal accounts (970436). The customer opens the VCB Digibank app via a Deep Link to complete the link verification:
{
"bankBin": "970436",
"accountType": "personal-account",
"accountName": "NGUYEN VAN A",
"mobile": "0935926988",
"appType": "baas",
"redirectUrl": "https://your-domain.com/vcb-link/result",
"webhookUrl": "https://your-domain.com/webhook/vcb-link",
"isNotifyAccountNumber": false
}
Applies to TPBank (970423). Pass a unique requestId and a webhookUrl to receive the linking result from TPBank Biz:
{
"bankBin": "970423",
"accountType": "personal-account",
"requestId": "REQ_20261008_0001",
"webhookUrl": "https://your-domain.com/webhook/tpbank-link",
"isNotifyAccountNumber": false
}
Applies when creating a business or household business account at ACB (970416):
{
"bankBin": "970416",
"accountType": "business-account",
"accountNumber": "88889999",
"accountName": "CONG TY CP CONG NGHE XYZ",
"mobile": "0935926988",
"acbUserId": "ACB_USER_LOGIN_ID",
"isNotifyAccountNumber": false
}
Applies when linking a Bảo Kim e-wallet:
{
"bankBin": "BAOKIM",
"walletId": "WALLET_BAOKIM_123456",
"isNotifyAccountNumber": false
}
The customer must complete the following steps on VIB's system before you call the create VA API:
- Register an account on the MyVIB app (if they don't have one).
- In the MyVIB app, register for the iBusiness package.
- Use the MyVIB account to log in to VIB iBusiness and click Đăng ký nhận loa (Register for payment speaker).
Sample request body for creating a VA at VIB (970441):
{
"bankBin": "970441",
"accountType": "personal-account",
"accountNumber": "0123456789",
"accountName": "NGUYEN VAN A",
"identity": "001200012345",
"isNotifyAccountNumber": false
}
Code samples
- cURL
- NestJS
- C#
- Java
- PHP
curl --location --request POST 'https://open-api.tingee.vn/v1/create-va' \
--header 'accept: application/json' \
--header 'x-client-id: YOUR_CLIENT_ID' \
--header 'x-request-timestamp: 20240509154004003' \
--header 'x-signature: YOUR_SIGNATURE' \
--header 'Content-Type: application/json' \
--data '{
"bankBin": "970418",
"accountType": "personal-account",
"accountNumber": "0935926988",
"accountName": "NGUYEN THI HUYEN",
"identity": "040191014068",
"mobile": "0123456789",
"isNotifyAccountNumber": false
}'
import axios from "axios";
import { createHmac } from "crypto";
const CLIENT_ID = "YOUR_CLIENT_ID";
const SECRET_KEY = "YOUR_SECRET_KEY";
// Timestamp in UTC+7, format yyyyMMddHHmmssSSS
function getTimestamp() {
return new Date(Date.now() + 7 * 60 * 60 * 1000)
.toISOString()
.replace(/[-:TZ.]/g, "")
.slice(0, 17);
}
async function createVa() {
const body = {
bankBin: "970418",
accountType: "personal-account",
accountNumber: "0935926988",
accountName: "NGUYEN THI HUYEN",
identity: "040191014068",
mobile: "0123456789",
isNotifyAccountNumber: false,
};
const bodyJson = JSON.stringify(body);
const timestamp = getTimestamp();
const signature = createHmac("sha512", SECRET_KEY)
.update(`${timestamp}:${bodyJson}`)
.digest("hex");
const res = await axios.request({
method: "POST",
url: "https://open-api.tingee.vn/v1/create-va",
data: bodyJson,
headers: {
accept: "application/json",
"Content-Type": "application/json",
"x-client-id": CLIENT_ID,
"x-request-timestamp": timestamp,
"x-signature": signature,
},
});
console.log(res.data);
}
createVa();
using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading.Tasks;
public class Program
{
private const string ClientId = "YOUR_CLIENT_ID";
private const string SecretKey = "YOUR_SECRET_KEY";
private static readonly HttpClient Client = new HttpClient();
public static async Task Main()
{
var body = new
{
bankBin = "970418",
accountType = "personal-account",
accountNumber = "0935926988",
accountName = "NGUYEN THI HUYEN",
identity = "040191014068",
mobile = "0123456789",
isNotifyAccountNumber = false
};
// Do not escape non-ASCII / special characters so the JSON matches TINGEE's JSON.stringify
var jsonOptions = new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
var bodyJson = JsonSerializer.Serialize(body, jsonOptions);
// Timestamp in UTC+7, format yyyyMMddHHmmssSSS
var timestamp = DateTime.UtcNow.AddHours(7).ToString("yyyyMMddHHmmssfff");
var signature = HmacSha512(SecretKey, $"{timestamp}:{bodyJson}");
var request = new HttpRequestMessage(HttpMethod.Post, "https://open-api.tingee.vn/v1/create-va");
request.Headers.Add("accept", "application/json");
request.Headers.Add("x-client-id", ClientId);
request.Headers.Add("x-request-timestamp", timestamp);
request.Headers.Add("x-signature", signature);
request.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json");
var response = await Client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
}
private static string HmacSha512(string key, string message)
{
using var hmac = new HMACSHA512(Encoding.UTF8.GetBytes(key));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
return Convert.ToHexString(hash).ToLower();
}
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class Main {
private static final String CLIENT_ID = "YOUR_CLIENT_ID";
private static final String SECRET_KEY = "YOUR_SECRET_KEY";
public static void main(String[] args) throws Exception {
// Minified JSON, same as JSON.stringify()
String bodyJson = "{\"bankBin\":\"970418\",\"accountType\":\"personal-account\",\"accountNumber\":\"0935926988\",\"accountName\":\"NGUYEN THI HUYEN\",\"identity\":\"040191014068\",\"mobile\":\"0123456789\",\"isNotifyAccountNumber\":false}";
// Timestamp in UTC+7, format yyyyMMddHHmmssSSS
String timestamp = ZonedDateTime.now(ZoneId.of("Asia/Ho_Chi_Minh"))
.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
String signature = hmacSha512(SECRET_KEY, timestamp + ":" + bodyJson);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://open-api.tingee.vn/v1/create-va"))
.header("accept", "application/json")
.header("Content-Type", "application/json")
.header("x-client-id", CLIENT_ID)
.header("x-request-timestamp", timestamp)
.header("x-signature", signature)
.method("POST", HttpRequest.BodyPublishers.ofString(bodyJson, StandardCharsets.UTF_8))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
private static String hmacSha512(String key, String data) throws Exception {
Mac mac = Mac.getInstance("HmacSHA512");
mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA512"));
byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : hash) hex.append(String.format("%02x", b));
return hex.toString();
}
}
<?php
$clientId = "YOUR_CLIENT_ID";
$secretKey = "YOUR_SECRET_KEY";
$body = [
"bankBin" => "970418",
"accountType" => "personal-account",
"accountNumber" => "0935926988",
"accountName" => "NGUYEN THI HUYEN",
"identity" => "040191014068",
"mobile" => "0123456789",
"isNotifyAccountNumber" => false,
];
// Do not escape "/" or non-ASCII characters so the JSON matches TINGEE's JSON.stringify
$bodyJson = json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
// Timestamp in UTC+7, format yyyyMMddHHmmssSSS
$now = new DateTime("now", new DateTimeZone("Asia/Ho_Chi_Minh"));
$timestamp = $now->format("YmdHisv");
$signature = hash_hmac("sha512", $timestamp . ":" . $bodyJson, $secretKey);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => "https://open-api.tingee.vn/v1/create-va",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => $bodyJson,
CURLOPT_HTTPHEADER => [
"Accept: application/json",
"Content-Type: application/json",
"x-client-id: " . $clientId,
"x-request-timestamp: " . $timestamp,
"x-signature: " . $signature,
],
]);
$response = curl_exec($ch);
if ($response === false) {
echo "cURL Error: " . curl_error($ch);
} else {
echo $response;
}
curl_close($ch);
Sample Response
The fields in data depend on the bank and the account type. There are 2 main result types:
- OTP confirmation required
- Created immediately
The bank sends an OTP to the account holder. Use the confirmId to call the Confirm account number API:
{
"code": "00",
"message": "Success",
"data": {
"confirmId": "886b05e9-2d94-4b41-9839-138c941fd1d9"
}
}
The account is created immediately, with no confirmation step (e.g. a configured business account, MSB):
{
"code": "00",
"message": "Success",
"data": {
"merchantAccountNumberId": 12345,
"bankName": "BIDV",
"shopId": 1024,
"status": "active",
"vaAccountNumber": "TINGEE8888"
}
}
| Field | Type | Description |
|---|---|---|
code | string | Result code (00 = success). |
message | string | Response message. |
data.confirmId | string | Confirmation ID to use with the Confirm account number API. Only returned when the bank requires OTP verification. For Vietcombank personal accounts, the linking result is sent via webhookUrl instead of calling confirm-va. |
data.merchantAccountNumberId | number | Account ID on the TINGEE system. Only returned when the account is created immediately (no OTP needed). |
data.bankName | string | Bank code (e.g. BIDV, CTG, MSB). Returned when the account is created immediately. |
data.shopId | number | ID of the shop the account is assigned to (the shop you passed or the one the system created). Returned when the account is created immediately. |
data.status | string | Account status: active, inactive, lock. Returned when the account is created immediately. |
data.vaAccountNumber | string | Virtual account (VA) number. Returned when the account is created immediately; for OCB and Vietcombank personal accounts, it is returned together with confirmId (the expected VA number, only valid after confirmation). |
data.authorizeLink | string | TPBank BIZ web login link where the customer verifies the link (TPBank only; in this case there is no confirmId and the result is sent via webhookUrl). |
data.deepLink | string | Link that opens the Vietcombank Digibank app (Vietcombank personal accounts only). |
data.otpMethod | string | OTP verification method set by the bank: SMSTTT, SmartOTP, mCode (Sacombank only). |
Some special cases return only part of the information: linking the original account number of an OCB business account returns only shopId. Use the List account numbers API to check the account after creation.
Common error codes
| Code | Description | How to handle |
|---|---|---|
1021 | Bank or account type not supported | Check bankBin and accountType against Supported banks. |
1083 | Invalid VA creation details | This bank/account type does not allow passing vaSuffix. See Body Parameter. |
1084 | Virtual account number contains invalid characters | Check vaPrefix and vaSuffix. |
1013 | Business account not configured | Complete the Business account configuration before creating a VA for a business account. |
1082 | Invalid display name | merchantName must start with the registered business name (BIDV, VietinBank). |
1022, 1048 | Account / VA number already exists | The account number or VA has already been linked / registered. Use List account numbers to check. |
1081 | Account number already linked to another Merchant | Each account number can only be linked to one Merchant. |
1031, 1085 | Shop does not exist or is inactive | Check that shopId belongs to the Merchant and is active, or leave it empty so the system creates a shop. |
1037, 1103 | The shop's TingeeBox device does not support this bank | Choose another shop or a bank that matches the device configuration. |
1133 | Not authorized | Linking VPBank or Vietcombank business / household business accounts must be done with the Merchant's API key, not the Master Merchant API key. |
1087, 1088 | Bảo Kim wallet not configured / synced | Check walletId and the Bảo Kim wallet sync status. |
90, 91, 92, 97 | Request authentication error (timestamp, client-id, signature) | See System error codes. |
Others | Other errors | See Error codes. |