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
Luồng từng bước
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.
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.
JSON UTF-8 đúng schema endpoint (qr / transactions / me). Ví dụ QR: customer_code ≤ 13 ký tự, customer_name, amount?, description?.
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.
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.
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.
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/jsonEndpoints
/partner/oauth/tokenPlaintextOAuth token{ "client_id": "...", "client_secret": "..." }{ "access_token": "...", "token_type": "Bearer", "expires_in": 3600 }/partner/qr/generateMã hoá + ký + BearerTạo VietQR{ "customer_code": "00000001", "customer_name": "NGUYEN VAN A", "amount": "100000", "description": "..." }{ "code", "va_code", "vietQR", "vietQRImage" }/partner/transactionsMã hoá + ký + BearerLịch sử GD đã thu{ "payout_status": "pending" } // optional: pending | settled{ "total_rows", "rows": [ { "code", "amount", "fee_amount", "net_amount", "payout_status", ... } ] }/partner/meMã hoá + ký + BearerThông tin đối tác{ }{ "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