curl --request POST \
--url https://www.ntanduy.com/api/v1/tools/bank-qr \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"bank": "MB",
"accountNumber": "0123456789",
"amount": 100000,
"memo": "Thanh toán DH 1028",
"imageSize": 512
}
'import requests
url = "https://www.ntanduy.com/api/v1/tools/bank-qr"
payload = {
"bank": "MB",
"accountNumber": "0123456789",
"amount": 100000,
"memo": "Thanh toán DH 1028",
"imageSize": 512
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
bank: 'MB',
accountNumber: '0123456789',
amount: 100000,
memo: 'Thanh toán DH 1028',
imageSize: 512
})
};
fetch('https://www.ntanduy.com/api/v1/tools/bank-qr', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://www.ntanduy.com/api/v1/tools/bank-qr",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'bank' => 'MB',
'accountNumber' => '0123456789',
'amount' => 100000,
'memo' => 'Thanh toán DH 1028',
'imageSize' => 512
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://www.ntanduy.com/api/v1/tools/bank-qr"
payload := strings.NewReader("{\n \"bank\": \"MB\",\n \"accountNumber\": \"0123456789\",\n \"amount\": 100000,\n \"memo\": \"Thanh toán DH 1028\",\n \"imageSize\": 512\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://www.ntanduy.com/api/v1/tools/bank-qr")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"bank\": \"MB\",\n \"accountNumber\": \"0123456789\",\n \"amount\": 100000,\n \"memo\": \"Thanh toán DH 1028\",\n \"imageSize\": 512\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.ntanduy.com/api/v1/tools/bank-qr")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"bank\": \"MB\",\n \"accountNumber\": \"0123456789\",\n \"amount\": 100000,\n \"memo\": \"Thanh toán DH 1028\",\n \"imageSize\": 512\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"data": {
"bank": {
"bin": "970422",
"code": "MB",
"shortName": "MBBank",
"name": "Ngân hàng TMCP Quân đội",
"transferSupported": true
},
"transfer": {
"accountNumber": "0123456789",
"amount": 100000,
"memo": "Thanh toan DH 1028"
},
"qr": {
"type": "dynamic",
"payload": "00020101021238540010A00000072701240006970422011001234567890208QRIBFTTA530370454061000005802VN62220818Thanh toan DH 102863042442",
"image": {
"format": "svg",
"mimeType": "image/svg+xml",
"dataUrl": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iLi4u",
"size": 512
}
},
"warnings": [
{
"code": "MEMO_NORMALIZED",
"message": "The transfer memo was normalized for banking-app compatibility."
}
]
}
}Generate a bank transfer QR code
Create a Vietnamese bank-transfer QR payload and SVG without sending payment details to a QR provider.
curl --request POST \
--url https://www.ntanduy.com/api/v1/tools/bank-qr \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"bank": "MB",
"accountNumber": "0123456789",
"amount": 100000,
"memo": "Thanh toán DH 1028",
"imageSize": 512
}
'import requests
url = "https://www.ntanduy.com/api/v1/tools/bank-qr"
payload = {
"bank": "MB",
"accountNumber": "0123456789",
"amount": 100000,
"memo": "Thanh toán DH 1028",
"imageSize": 512
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
bank: 'MB',
accountNumber: '0123456789',
amount: 100000,
memo: 'Thanh toán DH 1028',
imageSize: 512
})
};
fetch('https://www.ntanduy.com/api/v1/tools/bank-qr', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://www.ntanduy.com/api/v1/tools/bank-qr",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'bank' => 'MB',
'accountNumber' => '0123456789',
'amount' => 100000,
'memo' => 'Thanh toán DH 1028',
'imageSize' => 512
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://www.ntanduy.com/api/v1/tools/bank-qr"
payload := strings.NewReader("{\n \"bank\": \"MB\",\n \"accountNumber\": \"0123456789\",\n \"amount\": 100000,\n \"memo\": \"Thanh toán DH 1028\",\n \"imageSize\": 512\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://www.ntanduy.com/api/v1/tools/bank-qr")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"bank\": \"MB\",\n \"accountNumber\": \"0123456789\",\n \"amount\": 100000,\n \"memo\": \"Thanh toán DH 1028\",\n \"imageSize\": 512\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.ntanduy.com/api/v1/tools/bank-qr")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"bank\": \"MB\",\n \"accountNumber\": \"0123456789\",\n \"amount\": 100000,\n \"memo\": \"Thanh toán DH 1028\",\n \"imageSize\": 512\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"data": {
"bank": {
"bin": "970422",
"code": "MB",
"shortName": "MBBank",
"name": "Ngân hàng TMCP Quân đội",
"transferSupported": true
},
"transfer": {
"accountNumber": "0123456789",
"amount": 100000,
"memo": "Thanh toan DH 1028"
},
"qr": {
"type": "dynamic",
"payload": "00020101021238540010A00000072701240006970422011001234567890208QRIBFTTA530370454061000005802VN62220818Thanh toan DH 102863042442",
"image": {
"format": "svg",
"mimeType": "image/svg+xml",
"dataUrl": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iLi4u",
"size": 512
}
},
"warnings": [
{
"code": "MEMO_NORMALIZED",
"message": "The transfer memo was normalized for banking-app compatibility."
}
]
}
}bank-qr:generate scope.
This endpoint creates the EMV payload and SVG locally on Pirehub infrastructure;
it does not forward the
bank, account number, amount, or memo to an external QR image service.
See the Bank transfer QR guide for the complete integration
flow, input constraints, QR types, warning handling, and security limitations.
Use GET /api/v1/tools/bank-qr/banks to obtain accepted BINs, bank codes, short
names, transfer-support metadata, and allowlisted public logo URLs. The server
refreshes this metadata from an upstream bank-directory service and uses a versioned local snapshot if that
directory is temporarily unavailable. Payment details are never included in this
metadata request. Keep accountNumber as a JSON string so leading zeroes are preserved.
Review the bank directory endpoint for its
complete response schema and cache behavior.
curl --request POST 'https://www.ntanduy.com/api/v1/tools/bank-qr' \
--header 'Content-Type: application/json' \
--header "x-pirehub-api-key: $PIREHUB_API_KEY" \
--data '{
"bank": "MB",
"accountNumber": "0123456789",
"amount": 100000,
"memo": "Thanh toán DH 1028",
"imageSize": 512
}'
data.qr.payload: the complete bank-transfer QR payload;data.qr.image.dataUrl: a self-contained SVG data URI suitable for an<img>;data.transfer: values after normalization;data.warnings: compatibility notices that should be shown to operators.
Server integration
Keep API credentials on your server. Administrator-issued API keys use per-key minute and monthly request limits, reported throughX-ApiKey-*. Honor Retry-After after a 429.
const response = await fetch('https://www.ntanduy.com/api/v1/tools/bank-qr', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-pirehub-api-key': process.env.PIREHUB_API_KEY,
},
body: JSON.stringify({
bank: '970422',
accountNumber: '0123456789',
amount: 100000,
memo: 'DH 1028',
}),
})
const body = await response.json()
if (!response.ok) {
console.error(body.error?.code, body.data?.requestId)
throw new Error(body.error?.message ?? 'Bank QR generation failed.')
}
console.log(body.data.qr.image.dataUrl)
MEMO_NORMALIZED warning. A LIMITED_TRANSFER_SUPPORT warning means the
directory has limited compatibility metadata for that institution; test the QR
before publishing it.
Subscription-backed API keys
Paid self-service keys use shared owner credits and the plan’s per-second and concurrent-job limits. Successful results, including cache hits, consume the configured action cost; errors release the reservation. Video jobs settle at completion. InspectX-ApiKey-Credit-* and X-ApiKey-RateLimit-*.
An optional Idempotency-Key prevents repeated synchronous execution/debits;
repeated operations return 409 rather than replaying a response. Job starts use
requestId instead. See API plans and idempotency
for costs, billing periods, headers, error codes and retry rules.Authorizations
OAuth 2.0 Authorization Code with PKCE, issued by Pirehub's Clerk authorization server. Request only the scopes required by the operation.
Headers
Self-service keys only. Owner-scoped operation identifier; reuse after admission returns 409 without another debit or execution. USAGE_CONCURRENCY_EXCEEDED retains no new reservation: wait and retry with the same identifier and payload. No response replay. Video jobs use requestId instead.
1 - 80^[a-zA-Z0-9_-]+$Body
A six-digit BIN, bank code, or short name returned by the bank-directory endpoint.
2 - 64Beneficiary account number. Keep this as a JSON string so leading zeroes are preserved.
^[0-9]{6,19}$Optional amount in VND. Omit to create an open-amount QR.
1 <= x <= 9999999999999Optional transfer memo. It is folded to ASCII, sanitized, and truncated to 25 characters for compatibility.
200SVG width and height in pixels.
160 <= x <= 1024