Tài liệu Tích hợp Cổng Thanh toán

商户接入文档

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)下单接口与处理回调。

▶ Xem trang demo / 查看 Demo →

Mục lục / 目录

1. Thông tin tài khoản / 商户凭据 2. Ký HMAC / HMAC 签名 3. Tạo đơn / 创建订单 4. Truy vấn đơn / 查询订单 5. Callback / 异步回调 6. Trạng thái / 状态 7. Mã lỗi / 错误码

1. Thông tin tài khoản商户凭据

Mỗi merchant sau khi đăng ký sẽ nhận được:

TrườngMô tảVí dụ
merchant_idID merchant trên hệ thống — dùng làm header X-Merchant-Id5
short_codeMã 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_secretKhóa bí mật HMAC-SHA256 — 64 ký tự hex, không bao giờ gửi lên header/URL8b40…5131
product_idSản phẩm thanh toán được gán cho merchant (theo currency)7 (VCB VND)
callback_urlURL 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_code2 字符商户简码(A0-ZZ)— 出现在转账备注 TS+short_code+4 字符里B1
app_secretHMAC-SHA256 密钥 — 64 字符 hex,绝不放进 URL/Header8b40…5131
product_id分配给商户的产品 ID(按币种)7(VCB VND)
callback_url商户接收回调的 URL(必须 HTTPS、公网可达、需做幂等处理)https://merchant.com/api/cb

2. Ký HMAC-SHA256HMAC-SHA256 签名

Mọi request gọi cổng thanh toán phải mang 4 header sau:

所有调用网关的请求必须带这 4 个请求头:

HeaderDescription / 说明
X-Merchant-Idmerchant_id (ví dụ 5)
X-TimestampUnix millis (ví dụ 1716415200000) — lệch ≤ 5 phút
X-NonceChuỗi ngẫu nhiên 16–64 ký tự, không lặp lại trong 10 phút
X-Signaturebase64(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 必须用与发送一致的序列化结果。

Ví dụ ký (Python)签名示例(Python)

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)

Ví dụ ký (Node.js)签名示例(Node.js)

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,
  });
}

3. Tạo đơn nạp / Create Order创建代收订单

POST https://api.tscash.pro/gateway/v1/payin/orders

Tham số request请求参数

FieldTypeRequiredDescription / 说明
merchantOrderIdstring ≤64Mã đơn merchant (duy nhất)商户本侧唯一订单号
productIduint64product_id được gán分配的产品 ID
amountuint64Số tiền (đơn vị nhỏ nhất; VND không có thập phân)金额(最小单位;VND 无小数)
outMemIdstring ≤64ID người dùng phía merchant (dùng cho thống kê)商户侧玩家 ID(用于统计)
subjectstring ≤255Tiêu đề đơn (hiển thị nội bộ)订单标题(内部展示)
clientIpstring ≤64IP của người dùng cuối终端用户 IP
callbackUrlstring ≤500URL nhận callback (overrides default)回调地址(覆盖默认)
redirectUrlstring ≤500URL chuyển hướng sau khi thanh toán付款完成后跳转地址
timeoutMinutesuint32Mặc định 10 (1-60)默认 10 分钟 (1-60)

Ví dụ示例

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"
}

Response thành công成功响应

{
  "code": 200,
  "msg":  "OK",
  "data": {
    "orderCode":       "TDS2026052305594300000100000293",
    "merchantOrderId": "ORDER-20260523-001",
    "status":          "created",
    "amount":          500000,
    "currency":        "VND",
    "payUrl":          "https://h5.tscash.pro/#/pay/TDS2026052305594300000100000293"
  }
}
📱 Chuyển hướng người dùng tới 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 收银台。系统分配账户后,用户扫码并按提示的备注转账。

4. Truy vấn đơn查询订单

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"
  }
}

5. Callback (async)异步回调

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 callback回调 Header

HeaderMô tả / 说明
X-Merchant-Id5
X-TimestampUnix millis
X-Nonce16-64 chars
X-Signaturebase64(HMAC-SHA256(app_secret, METHOD+\n+PATH+\n+TS+\n+NONCE+\n+BODY))

Body回调 Body

{
  "orderCode":       "TDS2026052305594300000100000293",
  "merchantOrderId": "ORDER-20260523-001",
  "status":          "completed",
  "amount":          500000,
  "paidAmount":      500000,
  "currency":        "VND",
  "timestamp":       1716415692000,
  "completedAt":     "2026-05-23T05:48:12.345Z"
}
Yêu cầu xử lý:
  1. Verify X-Signature bằng app_secret riêng của bạn
  2. Kiểm tra orderCode + merchantOrderId match đơn trong DB của bạn
  3. Idempotency: cùng orderCode có thể nhận nhiều lần — chỉ cập nhật DB nếu status thay đổi
  4. Trả về HTTP 200 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)
处理要求:
  1. 用你自己的 app_secret 验证 X-Signature
  2. 校验 orderCode + merchantOrderId 与你 DB 里的订单匹配
  3. 幂等:同一个 orderCode 可能多次到达 — 仅在 status 变化时更新 DB
  4. 必须返回 HTTP 200 且 body 含 "ok"。其他响应会触发平台重试(≤ 8 次,指数退避)

Ví dụ verify callback (Python)验回调示例(Python)

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

6. Trạng thái đơn订单状态

StatusMô 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收到付款,正在确认
completedHoàn tất ✅ (terminal)完成 ✅(终态)
timeoutHết hạn ⏱ (terminal)超时 ⏱(终态)
rejectedBị từ chối ❌ (terminal)被驳回 ❌(终态)

7. Mã lỗi thường gặp常见错误码

codemsgHint / 说明
1000OK
1001UnauthorizedSai chữ ký / hết hạn timestamp / nonce trùng / IP không trong white-list签名错 / 时间戳过期 / nonce 重放 / IP 不在白名单
1002ForbiddenMerchant bị disable / không sở hữu order商户被禁用 / 无权访问订单
1004Bad RequestField thiếu/sai. Xem msg字段缺失或非法,详见 msg
1005Amount exceeds product maxamount > product.max_amount
1006Merchant business pausedMerchant đang bị tạm dừng (tier hoặc admin)商户业务暂停(等级或 admin 操作)
1007Duplicate merchantOrderIdCùng merchantOrderId đã có đơn同一 merchantOrderId 已存在