# TConnect Payment Open API — Complete Reference Integration guide for the TConnect Payment Open API system. This document consolidates all API endpoints, authentication, encryption, and callback specifications into a single reference. **Staging base URL:** `https://sme-open-api-sandbox.tconnect.vn` **Production base URL:** Provided after testing is complete --- ## Sandbox Test Credentials (End-to-End Demo) > These are **ready-to-use sandbox credentials** so a developer — or an AI agent — > can run the full flow end-to-end (**Login → Get Services → Create QR → Check > Order Status**) without any additional setup. Sandbox only; do not use in production. | Item | Value | |------|-------| | Base URL | `https://sme-open-api-sandbox.tconnect.vn` | | Partner-Code (Merchant CIF) | `KH_0000024` | | OAuth Username | `phamgiahuy@grr.la` | | OAuth Password | `N343#2^zPDLOx4{v` | | Client ID | `sh6dhA4hTu-rE0AFhgtHtg` | | Client Secret Key | `GsUppywKmcPyJUk6lwajsQ6CHtxbEXJ8` | | AES Secret Key (hex, 32 bytes) | `5fed5c2bd6d5ea44d33d71b3bdd02193bb28ea3ad026f85a07c82d70536d1f91` | **Values discovered from the sandbox (needed for Create QR / Check Status):** | Item | Value | How to obtain | |------|-------|---------------| | `x-service-code` | `vccb-qr` | From **Get Services** (`payment_method=qr`) — BVBank QR service | | `bincode` | `970454` | Provider BIN of BVBank (VCCB), from the service/provider | | `va` / `acc_no` (Merchant virtual account) | `99MPI0000330000007` | Merchant's receiving VA. Use as `va` in **Create QR** and as `acc_no` in **Check Order Status**. A new VA can be created via [Create VA](#4-create-va) if your account is provisioned with an origin bank account. | **End-to-end flow (minimum path to a paid order):** 1. **Login** with the credentials above → get `access_token`. 2. **Get Services** (`payment_method=qr`) → confirm `x-service-code = vccb-qr`. 3. **Create QR** with `va = 99MPI0000330000007`, `bincode = 970454`, your own `order_id`, and `amount > 0` (dynamic QR) → returns `image_png_base64` + `qr_content`. 4. Show the QR, customer pays via any bank app. 5. **Check Order Status** (poll) with the same `order_id` + `acc_no = 99MPI0000330000007` → `transactions[].status` becomes `SUCCESS` when payment is received. > ✅ **Recommended:** poll the **[Check QR Transaction by Order ID](#check-qr-transaction-by-order-id)** > API to confirm payment, rather than waiting for IPN. See > [Recommended: Confirming Payment](#recommended-confirming-payment-status) below. --- ## Table of Contents 1. [Sandbox Test Credentials](#sandbox-test-credentials-end-to-end-demo) 2. [Authentication](#authentication) - [Login](#1-login) - [Refresh Token](#2-refresh-token) 3. [Encryption](#encryption) - [AES-256-CBC Overview](#aes-256-cbc-overview) - [Python Helper](#python-helper) 4. [Services](#services) - [Get List Services](#3-get-list-services) 5. [Virtual Account](#virtual-account) - [Create VA](#4-create-va) 6. [Create Transactions](#create-transactions) - [Create QR](#5-create-qr) - [Push Payment to Devices](#6-push-payment-to-devices) 7. [Query Transactions](#query-transactions) - [Check QR Transaction by Order ID](#check-qr-transaction-by-order-id) - [Recommended: Confirming Payment Status](#recommended-confirming-payment-status) - [Get QR Transactions](#7-get-qr-transactions) - [Get Card Transactions](#8-get-card-transactions) - [Get Cash Transactions](#9-get-cash-transactions) 8. [IPN Callback](#ipn-callback) --- ## Authentication All APIs use JWT Bearer tokens. Obtain an `access_token` by calling **Login**, then include it as `Authorization: Bearer ` in every subsequent request. Two credential headers are required on every request: | Header | Description | |--------|-------------| | `Partner-Code` | Merchant identifier code — provided by TCONNECT | | `Authorization` | `Bearer ` — omit on Login / Refresh Token | --- ### 1. Login **POST** `/openapi/v1/auth/login` Authenticate the partner system and receive a JWT pair (Access Token + Refresh Token). #### Pre-encryption payload ```json { "username": "phamgiahuy@grr.la", "password": "N343#2^zPDLOx4{v", "client_id": "sh6dhA4hTu-rE0AFhgtHtg", "client_secret": "GsUppywKmcPyJUk6lwajsQ6CHtxbEXJ8" } ``` > The example above uses the shared **sandbox** credentials > ([see Sandbox Test Credentials](#sandbox-test-credentials-end-to-end-demo)). > Encrypt this JSON with the sandbox **AES Secret Key** and send it as the `data` field. | Field | Required | Description | |-------|----------|-------------| | `username` | ✅ | Login email | | `password` | ✅ | Password | | `client_id` | ✅ | Application ID (provided by TCONNECT) | | `client_secret` | ✅ | Application secret (provided by TCONNECT) | #### Request headers | Name | Required | Description | |------|----------|-------------| | `Partner-Code` | ✅ | Merchant identifier (provided by TCONNECT) | | `Content-Type` | ✅ | `application/json` | #### Request body | Field | Type | Required | Description | |-------|------|----------|-------------| | `data` | string | ✅ | AES-256-CBC encrypted payload as Hexadecimal string | #### Code examples ```bash curl --location '/openapi/v1/auth/login' \ --header 'Partner-Code: YOUR_PARTNER_CODE' \ --header 'Content-Type: application/json' \ --data '{"data": "ENCRYPTED_PAYLOAD"}' ``` ```python import requests url = "/openapi/v1/auth/login" payload = {"data": "ENCRYPTED_PAYLOAD"} headers = { "Partner-Code": "YOUR_PARTNER_CODE", "Content-Type": "application/json", } response = requests.post(url, headers=headers, json=payload) print(response.text) ``` ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { url := "/openapi/v1/auth/login" body := strings.NewReader(`{"data":"ENCRYPTED_PAYLOAD"}`) req, _ := http.NewRequest("POST", url, body) req.Header.Set("Partner-Code", "YOUR_PARTNER_CODE") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() result, _ := io.ReadAll(resp.Body) fmt.Println(string(result)) } ``` #### Response — 200 OK | Field | Type | Description | |-------|------|-------------| | `access_token` | string | JWT for authenticating requests. Set as `Authorization: Bearer ` | | `expires_in` | integer | `access_token` lifetime in seconds | | `refresh_token` | string | Long-lived token to obtain a new `access_token` when it expires | | `refresh_expires_in` | integer | `refresh_token` lifetime in seconds | | `token_type` | string | Always `"Bearer"` | | `id_token` | string | JWT carrying the user's identity claims | | `not-before-policy` | integer | Token revocation checkpoint (`0` = not applied) | | `session_state` | string | UUID of the authentication server session | | `scope` | string | List of granted permissions | --- ### 2. Refresh Token **POST** `/openapi/v1/auth/refresh` Obtain a new Access Token using a Refresh Token when the current Access Token has expired. #### Pre-encryption payload ```json { "refresh_token": "eyJhbGciO…" } ``` | Field | Required | Description | |-------|----------|-------------| | `refresh_token` | ✅ | JWT Refresh Token received from the Login API | #### Request headers | Name | Required | Description | |------|----------|-------------| | `Partner-Code` | ✅ | Merchant identifier (provided by TCONNECT) | | `Content-Type` | ✅ | `application/json` | #### Request body | Field | Type | Required | Description | |-------|------|----------|-------------| | `data` | string | ✅ | AES-256-CBC encrypted payload as Hexadecimal string | #### Code examples ```bash curl --location '/openapi/v1/auth/refresh' \ --header 'Partner-Code: YOUR_PARTNER_CODE' \ --header 'Content-Type: application/json' \ --data '{"data": "ENCRYPTED_PAYLOAD"}' ``` ```python import requests url = "/openapi/v1/auth/refresh" payload = {"data": "ENCRYPTED_PAYLOAD"} headers = { "Partner-Code": "YOUR_PARTNER_CODE", "Content-Type": "application/json", } response = requests.post(url, headers=headers, json=payload) print(response.text) ``` #### Response — 200 OK | Field | Type | Description | |-------|------|-------------| | `access_token` | string | New JWT for authenticating requests | | `expires_in` | integer | New `access_token` lifetime in seconds | | `refresh_token` | string | New refresh token | | `refresh_expires_in` | integer | New `refresh_token` lifetime in seconds | | `token_type` | string | Always `"Bearer"` | --- ## Encryption ### AES-256-CBC Overview All request bodies — except Get Services — are **AES-256-CBC encrypted**. The `data` field carries the encrypted payload as a **Hexadecimal** string. **Encryption flow:** ``` plaintext JSON → AES-256-CBC encrypt (random IV) → prepend IV to ciphertext → hex-encode → "data" field value ``` **Decryption flow:** ``` hex-decode → extract first 16 bytes as IV → AES-256-CBC decrypt → PKCS7 unpad → plaintext JSON ``` Key length: 256 bits (32 bytes). IV length: 128 bits (16 bytes). Block size: 128 bits (16 bytes). ### Python Helper ```python import os import binascii from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives import padding from cryptography.hazmat.backends import default_backend def aes_decrypt(encrypted: str, key: bytes) -> str: ciphertext = binascii.unhexlify(encrypted) if len(ciphertext) < 16: raise ValueError("ciphertext too short") iv = ciphertext[:16] ciphertext = ciphertext[16:] if len(ciphertext) % 16 != 0: raise ValueError("ciphertext is not a multiple of block size") cipher = Cipher( algorithms.AES(key), modes.CBC(iv), backend=default_backend() ) decryptor = cipher.decryptor() padded_plain = decryptor.update(ciphertext) + decryptor.finalize() unpadder = padding.PKCS7(128).unpadder() plain_bytes = unpadder.update(padded_plain) + unpadder.finalize() return plain_bytes.decode() def aes_encrypt(plain: str, key: bytes) -> str: cipher = Cipher( algorithms.AES(key), modes.CBC(os.urandom(16)), # AES block size = 16 bytes backend=default_backend() ) padder = padding.PKCS7(128).padder() # 128 bits = 16 bytes plain_bytes = padder.update(plain.encode()) + padder.finalize() encryptor = cipher.encryptor() ciphertext = encryptor.update(plain_bytes) + encryptor.finalize() result = cipher.mode.initialization_vector + ciphertext return binascii.hexlify(result).decode() ``` #### Sample usage ```python if __name__ == "__main__": # Sandbox AES Secret Key (32 bytes, hex) — see Sandbox Test Credentials KEY = bytes.fromhex( "5fed5c2bd6d5ea44d33d71b3bdd02193bb28ea3ad026f85a07c82d70536d1f91" ) message = '{"username": "phamgiahuy@grr.la", "password": "N343#2^zPDLOx4{v"}' encrypted = aes_encrypt(message, KEY) decrypted = aes_decrypt(encrypted, KEY) print(f"Original: {message}") print(f"Encrypted: {encrypted}") print(f"Decrypted: {decrypted}") print(f"Match: {message == decrypted}") ``` --- ## Services ### 3. Get List Services **GET** `/openapi/v1/services` Query the list of licensed payment gateways and services. Returns `service_code` values needed as headers when calling other APIs (e.g., Create QR). > **No encryption required** — Unlike other APIs, this endpoint does not encrypt the payload. Parameters are passed directly as query string. #### Request headers | Name | Required | Description | |------|----------|-------------| | `Partner-Code` | ✅ | Merchant identifier (provided by TCONNECT) | | `Authorization` | ✅ | `Bearer ` | #### Query parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `service_type` | string | | Service type (`finance`, `non-finance`) | | `payment_method` | string | | Payment method (`qr`, `card`) | | `code` | string | | Specific service code to filter by | | `limit` | number | ✅ | Maximum number of records to return | | `page` | number | ✅ | Page number (starting from `1`) | #### Code examples ```bash curl --location '/openapi/v1/services?service_type=finance' \ --header 'Partner-Code: 1111111' \ --header 'Authorization: Bearer eyJhbGciOiJ...' ``` ```python import requests url = "/openapi/v1/services" params = {"service_type": "finance"} headers = { "Partner-Code": "1111111", "Authorization": "Bearer eyJhbGciOiJ...", } response = requests.get(url, headers=headers, params=params) print(response.text) ``` #### Response — 200 OK | Field | Type | Description | |-------|------|-------------| | `data` | array | List of services | | `data[].code` | string | `service_code` used as `x-service-code` header in other APIs | | `data[].name` | string | Display name of the service | | `data[].service_type` | string | Service type (`finance`, `non-finance`) | | `data[].payment_method` | string | Supported payment method (`qr`, `card`) | | `data[].allowed_va_creation` | boolean | Whether VA creation is permitted for this service | | `data[].product_template.module_type` | string | Module type (e.g., `internal`) | | `data[].product_template.service_name` | string | Module name (e.g., `vccb-hub`) | | `data[].provider.provider_code` | string | Provider code | | `data[].provider.provider_name` | string | Provider name | | `pagination.page` | integer | Current page | | `pagination.limit` | integer | Records per page | | `pagination.total_items` | integer | Total record count | | `pagination.total_pages` | integer | Total page count | #### Response example ```json { "data": [ { "id": 28, "code": "vccb-qr", "name": "Dịch vụ thanh toán QR - VCCB", "service_type": "finance", "payment_method": "qr", "allowed_va_creation": false, "provider": { "provider_code": "vccb", "provider_name": "VCCB" } } ], "pagination": { "page": 1, "limit": 10, "total_items": 1, "total_pages": 1 } } ``` --- ## Virtual Account ### 4. Create VA **POST** `/openapi/v1/va/va-account/create` Create a Virtual Account (VA) for the Merchant to receive bank transfer payments. > Call **Get Services** first to obtain `service_code` (used as the `x-service-code` header). #### Pre-encryption payload ```json { "request_id": "2e234fa21", "bank_account_no": "31053489", "internal_code": "89234578" } ``` | Field | Required | Description | |-------|----------|-------------| | `request_id` | ✅ | Unique identifier for each VA creation request | | `bank_account_no` | ✅ | Merchant's underlying bank account number | | `internal_code` | ✅ | Merchant's internal customer identifier (Merchant CIF) | #### Request headers | Name | Required | Description | |------|----------|-------------| | `Partner-Code` | ✅ | Merchant identifier (provided by TCONNECT) | | `x-service-code` | ✅ | Mã dịch vụ ngân hàng (lấy từ API Get Services) | | `Content-Type` | ✅ | `application/json` | | `Authorization` | ✅ | `Bearer ` from Login | #### Request body | Field | Type | Required | Description | |-------|------|----------|-------------| | `data` | string | ✅ | AES-256-CBC encrypted payload as Hexadecimal string | #### Code examples ```bash curl --location '/openapi/v1/va/va-account/create' \ --header 'Partner-Code: YOUR_PARTNER_CODE' \ --header 'x-service-code: SERVICE_CODE' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer eyJhbGciOiJSUzI1…' \ --data '{"data": "ENCRYPTED_PAYLOAD"}' ``` ```python import requests url = "/openapi/v1/va/va-account/create" payload = {"data": "ENCRYPTED_PAYLOAD"} headers = { "Partner-Code": "YOUR_PARTNER_CODE", "x-service-code": "SERVICE_CODE", "Content-Type": "application/json", "Authorization": "Bearer eyJhbGciOiJSUzI1…", } response = requests.post(url, headers=headers, json=payload) print(response.text) ``` #### Response — 200 OK | Field | Type | Description | |-------|------|-------------| | `va_account_no` | string | Số tài khoản ảo được tạo (vd: `VA100023312`) | | `request_id` | string | Mã định danh yêu cầu | | `ref_id` | int | Mã định danh tài khoản ảo | ---. ## Create Transactions ### 5. Create QR **POST** `/openapi/v1/transaction/qr/generate` To ensure security, data sent will be chained and encrypted using the AES-256 algorithm. Generate a payment QR code for an order. > Call **Get Services** first to obtain `service_code` (used as the `x-service-code` header) and `bincode`. #### Pre-encryption payload ```json { "req_id": "req-12345", "order_id": "TCTK010520260001", "va": "VA100023312", "bincode": "970454", "amount": 10000 } ``` | Field | Required | Description | |-------|----------|-------------| | `req_id` | ✅ | Mã yêu cầu | | `order_id` | ✅ | Mã hóa đơn — tối đa 36 ký tự | | `va` | ✅ | Số tài khoản ảo | | `bincode` | ✅ | Mã ngân hàng | | `amount` | | `0` hoặc bỏ trống = QR tĩnh; `> 0` = QR động | #### Request headers | Name | Required | Description | |------|----------|-------------| | `partner-code` | ✅ | Mã định danh Merchant (TCONNECT cung cấp) | | `x-service-code` | ✅ | Mã dịch vụ | | `Content-Type` | ✅ | `application/json` | | `Authorization` | ✅ | `Bearer ` | #### Request body | Field | Type | Required | Description | |-------|------|----------|-------------| | `data` | string | ✅ | AES-256-CBC encrypted payload as Hexadecimal string | #### Code examples ```bash curl --location '/openapi/v1/transaction/qr/generate' \ --header 'partner-code: 81234567' \ --header 'x-service-code: SERVICE_CODE' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{"data": "ENCRYPTED_PAYLOAD"}' ``` ```python import requests url = "/openapi/v1/transaction/qr/generate" payload = {"data": "ENCRYPTED_PAYLOAD"} headers = { "partner-code": "81234567", "x-service-code": "SERVICE_CODE", "Authorization": "Bearer ", "Content-Type": "application/json", } response = requests.post(url, headers=headers, json=payload) print(response.text) ``` #### Response — 200 OK | Field | Type | Description | |-------|------|-------------| | `image_png_base64` | string | Base64 of the QR PNG image. Use directly in `` | | `qr_content` | string | Raw QR content string — use to render the QR code yourself if needed | #### Response example ```json { "image_png_base64": "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAA...", "qr_content": "00020101021138550010A000000727012500069704540111VA1000233120208QRIBFTTA5204561053037045802VN5912KFC VIET NAM6006Ha Noi622001031230809BILL123QR6304CE22" } ``` #### Status Codes | Code | Description | |------|-------------| | 00 | Success - Thành công | | 01 | Authenticate error - Lỗi xác thực | | 02 | The requested URL was not found on the server - URL yêu cầu không được tìm thấy trên máy chủ. | | 03 | Unknown error - Lỗi không xác định | --- ### 6. Push Payment to Devices **POST** `/openapi/v1/devices/payments/push` Push payment information (amount + QR code) directly to a physical device at the cashier counter (Smart POS or Soundbox). #### Pre-encryption payload — Smart POS ```json { "serial_no": "00059012710", "order_id": "0129210912", "amount": 150000, "type": "pos" } ``` #### Pre-encryption payload — Soundbox ```json { "serial_no": "SB-001234", "amount": 150000, "qr_string": "00020101021138550010A000000727...", "type": "soundbox" } ``` | Field | Required | Description | |-------|----------|-------------| | `serial_no` | ✅ | Device serial number | | `order_id` | ✅ when `type == "pos"` | Order identifier | | `amount` | ✅ | Amount (VND) | | `qr_string` | ✅ when `type == "soundbox"` | QR string returned by Create QR | | `type` | ✅ | `"pos"` or `"soundbox"` | #### Request headers | Name | Required | Description | |------|----------|-------------| | `Partner-Code` | ✅ | Merchant identifier (provided by TCONNECT) | | `Content-Type` | ✅ | `application/json` | | `Authorization` | ✅ | `Bearer ` | #### Request body | Field | Type | Required | Description | |-------|------|----------|-------------| | `data` | string | ✅ | AES-256-CBC encrypted payload as Hexadecimal string | #### Code examples ```bash curl --location '/openapi/v1/devices/payments/push' \ --header 'Partner-Code: 81234567' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ --data '{"data": "ENCRYPTED_PAYLOAD"}' ``` ```python import requests url = "/openapi/v1/devices/payments/push" payload = {"data": "ENCRYPTED_PAYLOAD"} headers = { "Partner-Code": "81234567", "Content-Type": "application/json", "Authorization": "Bearer ", } response = requests.post(url, headers=headers, json=payload) print(response.text) ``` #### Response — 200 OK | Field | Type | Description | |-------|------|-------------| | `message` | string | Push confirmation message (e.g., `Successfully pushed new payment to Soundbox`) | --- ## Query Transactions All three transaction query APIs share the same request structure. Only the endpoint path and response fields differ. ### Common request payload ```json { "from_date": "2026-01-07 00:00:00", "to_date": "2026-01-07 23:59:59", "limit": 10, "page": 1 } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `from_date` | string | ✅ | Start time (`YYYY-MM-DD HH:MM:SS`) | | `to_date` | string | ✅ | End time (`YYYY-MM-DD HH:MM:SS`) | | `limit` | number | ✅ | Records per page | | `page` | number | ✅ | Page number (starting from `1`) | ### Common request headers | Name | Required | Description | |------|----------|-------------| | `Partner-Code` | ✅ | Merchant identifier (provided by TCONNECT) | | `Content-Type` | ✅ | `application/json` | | `Authorization` | ✅ | `Bearer ` | ### Common pagination response fields | Field | Type | Description | |-------|------|-------------| | `pagination.page` | integer | Current page | | `pagination.limit` | integer | Records per page | | `pagination.total_items` | integer | Total record count | | `pagination.total_pages` | integer | Total page count | --- ### Check QR Transaction by Order ID **POST** `/openapi/v1/transaction/qr/order/status` Truy vấn trạng thái giao dịch thanh toán QR theo **Order ID**. Hệ thống sử dụng `order_id` để tra cứu giao dịch. `acc_no` là tham số tùy chọn và sẽ được sử dụng để đối chiếu nếu được cung cấp. #### Payload trước khi mã hóa ```json { "order_id": "1237DBD12", "acc_no": "1001000233120" } ``` | Field | Required | Description | |-------|----------|-------------| | `order_id` | ✅ | Mã đơn hàng cần tra cứu | | `acc_no` | ❌ | Số tài khoản nhận tiền. Bỏ trống = tra cứu trên mọi tài khoản của Merchant | #### Code examples ```bash curl --location '/openapi/v1/transaction/qr/order/status' \ --header 'Partner-Code: YOUR_PARTNER_CODE' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ --data '{"data": "ENCRYPTED_PAYLOAD"}' ``` ```python import requests url = "/openapi/v1/transaction/qr/order/status" payload = {"data": "ENCRYPTED_PAYLOAD"} headers = { "Partner-Code": "YOUR_PARTNER_CODE", "Content-Type": "application/json", "Authorization": "Bearer " } response = requests.post(url, headers=headers, json=payload) print(response.text) ``` #### Response — 200 OK | Field | Type | Description | |-------|------|-------------| | `message` | string | API processing status (e.g., `Success`) | | `tx_data` | object | Transaction data. Only returned when a record matching `order_id` (and `acc_no` if provided) is found | | `tx_data.order_id` | string | Order ID matching the request | | `tx_data.acc_no` | string | Merchant's receiving account number | | `tx_data.total_amount_paid` | integer | Total amount paid for this `order_id` (VND) | | `tx_data.transactions` | array | List of payment transaction details | | `tx_data.transactions[].request_id` | string | Request ID passed by the partner when creating the QR — used for reconciliation and tracing | | `tx_data.transactions[].status` | string | Transaction status — `SUCCESS` or `PENDING` | | `tx_data.transactions[].amount` | integer | Actual amount paid in this transaction (VND) | | `tx_data.transactions[].trn_ref_no` | string | Bank reference / reconciliation number | | `tx_data.transactions[].narrative` | string | Transfer description | | `tx_data.transactions[].txn_init_dt` | string | Transaction time (GMT+7, `YYYY-MM-DD HH:mm:ss`) | #### Response example — order has transactions ```json { "message": "Success", "tx_data": { "order_id": "1237DBD12", "acc_no": "1001000233120", "total_amount_paid": 72600, "transactions": [ { "request_id": "REQ_20260701_001", "status": "SUCCESS", "amount": 72600, "trn_ref_no": "HDSNNDHWIC", "narrative": "BILL1237DBD12QR", "txn_init_dt": "2026-01-22 10:00:00" } ] } } ``` #### Response example — order has no transaction yet ```json { "message": "Success", "tx_data": { "order_id": "1237DBD12", "acc_no": "1001000233120", "total_amount_paid": 0, "transactions": [] } } ``` #### Error responses | Code | Description | |------|-------------| | 400 | `Bad Request: order_id is required` | | 401 | `Unauthorized` — missing or invalid Bearer token | | 403 | `Forbidden: You do not have access to this account` | | 404 | `Not Found: QR transaction not found` | --- ### Recommended: Confirming Payment Status **Use the [Check QR Transaction by Order ID](#check-qr-transaction-by-order-id) API to confirm whether an order is paid — this is the recommended, most reliable method.** There are two ways to know a payment has completed. We recommend the **query (polling)** approach as the primary mechanism, and treat IPN as an optional enhancement: | Method | When it fires | Setup required | Recommendation | |--------|---------------|----------------|----------------| | **Query — Check QR Transaction by Order ID** (`/openapi/v1/transaction/qr/order/status`) | Whenever you call it (on demand) | **None** — works out of the box with your token | ✅ **Primary.** Poll this after showing the QR until `status = SUCCESS`. | | **IPN callback** (server-to-server push) | Automatically, right after payment | ⚠️ You must **register your callback URL with TCONNECT** first, and expose a public HTTPS endpoint | Optional. Nice for instant updates, but not required to go live. | **Why prefer the query API:** - **No pre-registration** — it works immediately with the sandbox credentials, so it is the fastest path to a working end-to-end integration (great for AI agents and quick demos). - **Self-contained** — no public/HTTPS endpoint, no firewall or tunnel needed on your side. - **Source of truth** — even if you also use IPN, you should re-query this endpoint to verify, because IPN delivery can be delayed, retried, or missed. **Recommended polling pattern:** 1. After **Create QR**, show the QR to the customer. 2. Poll `POST /openapi/v1/transaction/qr/order/status` with `{ order_id, acc_no }` every **3–5 seconds**. 3. When any item in `tx_data.transactions[]` has `status = "SUCCESS"`, mark the order paid and stop polling. 4. Apply a sensible timeout (e.g., stop after the QR expires) and show a retry option. > **IPN can be implemented as well**, but it requires **registering your partner callback > URL with TCONNECT** (contact TCONNECT to declare the URL). Until that is set up — and even > after — use the query API above as the authoritative check. See [IPN Callback](#ipn-callback). --- ### 7. Get QR Transactions **POST** `/openapi/v1/transaction/qr` Retrieve the list of successful QR payment transactions. All filter fields (`from_date`, `to_date`, `order_id`, `acc_no`) are optional. When no filter is supplied, every QR transaction of the Merchant is returned. > **Note:** `from_date` and `to_date` form a **pair**. Send both or neither — sending only one is rejected. #### Payload trước khi mã hóa ```json { "from_date": "2026-01-07 00:00:00", "to_date": "2026-01-07 23:59:59", "limit": 10, "page": 1, "order_id": "1237DBD12", "acc_no": "1001000233120" } ``` | Field | Required | Description | |-------|----------|-------------| | `from_date` | ❌ | Start time (`YYYY-MM-DD HH:MM:SS`). Must be sent together with `to_date` | | `to_date` | ❌ | End time (`YYYY-MM-DD HH:MM:SS`). Must be sent together with `from_date` | | `limit` | ✅ | Records per page | | `page` | ✅ | Page number (starting from `1`) | | `order_id` | ❌ | Filter by order id. Empty = no order filter | | `acc_no` | ❌ | Filter by receiving account number. Empty = all accounts of the Merchant | #### Code examples ```bash curl --location '/openapi/v1/transaction/qr' \ --header 'Partner-Code: YOUR_PARTNER_CODE' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsI…' \ --data '{"data": "ENCRYPTED_PAYLOAD"}' ``` ```python import requests url = "/openapi/v1/transaction/qr" payload = {"data": "ENCRYPTED_PAYLOAD"} headers = { "Partner-Code": "YOUR_PARTNER_CODE", "Content-Type": "application/json", "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsI…", } response = requests.post(url, headers=headers, json=payload) print(response.text) ``` #### Response — 200 OK | Field | Type | Description | |-------|------|-------------| | `data` | array | List of transactions | | `data[].trn_ref_no` | string | Bank transaction reference number | | `data[].amount` | string | Transaction amount (VND) | | `data[].narrative` | string | Transfer description | | `data[].txn_init_dt` | string | Transaction date/time (`YYYY-MM-DD HH:mm:ss`) | | `data[].order_id` | string | Your order identifier | | `pagination` | object | Pagination metadata | | `pagination.page` | integer | Current page | | `pagination.limit` | integer | Records per page | | `pagination.total_items` | integer | Total record count | | `pagination.total_pages` | integer | Total page count | #### Response example — 200 OK ```json { "data": [ { "trn_ref_no": "HDSNNDHWIC", "amount": "72600", "narrative": "BILL1237DBD12QR", "txn_init_dt": "2026-01-22 10:00:00", "order_id": "1237DBD12" }, { "trn_ref_no": "HDSNNDHWIC", "amount": "323000", "narrative": "BILL1237DBD12QR FT26021165605807", "txn_init_dt": "2026-01-21 18:00:00", "order_id": "1237DBD12" } ], "pagination": { "page": 1, "limit": 10, "total_items": 2, "total_pages": 1 } } ``` --- ### 8. Get Card Transactions **POST** `/openapi/v1/transaction/card` Retrieve the list of successful card payment transactions (VISA, NAPAS, etc.) within a specified time range. #### Code examples ```bash curl --location '/openapi/v1/transaction/card' \ --header 'Partner-Code: YOUR_PARTNER_CODE' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsI…' \ --data '{"data": "ENCRYPTED_PAYLOAD"}' ``` ```python import requests url = "/openapi/v1/transaction/card" payload = {"data": "ENCRYPTED_PAYLOAD"} headers = { "Partner-Code": "YOUR_PARTNER_CODE", "Content-Type": "application/json", "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsI…", } response = requests.post(url, headers=headers, json=payload) print(response.text) ``` #### Response — 200 OK | Field | Type | Description | |-------|------|-------------| | `data` | array | List of transactions | | `data[].order_id` | string | Order identifier | | `data[].card_no` | string | Masked card number (e.g., `1234****5678`) | | `data[].request_amount` | string | Transaction amount (VND) | | `data[].card_type` | string | Card type (e.g., `VISA`, `NAPAS`) | | `data[].payment_type` | string | Transaction type | | `data[].retrieval_ref_no` | string | Transaction reference number | | `data[].original_transaction_date` | string | Transaction date/time | --- ### 9. Get Cash Transactions **POST** `/openapi/v1/transaction/cash` Retrieve the list of recorded cash payment transactions. Used for shift closing and revenue reporting. #### Code examples ```bash curl --location '/openapi/v1/transaction/cash' \ --header 'Partner-Code: YOUR_PARTNER_CODE' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsI…' \ --data '{"data": "ENCRYPTED_PAYLOAD"}' ``` ```python import requests url = "/openapi/v1/transaction/cash" payload = {"data": "ENCRYPTED_PAYLOAD"} headers = { "Partner-Code": "YOUR_PARTNER_CODE", "Content-Type": "application/json", "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsI…", } response = requests.post(url, headers=headers, json=payload) print(response.text) ``` #### Response — 200 OK | Field | Type | Description | |-------|------|-------------| | `data` | array | List of transactions | | `data[].order_id` | string | Order identifier | | `data[].amount` | number | Amount (VND) | | `data[].original_transaction_date` | string | Transaction recorded date/time | --- ## IPN Callback TCONNECT uses IPN (Instant Payment Notification) to automatically notify the partner system when a customer completes a payment. > ℹ️ **IPN is optional and requires registration.** To receive IPN, you must first > **declare your callback URL with TCONNECT** (the partner backend endpoint below). > Until it is registered — and as a safety net even after — use the > **[Check QR Transaction by Order ID](#check-qr-transaction-by-order-id)** query API as the > authoritative way to confirm payment status. See > [Recommended: Confirming Payment Status](#recommended-confirming-payment-status). **POST** `{{partner_url}}` TCONNECT calls this URL on the partner's backend to update the order status immediately when payment is recorded. ### Request headers | Name | Required | Description | |------|----------|-------------| | `Content-Type` | ✅ | `application/json` | ### Body (Encrypted) | Field | Type | Required | Description | |-------|------|----------|-------------| | `data` | string | ✅ | AES-256-CBC encrypted JSON containing transaction details | ### Body (Decrypted) | Field | Type | Description | |-------|------|-------------| | `order_id` | string | Partner's order identifier | | `amount` | number | Actual payment amount | | `payment_type` | string | Payment type (default: QR) | | `retrieval_ref_no` | string | Bank transaction reference number | | `request_id` | string | Unique request identifier | | `narrative` | string | Transfer description entered by the customer | | `acc_no` | string | Account number used for the transaction (if available) | | `original_transaction_date` | number | Unix timestamp of the transaction date | ### Examples **IPN payload (encrypted)** ```json { "data": "e53cb37a8743b33bbe598bf43394c4..." } ``` **IPN payload (decrypted)** ```json { "order_id": "0106002530", "amount": 2000.0, "payment_type": "QR", "retrieval_ref_no": "20260211000006", "request_id": "39b73dd3-1b1c-4b37-b2d2...", "narrative": "117926751847 FT26021165605807", "acc_no": "9648364", "original_transaction_date": 1770782306 } ``` ### Implementation requirements The partner must expose a publicly accessible backend endpoint for TCONNECT to call. The endpoint must: - Accept `POST` requests with `Content-Type: application/json` - Decrypt the `data` field using AES-256-CBC with the shared key - Process the transaction and update the order status - Return HTTP `200` to confirm successful receipt of the IPN > **Important:** If TCONNECT does not receive an HTTP `200` response, it may retry the IPN delivery. Ensure your endpoint handles duplicate IPN calls idempotently using `request_id`. --- ## Environments | Environment | Base URL | |-------------|----------| | **Staging** | `https://sme-open-api-sandbox.tconnect.vn` | | **Production** | Provided after testing is complete | ## Contact - **Email:** info@tconnect.vn - **Website:** https://tconnect.vn