# 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 --- ## Table of Contents 1. [Authentication](#authentication) - [Login](#1-login) - [Refresh Token](#2-refresh-token) 2. [Encryption](#encryption) - [AES-256-CBC Overview](#aes-256-cbc-overview) - [Python Helper](#python-helper) 3. [Services](#services) - [Get List Services](#3-get-list-services) 4. [Virtual Account](#virtual-account) - [Create VA](#4-create-va) 5. [Create Transactions](#create-transactions) - [Create QR](#5-create-qr) 6. [Query Transactions](#query-transactions) - [Get QR Transactions](#6-get-qr-transactions) - [Get Card Transactions](#7-get-card-transactions) - [Get Cash Transactions](#8-get-cash-transactions) 7. [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": "demo@yourdomain.vn", "password": "Password#123", "client_id": "fMLgZltRRtetnsOHXgxsHQ", "client_secret": "hUFhGJIqz746zcxsYVtrwhkveDuEfcYd" } ``` | 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"` | | `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__": KEY = bytes.fromhex( "6ddf4e7def3233f17984aaaa90e26bfe2859bb349d23d50988661056b6ecc11" ) message = '{"username": "demo@yourdomain.com", "password": "Password#123"}' 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 (e.g., `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[].payment_method` | string | Supported payment method | | `data[].allowed_va_creation` | boolean | Whether VA creation is permitted for this service | | `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": "bidv-qr", "name": "Dịch vụ thanh toán QR - BIDV", "service_type": "finance", "payment_method": "qr", "allowed_va_creation": false, "provider": { "provider_code": "bidv", "provider_name": "BIDV" } } ], "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. #### Pre-encryption payload ```json { "bank_code": "970454", "account_name": "NGUYEN VAN A" } ``` | Field | Required | Description | |-------|----------|-------------| | `bank_code` | ✅ | Bank code of the issuing bank | | `account_name` | ✅ | Display name on the virtual account | #### Request headers | Name | Required | Description | |------|----------|-------------| | `Partner-Code` | ✅ | Merchant identifier (provided by TCONNECT) | | `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 '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", "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_number` | string | Virtual account number created (e.g., `VA100023312`) | | `bank_code` | string | Issuing bank code | | `account_name` | string | Virtual account name | | `status` | string | VA status (`active`) | --- ## Create Transactions ### 5. Create QR **POST** `/openapi/v1/qr` To ensure security, data sent will be chained and encrypted using the AES-256 algorithm. Generate a payment QR code for an order. #### Pre-encryption payload ```json { "accountNumber": "99MPI0000267600005", "amount": 10000, "orderId": "ORDER001", "binCode": "970454", "narrative": "Thanh toán đơn hàng", "extraData": { "key1": "value1", "key2": "value2" } } ``` | Field | Required | Description | |-------|----------|-------------| | `accountNumber` | ✅ | Virtual account number (VA) of the merchant | | `amount` | | Transaction amount | | `narrative` | | Transaction description | | `binCode` | | Bank BIN code (Default: 970454) | | `orderId` | | Merchant transaction ID. If provided, narrative is disabled. | | `extraData` | | Extended object for IPN custom data. Only works if orderId is set. | #### Request headers | Name | Required | Description | |------|----------|-------------| | `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/qr' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{"data": "ENCRYPTED_PAYLOAD"}' ``` ```python import requests url = "/openapi/v1/qr" payload = {"data": "ENCRYPTED_PAYLOAD"} headers = { "Authorization": "Bearer ", "Content-Type": "application/json", } response = requests.post(url, headers=headers, json=payload) print(response.text) ``` #### Response — 200 OK | Field | Type | Description | |-------|------|-------------| | `qrString` | string | Raw QR code string — use to render the QR code yourself if needed | #### 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 | --- ## 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 | --- ### 6. Get QR Transactions **POST** `/openapi/v1/transaction/qr` Retrieve the list of successful QR payment transactions within a specified time range. #### 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[].order_id` | string | Your order identifier | | `data[].amount` | string | Transaction amount (VND) | | `data[].trn_ref_no` | string | Bank transaction reference number | | `data[].narrative` | string | Transfer description | | `data[].txn_init_dt` | string | Transaction date/time (`YYYY-MM-DD HH:mm:ss`) | --- ### 7. 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 | --- ### 8. 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. **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