Public API Documentation
Version: 1.1.0 Last Updated: 2025-12-24
Environments
| Environment | Base URL | Description |
|---|---|---|
| Production | https://gateway.facaiqq.com/v1 | Real transactions, live payments |
| Sandbox | https://gateway.facaiqq.com/v1 | Test transactions (sandbox merchant type only) |
Overview
The Public API enables merchants to integrate payment processing capabilities into their applications. All API requests require HMAC-SHA256 authentication.
Production vs Sandbox
Merchant type determines transaction routing:
- Production Merchant: Real money transactions processed through payment providers.
- Sandbox Merchant: Simulated transactions for testing. Responses are mocked locally without external processing.
Authentication
All API requests must include the following headers:
| Header | Required | Description |
|---|---|---|
X-API-Key | Yes | Your merchant API key (64 characters) |
X-Signature | Yes | HMAC-SHA256 signature of the request |
X-Timestamp | Yes | Unix timestamp (seconds since epoch) |
Content-Type | Yes | application/json |
Signature Generation
The signature is computed using HMAC-SHA256 with your Secret Key:
Signature = HMAC-SHA256(SignatureData, SecretKey)
SignatureData = METHOD|PATH|TIMESTAMP|BODYExample (POST request):
SignatureData = "POST|/v1/payments/deposit|1702400000|{\"amount\":100.00,\"currency\":\"THB\",\"reference_id\":\"ORDER-001\"}"Example (GET request):
SignatureData = "GET|/v1/payments/pay_abc123|1702400000"Code Examples
import hmac
import hashlib
import time
import json
import requests
api_key = "your_api_key"
secret_key = "your_secret_key"
base_url = "https://gateway.facaiqq.com"
def create_signature(method, path, timestamp, body=""):
# IMPORTANT: path must be the full HTTP path (e.g., /v1/payments/deposit)
data = f"{method}|{path}|{timestamp}"
if body:
data += f"|{body}"
return hmac.new(
secret_key.encode(),
data.encode(),
hashlib.sha256
).hexdigest()
def make_request(method, path, body=None):
timestamp = str(int(time.time()))
body_str = json.dumps(body) if body else ""
signature = create_signature(method, path, timestamp, body_str)
headers = {
"X-API-Key": api_key,
"X-Signature": signature,
"X-Timestamp": timestamp,
"Content-Type": "application/json"
}
url = base_url + path
if method == "POST":
return requests.post(url, headers=headers, json=body)
else:
return requests.get(url, headers=headers)
# Create deposit - note: use full path including /v1
response = make_request("POST", "/v1/payments/deposit", {
"amount": 1000.00,
"currency": "THB",
"reference_id": "ORD888888888",
"bank_code": "004",
"bank_account_no": "1234567890",
"bank_account_name": "test test",
"webhook_url": "https://example.com/path/to/your/webhook"
})
print(response.json())const crypto = require("crypto");
const axios = require("axios");
const apiKey = "your_api_key";
const secretKey = "your_secret_key";
const baseUrl = "https://gateway.facaiqq.com";
function createSignature(method, path, timestamp, body = "") {
// IMPORTANT: path must be the full HTTP path (e.g., /v1/payments/deposit)
let data = `${method}|${path}|${timestamp}`;
if (body) {
data += `|${body}`;
}
return crypto.createHmac("sha256", secretKey).update(data).digest("hex");
}
async function makeRequest(method, path, body = null) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const bodyStr = body ? JSON.stringify(body) : "";
const signature = createSignature(method, path, timestamp, bodyStr);
const headers = {
"X-API-Key": apiKey,
"X-Signature": signature,
"X-Timestamp": timestamp,
"Content-Type": "application/json",
};
const url = baseUrl + path;
if (method === "POST") {
return axios.post(url, body, { headers });
} else {
return axios.get(url, { headers });
}
}
// Create deposit - note: use full path including /v1
makeRequest("POST", "/v1/payments/deposit", {
amount: 1000.0,
currency: "THB",
reference_id: "ORD888888888",
bank_code: "004",
bank_account_no: "1234567890",
bank_account_name: "test test",
webhook_url: "https://example.com/path/to/your/webhook",
}).then((res) => console.log(res.data));<?php
$apiKey = 'your_api_key';
$secretKey = 'your_secret_key';
$baseUrl = 'https://gateway.facaiqq.com';
function createSignature($method, $path, $timestamp, $body = '') {
global $secretKey;
// IMPORTANT: path must be the full HTTP path (e.g., /v1/payments/deposit)
$data = "$method|$path|$timestamp";
if ($body) {
$data .= "|$body";
}
return hash_hmac('sha256', $data, $secretKey);
}
function makeRequest($method, $path, $body = null) {
global $apiKey, $baseUrl;
$timestamp = time();
$bodyStr = $body ? json_encode($body) : '';
$signature = createSignature($method, $path, $timestamp, $bodyStr);
$headers = [
'X-API-Key: ' . $apiKey,
'X-Signature: ' . $signature,
'X-Timestamp: ' . $timestamp,
'Content-Type: application/json'
];
$ch = curl_init($baseUrl . $path);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $bodyStr);
}
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
// Create deposit - note: use full path including /v1
$response = makeRequest('POST', '/v1/payments/deposit', [
'amount' => 1000.00,
'currency' => 'THB',
'reference_id' => 'ORD888888888',
'bank_code' => '004',
'bank_account_no' => '1234567890',
'bank_account_name' => 'test test',
'webhook_url' => 'https://example.com/path/to/your/webhook'
]);
print_r($response);package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
)
const (
apiKey = "your_api_key"
secretKey = "your_secret_key"
baseURL = "https://gateway.facaiqq.com"
)
func createSignature(method, path, timestamp, body string) string {
// IMPORTANT: path must be the full HTTP path (e.g., /v1/payments/deposit)
data := fmt.Sprintf("%s|%s|%s", method, path, timestamp)
if body != "" {
data += "|" + body
}
h := hmac.New(sha256.New, []byte(secretKey))
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}
func makeRequest(method, path string, body interface{}) (map[string]interface{}, error) {
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
var bodyStr string
var reqBody io.Reader
if body != nil {
bodyBytes, _ := json.Marshal(body)
bodyStr = string(bodyBytes)
reqBody = bytes.NewBuffer(bodyBytes)
}
signature := createSignature(method, path, timestamp, bodyStr)
req, _ := http.NewRequest(method, baseURL+path, reqBody)
req.Header.Set("X-API-Key", apiKey)
req.Header.Set("X-Signature", signature)
req.Header.Set("X-Timestamp", timestamp)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}
func main() {
// Create deposit - note: use full path including /v1
result, _ := makeRequest("POST", "/v1/payments/deposit", map[string]interface{}{
"amount": 1000.00,
"currency": "THB",
"reference_id": "ORD888888888",
"bank_code": "004",
"bank_account_no": "1234567890",
"bank_account_name": "test test",
"webhook_url": "https://example.com/path/to/your/webhook",
})
fmt.Printf("%+v\n", result)
}Endpoints
Create Deposit
Create a new deposit payment request. Returns a PromptPay QR code for the customer to pay.
POST /v1/payments/deposit
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Payment amount (> 0) |
currency | string | Yes | Currency code (3 characters, e.g., "THB") |
reference_id | string | Yes | Your unique order reference (1-64 characters) |
customer_email | string | No | Customer email for notifications |
customer_phone | string | No | Customer phone number (max 20 characters) |
description | string | No | Payment description (max 255 characters) |
webhook_url | string | No | Webhook URL for payment notifications |
bank_code | string | No | Payer's bank code (3 digits) for reference |
bank_account_no | string | No | Payer's bank account number (max 20 characters) |
bank_account_name | string | No | Payer's bank account holder name (max 100 characters) |
Example Request
{
"amount": 1000.00,
"currency": "THB",
"reference_id": "ORD888888888",
"bank_code": "004",
"bank_account_no": "1234567890",
"bank_account_name": "test test",
"webhook_url": "https://example.com/path/to/your/webhook"
}Response
The response format depends on whether the amount reaches an admin-defined threshold (the example below uses 5000 for illustration only — the actual threshold is configured by the admin).
When amount is below the threshold (e.g., < 5000) — Pay via PromptPay QR
company_account = false and a PromptPay QR code is returned as usual.
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"reference_id": "ORD888888888",
"type": "deposit",
"status": "pending",
"amount": 1000.00,
"currency": "THB",
"company_account": false,
"qr_code": "00020101021129370016A000000677010111011300668123456785802TH5303764...",
"qr_code_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
"created_at": "2025-12-24T10:30:00Z",
"expires_at": "2025-12-24T10:45:00Z"
}When amount reaches the threshold (e.g., >= 5000) — Transfer to company account
company_account = true with company account details (company_account_bank_code, company_account_bank_name, company_account_name, company_account_no). The qr_code / qr_code_image fields are not returned in this response.
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"reference_id": "ORD888888888",
"type": "deposit",
"status": "pending",
"amount": 5000.00,
"currency": "THB",
"company_account": true,
"company_account_bank_code": "004",
"company_account_bank_name": "Kasikornbank",
"company_account_name": "Example Company Ltd.",
"company_account_no": "1234567890",
"created_at": "2025-12-24T10:30:00Z",
"expires_at": "2025-12-24T10:45:00Z"
}Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Unique transaction ID (UUID format) |
reference_id | string | Your order reference |
type | string | Transaction type (deposit) |
status | string | Payment status |
amount | number | Payment amount |
currency | string | Currency code |
company_account | boolean | Company account flag — true when amount reaches the admin-defined threshold (transfer to company account), false when below the threshold (pay via QR). 5000 is an example threshold. |
company_account_bank_code | string | Company account bank code (3 digits) (only present when company_account = true) |
company_account_bank_name | string | Company account bank name (only present when company_account = true) |
company_account_name | string | Company account holder name (only present when company_account = true) |
company_account_no | string | Company account number (only present when company_account = true) |
qr_code | string | PromptPay QR code string (only returned when company_account = false) |
qr_code_image | string | Base64 encoded QR code image, PNG (only returned when company_account = false) |
created_at | string | Creation timestamp (ISO 8601) |
expires_at | string | Expiration timestamp (ISO 8601, 15 min default) |
Create Withdraw
Create a new withdrawal/payout request to a bank account.
POST /v1/payments/withdraw
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Withdrawal amount (> 0) |
currency | string | Yes | Currency code (3 characters) |
reference_id | string | Yes | Your unique reference (1-64 characters) |
bank_code | string | Yes | Bank code (3 digits, see table below) |
account_number | string | Yes | Bank account number (10-20 digits) |
account_holder_name | string | Yes | Account holder name (1-100 characters) |
description | string | No | Transfer description (max 255 characters) |
webhook_url | string | No | Webhook URL for transfer notifications |
Bank Codes
| Code | Thai Name | English Name |
|---|---|---|
002 | ธนาคารกรุงเทพ | Bangkok Bank PCL (BBL) |
004 | ธนาคารกสิกรไทย | Kasikornbank PCL (Kbank) |
006 | ธนาคารกรุงไทย | Krungthai Bank PCL (KTB) |
011 | ธนาคารทหารไทยธนชาต | TMBThanachart Bank PCL (TTB) |
014 | ธนาคารไทยพาณิชย์ | Siam Commercial Bank PCL (SCB) |
017 | ธนาคารซิตี้แบงก์ | Citibank N.A. |
020 | ธนาคารสแตนดาร์ดชาร์เตอร์ด (ไทย) | Standard Chartered Bank (Thai) PCL |
022 | ธนาคารซีไอเอ็มบี ไทย | CIMB Thai Bank PCL |
024 | ธนาคารยูโอบี | United Overseas Bank (Thai) PCL |
025 | ธนาคารกรุงศรีอยุธยา | Bank of Ayudhya PCL (Krungsri) |
026 | ธนาคารเมกะสากลพาณิชย์ | Mega International Commercial Bank |
030 | ธนาคารออมสิน | Government Savings Bank (GSB) |
031 | ธนาคารฮ่องกงและเซี่ยงไฮ้ | HSBC Thailand |
033 | ธนาคารอาคารสงเคราะห์ | Government Housing Bank (GHB) |
034 | ธนาคารเพื่อการเกษตรฯ | BAAC |
065 | ธนาคารธนชาต | Thanachart Bank PCL |
066 | ธนาคารอิสลามแห่งประเทศไทย | Islamic Bank of Thailand |
067 | ธนาคารทิสโก้ | TISCO Bank PCL |
069 | ธนาคารเกียรตินาคินภัทร | Kiatnakin Phatra Bank PCL (KKP) |
070 | ธนาคารไอซีบีซี (ไทย) | ICBC (Thai) PCL |
071 | ธนาคารไทยเครดิต | Thai Credit Retail Bank |
073 | ธนาคารแลนด์ แอนด์ เฮ้าส์ | Land and Houses Bank PCL (LH Bank) |
088 | ธนาคารคลิกซ์ | Clicx Bank |
PROMPTPAY | พร้อมเพย์ | PromptPay |
TRUE | ทรูมันนี่ | TrueMoney |
PromptPay Withdraw
When using bank_code: "PROMPTPAY", the account_number field should contain:
| Account Number Length | Proxy Type | Description |
|---|---|---|
| 10 digits | MOBILE | Mobile phone number |
| 13 digits | TXID | National ID or Tax ID |
Example Request (PromptPay with Mobile):
{
"amount": 5000.00,
"currency": "THB",
"reference_id": "ORD888888890",
"bank_code": "PROMPTPAY",
"account_number": "0812345678",
"account_holder_name": "John Doe"
}Example Request (PromptPay with National ID):
{
"amount": 5000.00,
"currency": "THB",
"reference_id": "ORD888888891",
"bank_code": "PROMPTPAY",
"account_number": "1234567890123",
"account_holder_name": "John Doe"
}Example Request
{
"amount": 5000.00,
"currency": "THB",
"reference_id": "ORD888888889",
"bank_code": "014",
"account_number": "1234567890",
"account_holder_name": "John Doe",
"webhook_url": "https://example.com/path/to/your/webhook"
}Response
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"reference_id": "ORD888888889",
"type": "withdraw",
"status": "processing",
"amount": 5000.00,
"currency": "THB",
"created_at": "2025-12-24T11:00:00Z"
}Get Payment
Retrieve payment details by payment ID.
GET /v1/payments/{id}
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Payment ID (UUID format) |
Example Response
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"reference_id": "ORDER-2024-001",
"type": "deposit",
"status": "completed",
"amount": 1500.00,
"currency": "THB",
"qr_code": "00020101021129370016A000000677010111...",
"created_at": "2024-12-12T10:30:00Z",
"expires_at": "2024-12-12T10:45:00Z"
}Cancel Payment
Cancel a pending payment. Only payments with status pending can be cancelled.
POST /v1/payments/{id}/cancel
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Payment ID to cancel |
Example Response
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"reference_id": "ORDER-2024-001",
"type": "deposit",
"status": "cancelled",
"amount": 1500.00,
"currency": "THB",
"created_at": "2024-12-12T10:30:00Z"
}Payment Statuses
| Status | Description |
|---|---|
pending | Payment created, awaiting customer action |
processing | Payment is being processed |
completed | Payment successfully completed |
failed | Payment failed (includes expired transactions) |
cancelled | Payment was cancelled |
refunded | Payment was refunded |
Note
When a payment expires (customer doesn't pay within the time limit), the status changes to failed with a failure reason of "Transaction expired". A webhook is sent to notify your system.
Webhooks
When you provide a webhook_url, we'll send POST requests with payment updates.
Webhook Payload
The webhook payload is sent directly (not wrapped in an event structure):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "deposit",
"amount": 1500.00,
"fee": 37.50,
"netAmount": 1462.50,
"bankAccountNo": "1234567890",
"bankAccountName": "SOMCHAI JAIDEE",
"bankCode": "014",
"bankName": "ธนาคารไทยพาณิชย์",
"currency": "THB",
"referenceNo": "ORDER-2024-001",
"status": "success",
"timestamp": "2024-12-12T10:35:00Z",
"paidAt": "2024-12-12T10:34:55.123456789Z"
}Webhook Payload Fields
| Field | Type | Description |
|---|---|---|
id | string | Transaction ID |
type | string | Transaction type: deposit or withdraw |
amount | number | Original payment amount |
fee | number | Processing fee charged |
netAmount | number | Amount after fee deduction |
bankAccountNo | string | Bank account number (for deposits with payer info) |
bankAccountName | string | Bank account holder name |
bankCode | string | Bank code |
bankName | string | Bank name in Thai |
currency | string | Currency code |
referenceNo | string | Your reference ID |
status | string | success, failed, or expired |
timestamp | string | Webhook sent timestamp (ISO 8601) |
paidAt | string | Payment completion time (empty if not paid) |
Webhook Status Values
| Status | Description |
|---|---|
success | Payment completed successfully |
failed | Payment failed or expired |
expired | Payment expired (legacy, now uses failed) |
Webhook Retry Policy
- Webhooks are retried up to 5 times if delivery fails
- Retry interval: 1 minute between attempts
- A successful delivery is any 2xx HTTP response
- After 5 failed attempts, the webhook is marked as failed
Webhook Verification
Webhook requests include headers for verification:
| Header | Description |
|---|---|
X-Webhook-Signature | HMAC-SHA256 signature of the request body |
X-Webhook-Timestamp | Unix timestamp of the webhook |
Content-Type | application/json |
Verify webhooks using the same HMAC method as API requests:
import hmac
import hashlib
def verify_webhook(body, signature, timestamp, secret_key):
expected = hmac.new(
secret_key.encode(),
f"{timestamp}|{body}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)Error Responses
All errors follow this format:
{
"code": "ERROR_CODE",
"message": "Human-readable error message"
}HTTP Status Codes
| Status | Description |
|---|---|
200 | Success |
201 | Created successfully |
400 | Bad request - invalid parameters |
401 | Unauthorized - invalid API key or signature |
403 | Forbidden - merchant inactive or insufficient permissions |
404 | Not found |
409 | Conflict - duplicate reference_id |
500 | Internal server error |
504 | Gateway timeout |
Error Codes
| Code | Description |
|---|---|
INVALID_REQUEST | Request body validation failed |
MISSING_API_KEY | X-API-Key header is missing |
MISSING_SIGNATURE | X-Signature header is missing |
MISSING_TIMESTAMP | X-Timestamp header is missing |
INVALID_API_KEY | API key is invalid |
INVALID_SIGNATURE | Signature verification failed |
INVALID_TIMESTAMP | Timestamp format is invalid |
TIMESTAMP_EXPIRED | Request timestamp is outside valid window |
MERCHANT_INACTIVE | Merchant account is inactive |
PAYMENT_NOT_FOUND | Payment not found |
PAYMENT_ERROR | General payment processing error |
DUPLICATE_REFERENCE | Reference ID already exists |
INSUFFICIENT_BALANCE | Insufficient balance for withdrawal |
INVALID_BANK_CODE | Invalid bank code |
INVALID_ACCOUNT | Invalid bank account (sandbox) |
PAYMENT_CANNOT_BE_CANCELLED | Payment cannot be cancelled |
TIMEOUT | Request timed out |
Sandbox Testing
How Sandbox Works
Sandbox mode is determined by your merchant type, not a separate URL. Both sandbox and production merchants use the same API endpoint:
Base URL: https://gateway.facaiqq.com/v1
- Sandbox Merchants: Transactions stored in sandbox database. No real money is processed.
- Production Merchants: Transactions stored in production database and processed through payment providers.
Contact support to request sandbox credentials for testing.
Test Amounts
In sandbox mode, specific amounts trigger different behaviors:
| Amount | Currency | Behavior | Description |
|---|---|---|---|
100.00 | THB | auto_approve | Payment completes immediately |
200.00 | THB | auto_pending | Payment stays pending (simulates delayed) |
500.00 | THB | auto_approve | Payment completes immediately |
888.88 | THB | timeout | Payment times out |
999.99 | THB | auto_fail | Payment fails immediately |
1000.00 | THB | auto_approve | Payment completes immediately |
| Other | THB | pending | Normal payment flow |
Test Scenarios
You can override the behavior using the X-Sandbox-Scenario header:
| Scenario | Description |
|---|---|
auto_approve | Force payment to complete immediately |
auto_fail | Force payment to fail |
auto_pending | Force payment to stay pending |
timeout | Simulate a timeout |
invalid_account | Simulate invalid bank account (withdraw only) |
Example:
curl -X POST https://gateway.facaiqq.com/v1/payments/deposit \
-H "X-Sandbox-Scenario: auto_approve" \
-H "X-API-Key: your_sandbox_api_key" \
-H "X-Signature: ..." \
-H "X-Timestamp: ..." \
-H "Content-Type: application/json" \
-d '{"amount": 500.00, "currency": "THB", "reference_id": "TEST-001"}'Response Headers
Responses include a header indicating the environment:
| Header | Value | Description |
|---|---|---|
X-Environment | sandbox or production | Indicates the merchant type |
Transaction Expiry
- Deposit transactions have a default expiry of 15 minutes
- If the customer doesn't pay within this time:
- Status changes to
failed(notexpired) failure_reasonis set to "Transaction expired"- Webhook is sent with
status: "failed"
- Status changes to
- Expiry check runs every 1 minute
Rate Limits
| Endpoint | Rate Limit |
|---|---|
| All endpoints | 100 requests/min |
| Create Deposit | 30 requests/min |
| Create Withdraw | 20 requests/min |
Rate limit headers are included in responses:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per window |
X-RateLimit-Remaining | Remaining requests in window |
X-RateLimit-Reset | Unix timestamp when limit resets |
Support
For technical support:
- Email: [email protected]
- Documentation: https://docs.facaiqq.com
- Status Page: https://gateway.facaiqq.com