BP
BankGate

Tài liệu Partner OpenAPI

Base: /api-proxy · Mã hoá AES-256-GCM · Chữ ký HMAC-SHA256 · Nguồn: partner_auth.py / partner_crypto.py

Chưa có Credential trong session

Vào Credential API lưu 4 trường, rồi bấm «Gen code từ Credential» để mẫu chạy được ngay.
Mở Credential API

Luồng từng bước

1
Nhận credential từ BankGate

Admin CMS sinh credential (partner-admin/generate-credential). Bạn nhận 4 giá trị: client_id, client_secret (chỉ hiện 1 lần), signing_secret, encryption_key (64 hex = 32 byte AES key). Lưu an toàn trên server của bạn.

2
Lấy access_token

POST /partner/oauth/token với body JSON plaintext { client_id, client_secret }. Không mã hoá, không ký. Response: { access_token, token_type, expires_in } (thường 3600s). Gắn Bearer vào các request tiếp theo.

3
Chuẩn bị plaintext nghiệp vụ

JSON UTF-8 đúng schema endpoint (qr / transactions / me). Ví dụ QR: customer_code ≤ 13 ký tự, customer_name, amount?, description?.

4
Mã hoá AES-256-GCM

Key = bytes.fromhex(encryption_key). IV/nonce ngẫu nhiên 12 byte. Encrypt plaintext → ciphertext (kèm auth tag GCM). Body HTTP thật sự gửi đi là envelope JSON: { "iv": "<base64>", "ciphertext": "<base64>" }. Không gửi plaintext.

5
Ký HMAC-SHA256

Chuỗi ký (UTF-8): timestamp + "|" + METHOD + "|" + path + "|" + raw_body. Trong đó path = request path server nhận (vd /partner/qr/generate), raw_body = đúng chuỗi JSON envelope đã gửi (không pretty-print lệch). signature = HMAC_SHA256(key=signing_secret, msg=chuỗi) → hex lowercase.

6
Gắn header và gửi

Authorization: Bearer <access_token> · X-Timestamp: unix giây (lệch server ≤ 300s) · X-Request-Id: UUID tự sinh · X-Signature: hex HMAC · Content-Type: application/json. Body = raw_body envelope.

7
Nhận và giải mã response

Thành công: body cũng là { iv, ciphertext }. Giải mã bằng cùng encryption_key → JSON nghiệp vụ. Lỗi auth/decrypt thường trả JSON plaintext { error: "..." }. Có thể có header X-Signature trên response để đối tác verify (tuỳ phiên bản).

Công thức mã hoá & chữ ký

AES-256-GCM
  key        = unhex(encryption_key)          # 32 bytes
  iv         = random(12 bytes)
  ciphertext = AESGCM.encrypt(key, iv, plaintext_utf8)
  body       = JSON({ "iv": b64(iv), "ciphertext": b64(ciphertext) })

HMAC-SHA256 (hex lowercase)
  message   = "{timestamp}|{METHOD}|{path}|{raw_body}"
  # ví dụ: 1710000000|POST|/partner/qr/generate|{"iv":"...","ciphertext":"..."}
  signature = HMAC_SHA256(key=signing_secret_utf8, msg=message_utf8).hex()

Headers bắt buộc (endpoint có auth)
  Authorization: Bearer {access_token}
  X-Timestamp:   {unix_seconds}          # lệch ≤ 300s
  X-Request-Id:  {uuid}
  X-Signature:   {signature_hex}
  Content-Type:  application/json

Endpoints

POST/partner/oauth/tokenPlaintextOAuth token
Request (plaintext trước mã hoá)
{ "client_id": "...", "client_secret": "..." }
Response (sau giải mã)
{ "access_token": "...", "token_type": "Bearer", "expires_in": 3600 }
POST/partner/qr/generateMã hoá + ký + BearerTạo VietQR
Request (plaintext trước mã hoá)
{ "customer_code": "00000001", "customer_name": "NGUYEN VAN A", "amount": "100000", "description": "..." }
Response (sau giải mã)
{ "code", "va_code", "vietQR", "vietQRImage" }
POST/partner/transactionsMã hoá + ký + BearerLịch sử GD đã thu
Request (plaintext trước mã hoá)
{ "payout_status": "pending" }  // optional: pending | settled
Response (sau giải mã)
{ "total_rows", "rows": [ { "code", "amount", "fee_amount", "net_amount", "payout_status", ... } ] }
POST/partner/meMã hoá + ký + BearerThông tin đối tác
Request (plaintext trước mã hoá)
{ }
Response (sau giải mã)
{ "code", "name", "pending_payout", "settled_payout" }

Code mẫu (Python / JavaScript)

|
# -*- coding: utf-8 -*-
"""
Partner OpenAPI — mã hoá AES-256-GCM + ký HMAC-SHA256 + gọi API.
Endpoint: POST /partner/qr/generate
"""
import base64
import hashlib
import hmac
import json
import secrets
import time
import uuid

import requests
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

BASE = "/api-proxy"
PATH = "/partner/qr/generate"  # path dùng trong chữ ký — KHÔNG gồm domain
CLIENT_ID = "pk_xxxxxxxx"
CLIENT_SECRET = "YOUR_CLIENT_SECRET"
SIGNING_SECRET = "YOUR_SIGNING_SECRET_HEX"
# encryption_key: 64 ký tự hex = 32 bytes
ENCRYPTION_KEY_HEX = "YOUR_ENCRYPTION_KEY_64_HEX_CHARS"


def encrypt_payload(plaintext: str, key_hex: str) -> dict:
    """AES-256-GCM. Trả envelope { iv, ciphertext } (base64)."""
    key = bytes.fromhex(key_hex)
    iv = secrets.token_bytes(12)  # 96-bit nonce
    aesgcm = AESGCM(key)
    # AESGCM.encrypt trả ciphertext || tag (16 bytes) — server decrypt cùng format
    ct = aesgcm.encrypt(iv, plaintext.encode("utf-8"), None)
    return {
        "iv": base64.b64encode(iv).decode("ascii"),
        "ciphertext": base64.b64encode(ct).decode("ascii"),
    }


def decrypt_payload(envelope: dict, key_hex: str) -> str:
    key = bytes.fromhex(key_hex)
    iv = base64.b64decode(envelope["iv"])
    ct = base64.b64decode(envelope["ciphertext"])
    aesgcm = AESGCM(key)
    pt = aesgcm.decrypt(iv, ct, None)
    return pt.decode("utf-8")


def sign_message(signing_secret: str, message: str) -> str:
    """HMAC-SHA256 → hex lowercase (giống server verify_signature)."""
    return hmac.new(
        signing_secret.encode("utf-8"),
        message.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()


# --- 1) Token ---
tok = requests.post(
    f"{BASE}/partner/oauth/token",
    json={"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET},
    timeout=30,
)
tok.raise_for_status()
access_token = tok.json()["access_token"]

# --- 2) Plaintext nghiệp vụ ---
plain_obj = {
  "customer_code": "00000001",
  "customer_name": "NGUYEN VAN A",
  "amount": "100000",
  "description": "Thanh toan HD"
}
plaintext = json.dumps(plain_obj, ensure_ascii=False, separators=(",", ":"))

# --- 3) Mã hoá body ---
envelope = encrypt_payload(plaintext, ENCRYPTION_KEY_HEX)
raw_body = json.dumps(envelope, separators=(",", ":"))  # đúng bytes sẽ gửi

# --- 4) Ký ---
timestamp = str(int(time.time()))  # unix seconds, lệch server ≤ 300s
request_id = str(uuid.uuid4())
# message = timestamp|METHOD|path|raw_body
message = f"{timestamp}|POST|{PATH}|{raw_body}"
signature = sign_message(SIGNING_SECRET, message)

headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Authorization": f"Bearer {access_token}",
    "X-Timestamp": timestamp,
    "X-Request-Id": request_id,
    "X-Signature": signature,
}

print("=== Request headers ===")
for k, v in headers.items():
    print(f"  {k}: {v if k != 'Authorization' else 'Bearer ***'}")

print("=== Request body (envelope) ===")
print(raw_body[:200] + ("..." if len(raw_body) > 200 else ""))

# --- 5) Gửi ---
resp = requests.post(f"{BASE}{PATH}", data=raw_body, headers=headers, timeout=30)
print("HTTP", resp.status_code)

# --- 6) Giải mã response (cũng là envelope iv+ciphertext) ---
try:
    env_out = resp.json()
except Exception:
    print(resp.text)
    raise

if "iv" in env_out and "ciphertext" in env_out:
    plain_out = decrypt_payload(env_out, ENCRYPTION_KEY_HEX)
    print("=== Response (decrypted) ===")
    print(json.dumps(json.loads(plain_out), ensure_ascii=False, indent=2))
else:
    # Một số lỗi 4xx trả plaintext JSON
    print("=== Response (raw) ===")
    print(json.dumps(env_out, ensure_ascii=False, indent=2))

Lưu ý tích hợp

Path trong chữ ký phải khớp path server (thường /partner/...). Proxy gateway không được đổi body sau khi ký. encryption_key là hex 64 ký tự. Production: giữ secret phía server, không embed trên frontend public.