Skip to main content
This guide explains how to integrate with the UQPAY POS API for card present transactions. It covers terminal registration, PIN key management, PIN block encryption, and payment processing.

1. Overview

1.1 Prerequisites

Before starting the integration, ensure you have:
  • Provide your UQPAY account ID to enable the POS API feature

1.2 Integration Flow

┌─────────────────────────────────────────────────────────────────────────┐
│                        POS API Integration Flow                         │
└─────────────────────────────────────────────────────────────────────────┘

Step 1: Terminal Registration (One time setup)
    POS Terminal ──► Register Terminal API ──► Receive terminal_id (TID)

Step 2: PIN Key Setup
    Generate AES Key ──► Get PIN Key API ──► Decrypt to get PINKEY

Step 3: Process Payment
    Read Card ──► Encrypt PIN ──► Create Payment Intent ──► Receive Result

2. Terminal Registration

Each POS terminal must be registered with UQPAY before processing transactions. This is a one time setup; you only need to register each terminal once.

2.1 API Endpoint

POST /v2/terminal/register
API Reference: Register Terminal

2.2 Request Parameters

FieldTypeRequiredDescriptionExample
firm_codestringYesTerminal manufacturer code"03"
firm_snstringYesTerminal serial number from manufacturer"TG676SX1764902333"
terminal_modelstringYesTerminal model name"P1000"
Supported Manufacturer Codes:
CodeManufacturer
01Shengben
02Newland
03Xinguodu
04iMin
05PAX

2.3 Request Example

{
    "firm_code": "03",
    "firm_sn": "TG676SX1764902333",
    "terminal_model": "P1000"
}

2.4 Response

FieldTypeDescription
terminal_idstringUQPAY assigned terminal identifier (TID)
firm_snstringTerminal serial number
create_timestringRegistration timestamp
{
    "create_time": "2025-12-05 10:38:53",
    "firm_sn": "TG676SX1764902333",
    "terminal_id": "10000254"
}
Note: Store the terminal_id securely. You will need it for all subsequent API calls.

3. PIN Key Management

To securely process PIN based transactions, you must obtain and manage PIN encryption keys.

3.1 Generate AES Private Key

Before requesting a PIN key, generate an AES private key on your terminal or server:
RequirementSpecification
AlgorithmAES
Key LengthUp to 256 bits (64 hex characters)
FormatHexadecimal string
Example AES Key (256 bit):
5214abf357f2cc47645c4d942c02fd0180acc6d8de8fae3708849b6a459cba00
Important: Store this private key securely. You will use it to decrypt the encrypt_pin_key returned from the Get PIN Key API.

3.2 Request PIN Key

API Endpoint:
POST /v2/terminal/getPinKey
API Reference: Get PIN Key Request Parameters:
FieldTypeRequiredDescriptionExample
terminal_idstringYesTerminal ID received from registration"10000254"
prv_keystringYesYour AES private key (hex format, max 64 characters)"5214abf357f2cc47645c4d942c02fd0180acc6d8de8fae3708849b6a459cba00"
Request Example:
{
    "terminal_id": "10000254",
    "prv_key": "5214abf357f2cc47645c4d942c02fd0180acc6d8de8fae3708849b6a459cba00"
}
Response:
FieldTypeDescription
terminal_idstringTerminal identifier
encrypt_pin_keystringEncrypted PIN key (Base64 encoded)
pin_key_expireintegerPIN key expiration timestamp (milliseconds)
{
    "encrypt_pin_key": "LASDho1ILHoYRf/5YEyIgieoc+SXJkUsHZElXOMNGv7WnC3fZzFPYDH8mJaDbnwvom3QEtdv3NjvaEUtVeetWdQsv2RtQw4XEYh/Cg==",
    "pin_key_expire": 1764990056170,
    "terminal_id": "10000254"
}

3.3 Decrypt PIN Key

Use your AES private key to decrypt the encrypt_pin_key and obtain the actual PINKEY. Decryption Parameters:
ParameterValue
AlgorithmAES-256-GCM
Inputencrypt_pin_key
KeyYour prv_key
OutputPINKEY
Example:
  • encrypt_pin_key: LASDho1ILHoYRf/5YEyIgieoc+SXJkUsHZElXOMNGv7WnC3fZzFPYDH8mJaDbnwvom3QEtdv3NjvaEUtVeetWdQsv2RtQw4XEYh/Cg==
  • actual PINKEY: 26e1734e4d52b905e74574b7bf0897daeec7e217c61b3ac0
See Appendix for decryption code samples.

3.4 Key Expiration

Monitor the pin_key_expire timestamp and refresh the PIN key before expiration. Request a new PIN key using the same process when needed.

4. PIN Block Encryption

When processing PIN based transactions, you must encrypt the cardholder PIN into a PIN block.

4.1 Overview

The PIN block is created by combining:
  • Card PAN (Primary Account Number)
  • Cardholder PIN
  • PINKEY

4.2 Example

Input:
ParameterValue
Card Number5554748800260229
PIN302312
PINKEY26e1734e4d52b905e74574b7bf0897daeec7e217c61b3ac0
Output:
ParameterValue
PINBLOCK4F5D12303995DD06
See Appendix for PIN block encryption code samples.

5. Create Payment Intent

After setting up your terminal and obtaining the PINKEY, you can process card present transactions.

5.1 API Endpoint

POST /v2/payment_intents/create
API Reference: Create Payment Intent

5.2 Request Example

Use the card_present payment method type for POS transactions. Key fields to note:
  • terminal_id: Set to the TID received from terminal registration
  • encrypted_pin: Set to the PINBLOCK generated from PIN encryption
  • system_trace_audit_number: Must be unique for each new transaction (STAN)
{
    "amount": "2.22",
    "currency": "SGD",
    "payment_method": {
        "type": "card_present",
        "card_present": {
            "card_name": "mastercard 0229",
            "card_number": "5554748800260229",
            "expiry_month": "03",
            "expiry_year": "2026",
            "cardholder_verification_method": "online_pin",
            "encrypted_pin": "4F5D12303995DD06",
            "pan_entry_mode": "contactless_chip",
            "fallback": false,
            "fallback_reason": "",
            "emv_tags": "9F39010757135554748800260229D26032211837752200000F820219809F360202CE9F1E0831303030303030319F10120110A04003240000000000000000000000FF9F3303E060C89F350122950500000480019F0106A000000000019F02060000000500005F24032603315A0855547488002602295F3401009F150258129F160F3130303030303031303030303030309F1A0207029F1C08313030303030303181040000C3505F2A0207029A032512029F21031418059C01005F280204409F120A4D6173746572636172649F1101015F2D02656E9F42020978940810010101200104008701019F0A04000101049F080200028C279F02069F03069F1A0295055F2A029A039C019F37049F35019F45029F4C089F34039F21039F7C149F4C08CF57740F41E613609F4A01825F300202219F26088BD046AF7A2D8D9A9F2701809F34030203009F37041DF853909F03060000000000005F25032303019F4104000000009F0702FFC08E0E000000000000000002031E031F039F0D05B4508400009F0E0500000000009F0F05B4708480004F07A00000000410108407A00000000410109F090200029B02E0009F4E083130303030303031500A4D6173746572636172649F4005F000F0F0019F0607A00000000410109F6E0704400000323000D4030000029908EA53B2F5A7D54E309F72080000000200000000",
            "track1": "",
            "track2": "5554748800260229D26032211837752200000",
            "terminal_info": {
                "mobile_device": false,
                "system_trace_audit_number": "000003",
                "terminal_id": "10000254",
                "use_embedded_reader": true
            }
        }
    },
    "merchant_order_id": "{{$guid}}",
    "description": "pos api order from sandbox",
    "metadata": {
        "request_id": "{{$guid}}"
    },
    "return_url": "https://return-url/api/v2/callback"
}

5.3 Response Example

{
    "amount": "2.22",
    "available_payment_method_types": null,
    "cancel_time": "",
    "cancellation_reason": "",
    "captured_amount": "2.22",
    "client_secret": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtYXN0ZXJfaWQiOiIwIiwiYWNjb3VudF9pZCI6ImIxYjg5Njg0LWMyYzQtNGQ1NC1iOGE4LTM1NzI3MjdmZDEyMCIsImludGVudF9pZCI6IlBJMTk5Njc5NTM0MjE0OTkxNDYyNCIsImV4cCI6MTc2NDkwOTg4MiwiaWF0IjoxNzY0OTA4MDgyfQ.tUAF1IHWm-payNAES4qOv_Vngbm3mn5z217DayihgLI",
    "complete_time": "2025-12-05T12:14:42+08:00",
    "create_time": "2025-12-05T12:14:42+08:00",
    "currency": "SGD",
    "description": "pos api order from sandbox",
    "intent_status": "SUCCEEDED",
    "latest_payment_attempt": {
        "amount": "2.22",
        "attempt_id": "PA1996795342275743744",
        "attempt_status": "CAPTURE_REQUESTED",
        "captured_amount": "2.22",
        "complete_time": "2025-12-05T12:14:42+08:00",
        "create_time": "2025-12-05T12:14:42+08:00",
        "currency": "SGD",
        "failure_code": "",
        "refunded_amount": "0",
        "update_time": "2025-12-05T12:14:42+08:00"
    },
    "merchant_order_id": "76bcc9a2-0aad-467a-aded-06c0c03bdced",
    "metadata": {
        "request_id": "3e635c58-8774-4425-84a9-42a602299576"
    },
    "next_action": null,
    "payment_intent_id": "PI1996795342149914624",
    "return_url": "https://return-url/api/v2/callback",
    "update_time": "2025-12-05T12:14:42+08:00"
}

6. Error Handling

6.1 POS API Error Codes

Error CodeError Message
account_pos_api_no_permissionAccount pos api no permission
terminal_not_belong_to_accountTerminal not belong to account
terminal_register_failedTerminal register failed
terminal_info_not_foundTerminal info not found
pinkey_retrieve_failedPinkey retrieve failed

7. Testing

7.1 Test Card

Use the following test card data in the sandbox environment:
FieldValue
Card Number5554748800161559
Expiry Month07
Expiry Year2027
PIN (raw)
Card BrandMastercard
Sample card_present object for testing:
{
        "type": "card_present",
        "card_present": {
            "card_name": "uqpay cardholder",
            "card_number": "5554748800161559",
            "expiry_month": "07",
            "expiry_year": "2027",
            "cardholder_verification_method": "online_pin",
            // "encrypted_pin": "D6CD73498446A0C9",
            "pan_entry_mode": "contactless_chip", //卡片接触方式
            "fallback": false,
            "fallback_reason": "",
            "emv_tags": "9F39010757135554748800161559D25042211103236300000F820219809F3602051B9F1E0831303030303135319F10120110A00003240000000000000000000000FF9F3303E008C89F350122950504400080019F0106A000000000019F02060000000000015F24032504305A0855547488001615595F3401009F150258129F160F3130303030313531303030303030309F1A0207029F1C0831303030303135318104000000015F2A0207029A032512059F21031652269C01005F280204409F42020978940810010101200103008701019F0A04000101049F080200028C279F02069F03069F1A0295055F2A029A039C019F37049F35019F45029F4C089F34039F21039F7C149F4A01825F300202219F260817DE07EBE32C04859F2701809F34031F03029F3704C18B521B9F03060000000000005F25032204019F4104000000009F0702FFC08E0E000000000000000002031E031F039F0D05B4508400009F0E0500000000009F0F05B4708480004F07A00000000410108407A00000000410109F090200029B0260009F4E083130303030313531500A4D6173746572636172649F4005F000F0F0019F0607A00000000410109F6E0704400000323000D4034000019F72080000000000000000", //从芯片卡读取的标签长度值(TLV)编码数据。
            "track1": "", //磁道1数据
            "track2": "5554748800161559D25042211103236300000", //磁道2数据
            "terminal_info": {
                "mobile_device": false, //说明 POS 终端是否为移动 POS 设备。
                "system_trace_audit_number": "000014", //终端交易流水号
                "terminal_id": "10000267", //终端号
                "use_embedded_reader": true //读卡器是否嵌入在移动 POS 设备中。
            }
        }
    }
Note: Remember to use a unique system_trace_audit_number for each test transaction.

8. Appendix

8.1 Code Samples

AES-256-GCM Decryption (Python)

# aes_decrypt.py
import sys
import base64
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend

def aes_decrypt(prv_key: str, encrypt_pin_key: str) -> str:
    key = bytes.fromhex(prv_key)
    ciphertext = base64.b64decode(encrypt_pin_key)
    nonce = ciphertext[:12]
    tag = ciphertext[-16:]
    ct = ciphertext[12:-16]
    cipher = Cipher(algorithms.AES(key), modes.GCM(nonce, tag), backend=default_backend())
    decryptor = cipher.decryptor()
    plaintext = decryptor.update(ct) + decryptor.finalize()
    return plaintext.decode("utf-8")

if __name__ == "__main__":
    print(aes_decrypt(sys.argv[1], sys.argv[2]))
# Usage
python3 aes_decrypt.py <prv_key> <encrypt_pin_key>

# Example
python3 aes_decrypt.py 5214abf357f2cc47645c4d942c02fd0180acc6d8de8fae3708849b6a459cba00 "LASDho1ILHoYRf/5YEyIgieoc+SXJkUsHZElXOMNGv7WnC3fZzFPYDH8mJaDbnwvom3QEtdv3NjvaEUtVeetWdQsv2RtQw4XEYh/Cg=="
# Output: 26e1734e4d52b905e74574b7bf0897daeec7e217c61b3ac0

PIN Block Encryption (Python)

# pinblock.py
import sys
import binascii
from Crypto.Cipher import DES3
from Crypto.Util.strxor import strxor

def create_pin_block(card_number: str, pin: str, pinkey: str) -> str:
    pin_field_hex = f"0{len(pin)}{pin}".ljust(16, 'F')
    pin_field_bytes = binascii.unhexlify(pin_field_hex)
    pan_part = card_number[-13:-1]
    pan_field_hex = "0000" + pan_part
    pan_field_bytes = binascii.unhexlify(pan_field_hex)
    pin_block = strxor(pin_field_bytes, pan_field_bytes)
    pek_bytes = binascii.unhexlify(pinkey)
    cipher = DES3.new(pek_bytes, DES3.MODE_ECB)
    encrypted = cipher.encrypt(pin_block)
    return binascii.hexlify(encrypted).decode('utf-8').upper()

if __name__ == "__main__":
    print(create_pin_block(sys.argv[1], sys.argv[2], sys.argv[3]))
# Usage
python3 pinblock.py <card_number> <pin> <pinkey>

# Example
python3 pinblock.py 5554748800260229 302312 26e1734e4d52b905e74574b7bf0897daeec7e217c61b3ac0
# Output: 4F5D12303995DD06

8.2 Glossary

TermDefinition
TIDTerminal Identifier, unique ID assigned by UQPAY
PINKEYPIN encryption key used to encrypt cardholder PINs
PIN BlockEncrypted representation of cardholder PIN
STANSystem Trace Audit Number, unique transaction trace number
EMVEuropay, Mastercard, Visa; chip card standard
PANPrimary Account Number (card number)
CVMCardholder Verification Method