TsCash là cổng thanh toán trung gian VietQR cho thị trường Việt Nam. Tài liệu này hướng dẫn merchant kỹ thuật tích hợp API tạo đơn nạp tiền (payin) và xử lý callback.
TsCash 是面向越南市场的 VietQR 中间支付平台。本文档面向商户技术对接人,说明如何调用代收(payin)下单接口与处理回调。
Mỗi merchant sau khi đăng ký sẽ nhận được:
| Trường | Mô tả | Ví dụ |
|---|---|---|
merchant_id | ID merchant trên hệ thống — dùng làm header X-Merchant-Id | 5 |
short_code | Mã ngắn 2 ký tự (A0-ZZ) — xuất hiện trong nội dung chuyển khoản (TS+short_code+4 ký tự) | B1 |
app_secret | Khóa bí mật HMAC-SHA256 — 64 ký tự hex, không bao giờ gửi lên header/URL | 8b40…5131 |
product_id | Sản phẩm thanh toán được gán cho merchant (theo currency) | 7 (VCB VND) |
callback_url | URL của merchant nhận thông báo trạng thái (phải HTTPS, public, có thể bị retry) | https://merchant.com/api/cb |
每个商户开通后会收到:
| 字段 | 说明 | 示例 |
|---|---|---|
merchant_id | 平台商户 ID — 用于 X-Merchant-Id 请求头 | 5 |
short_code | 2 字符商户简码(A0-ZZ)— 出现在转账备注 TS+short_code+4 字符里 | B1 |
app_secret | HMAC-SHA256 密钥 — 64 字符 hex,绝不放进 URL/Header | 8b40…5131 |
product_id | 分配给商户的产品 ID(按币种) | 7(VCB VND) |
callback_url | 商户接收回调的 URL(必须 HTTPS、公网可达、需做幂等处理) | https://merchant.com/api/cb |
Mọi request gọi cổng thanh toán phải mang 4 header sau:
所有调用网关的请求必须带这 4 个请求头:
| Header | Description / 说明 |
|---|---|
X-Merchant-Id | merchant_id (ví dụ 5) |
X-Timestamp | Unix millis (ví dụ 1716415200000) — lệch ≤ 5 phút |
X-Nonce | Chuỗi ngẫu nhiên 16–64 ký tự, không lặp lại trong 10 phút |
X-Signature | base64(HMAC-SHA256(app_secret, payload)) |
Payload để ký (kết nối bằng \n):
用于签名的 payload(用 \n 连接):
payload = METHOD + "\n" +
PATH + "\n" +
TIMESTAMP + "\n" +
NONCE + "\n" +
BODY
BODY phải là chuỗi raw bytes y hệt như sẽ gửi qua mạng. Không được chèn space/newline. Nếu là JSON, dùng đúng chuỗi serialized.BODY 必须是原样发送到网络的原始字节,不能插入空白或换行。JSON 必须用与发送一致的序列化结果。import time, secrets, hmac, hashlib, base64, json, requests
APP_SECRET = "8b4068e85b582fb3695c795dc37fad3d2b2c8dfc240d7a0fc7a028913d835131"
MERCHANT_ID = "5"
def sign_and_post(method, path, body):
ts = str(int(time.time() * 1000))
nonce = secrets.token_hex(12) # 24 chars
raw = json.dumps(body, separators=(',', ':'), ensure_ascii=False).encode()
payload = "\n".join([method, path, ts, nonce]).encode() + b"\n" + raw
sig = base64.b64encode(hmac.new(APP_SECRET.encode(), payload, hashlib.sha256).digest()).rstrip(b"=").decode()
headers = {
"Content-Type": "application/json",
"X-Merchant-Id": MERCHANT_ID,
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Signature": sig,
}
return requests.post("https://api.tscash.pro" + path, headers=headers, data=raw, timeout=15)
const crypto = require('crypto');
const fetch = require('node-fetch');
const APP_SECRET = '8b4068e85b582fb3695c795dc37fad3d2b2c8dfc240d7a0fc7a028913d835131';
const MERCHANT_ID = '5';
async function signAndPost(method, path, body) {
const ts = Date.now().toString();
const nonce = crypto.randomBytes(12).toString('hex'); // 24 chars
const raw = JSON.stringify(body);
const payload = [method, path, ts, nonce, raw].join('\n');
const sig = crypto.createHmac('sha256', APP_SECRET)
.update(payload).digest('base64').replace(/=+$/, '');
return fetch('https://api.tscash.pro' + path, {
method,
headers: {
'Content-Type': 'application/json',
'X-Merchant-Id': MERCHANT_ID,
'X-Timestamp': ts,
'X-Nonce': nonce,
'X-Signature': sig,
},
body: raw,
});
}
POST https://api.tscash.pro/gateway/v1/payin/orders
| Field | Type | Required | Description / 说明 |
|---|---|---|---|
merchantOrderId | string ≤64 | ✓ | Mã đơn merchant (duy nhất)商户本侧唯一订单号 |
productId | uint64 | ✓ | product_id được gán分配的产品 ID |
amount | uint64 | ✓ | Số tiền (đơn vị nhỏ nhất; VND không có thập phân)金额(最小单位;VND 无小数) |
outMemId | string ≤64 | — | ID người dùng phía merchant (dùng cho thống kê)商户侧玩家 ID(用于统计) |
subject | string ≤255 | — | Tiêu đề đơn (hiển thị nội bộ)订单标题(内部展示) |
clientIp | string ≤64 | — | IP của người dùng cuối终端用户 IP |
callbackUrl | string ≤500 | — | URL nhận callback (overrides default)回调地址(覆盖默认) |
redirectUrl | string ≤500 | — | URL chuyển hướng sau khi thanh toán付款完成后跳转地址 |
timeoutMinutes | uint32 | — | Mặc định 10 (1-60)默认 10 分钟 (1-60) |
POST /gateway/v1/payin/orders
Content-Type: application/json
X-Merchant-Id: 5
X-Timestamp: 1716415200000
X-Nonce: a1b2c3d4e5f60718293a4b5c
X-Signature: p9XzAbCdEfGhIjK…
{
"merchantOrderId": "ORDER-20260523-001",
"productId": 7,
"amount": 500000,
"outMemId": "user-9527",
"subject": "Recharge 500K VND",
"callbackUrl": "https://merchant.com/api/cb",
"redirectUrl": "https://merchant.com/pay-done"
}
{
"code": 200,
"msg": "OK",
"data": {
"orderCode": "TDS2026052305594300000100000293",
"merchantOrderId": "ORDER-20260523-001",
"status": "created",
"amount": 500000,
"currency": "VND",
"payUrl": "https://h5.tscash.pro/#/pay/TDS2026052305594300000100000293"
}
}
payUrl để hiển thị màn hình thanh toán VietQR. Sau khi đơn được phân bổ tài khoản, người dùng quét QR và chuyển khoản với nội dung đã ghi sẵn.payUrl 显示 VietQR 收银台。系统分配账户后,用户扫码并按提示的备注转账。GET https://api.tscash.pro/gateway/v1/payin/orders/{orderCode}
BODY rỗng ("") khi ký. Sau khi nhận callback merchant không cần poll thêm, endpoint này chỉ dùng cho debug / đối soát.
签名时 BODY 为空("")。回调到达后商户无需再轮询;此接口主要用于排查/对账。
GET /gateway/v1/payin/orders/TDS2026052305594300000100000293
X-Merchant-Id: 5
X-Timestamp: 1716415300000
X-Nonce: 7a8b9c0d1e2f3041526378…
X-Signature: …
# Response:
{
"code": 200,
"data": {
"orderCode": "TDS2026052305594300000100000293",
"merchantOrderId": "ORDER-20260523-001",
"status": "completed",
"amount": 500000,
"realAmount": 500000,
"paidAmount": 500000,
"currency": "VND",
"completedAt": "2026-05-23T05:48:12Z"
}
}
Khi đơn chuyển sang trạng thái terminal (completed / timeout / rejected), platform sẽ POST tới callbackUrl của merchant. Header signing y hệt như request gateway, nhưng secret là app_secret merchant đã cấu hình.
订单进入终态(completed / timeout / rejected)时,平台会 POST 到商户的 callbackUrl。签名格式同 gateway 请求,但 secret 是商户自己配置的 app_secret。
| Header | Mô tả / 说明 |
|---|---|
X-Merchant-Id | 5 |
X-Timestamp | Unix millis |
X-Nonce | 16-64 chars |
X-Signature | base64(HMAC-SHA256(app_secret, METHOD+\n+PATH+\n+TS+\n+NONCE+\n+BODY)) |
{
"orderCode": "TDS2026052305594300000100000293",
"merchantOrderId": "ORDER-20260523-001",
"status": "completed",
"amount": 500000,
"paidAmount": 500000,
"currency": "VND",
"timestamp": 1716415692000,
"completedAt": "2026-05-23T05:48:12.345Z"
}
X-Signature bằng app_secret riêng của bạnorderCode + merchantOrderId match đơn trong DB của bạnorderCode có thể nhận nhiều lần — chỉ cập nhật DB nếu status thay đổi200 và body chứa "ok". Bất kỳ phản hồi khác sẽ khiến platform retry (≤ 8 lần với backoff exponential)X-SignatureorderCode + merchantOrderId 与你 DB 里的订单匹配orderCode 可能多次到达 — 仅在 status 变化时更新 DB200 且 body 含 "ok"。其他响应会触发平台重试(≤ 8 次,指数退避)import hmac, hashlib, base64
from flask import request, abort
APP_SECRET = "8b4068e85b582fb3695c795dc37fad3d2b2c8dfc240d7a0fc7a028913d835131"
@app.route("/api/cb", methods=["POST"])
def cb():
ts = request.headers.get("X-Timestamp", "")
nonce = request.headers.get("X-Nonce", "")
sig = request.headers.get("X-Signature", "")
body = request.get_data() # raw bytes
payload = b"POST\n/api/cb\n" + ts.encode() + b"\n" + nonce.encode() + b"\n" + body
expect = base64.b64encode(hmac.new(APP_SECRET.encode(), payload, hashlib.sha256).digest()).rstrip(b"=").decode()
if not hmac.compare_digest(sig, expect):
abort(401)
data = request.get_json()
# idempotent update by orderCode + status
...
return "ok", 200
| Status | Mô tả / 说明 |
|---|---|
created | Đơn mới tạo, chờ phân bổ tài khoản订单新建,等待分配收款账户 |
allocated | Đã phân bổ, đang chờ thanh toán已分配账户,等待付款 |
paid | Đã ghi nhận thanh toán, đang chờ xác nhận收到付款,正在确认 |
completed | Hoàn tất ✅ (terminal)完成 ✅(终态) |
timeout | Hết hạn ⏱ (terminal)超时 ⏱(终态) |
rejected | Bị từ chối ❌ (terminal)被驳回 ❌(终态) |
| code | msg | Hint / 说明 |
|---|---|---|
| 1000 | OK | — |
| 1001 | Unauthorized | Sai chữ ký / hết hạn timestamp / nonce trùng / IP không trong white-list签名错 / 时间戳过期 / nonce 重放 / IP 不在白名单 |
| 1002 | Forbidden | Merchant bị disable / không sở hữu order商户被禁用 / 无权访问订单 |
| 1004 | Bad Request | Field thiếu/sai. Xem msg字段缺失或非法,详见 msg |
| 1005 | Amount exceeds product max | amount > product.max_amount |
| 1006 | Merchant business paused | Merchant đang bị tạm dừng (tier hoặc admin)商户业务暂停(等级或 admin 操作) |
| 1007 | Duplicate merchantOrderId | Cùng merchantOrderId đã có đơn同一 merchantOrderId 已存在 |