Skip to content

Public API Documentation ​

Version: 1.1.0 Last Updated: 2025-12-24

Environments ​

EnvironmentBase URLDescription
Productionhttps://gateway.facaiqq.com/v1Real transactions, live payments
Sandboxhttps://gateway.facaiqq.com/v1Test 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:

HeaderRequiredDescription
X-API-KeyYesYour merchant API key (64 characters)
X-SignatureYesHMAC-SHA256 signature of the request
X-TimestampYesUnix timestamp (seconds since epoch)
Content-TypeYesapplication/json

Signature Generation ​

The signature is computed using HMAC-SHA256 with your Secret Key:

Signature = HMAC-SHA256(SignatureData, SecretKey)
SignatureData = METHOD|PATH|TIMESTAMP|BODY

Example (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 ​

python
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())
javascript
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
<?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);
go
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 ​

FieldTypeRequiredDescription
amountnumberYesPayment amount (> 0)
currencystringYesCurrency code (3 characters, e.g., "THB")
reference_idstringYesYour unique order reference (1-64 characters)
customer_emailstringNoCustomer email for notifications
customer_phonestringNoCustomer phone number (max 20 characters)
descriptionstringNoPayment description (max 255 characters)
webhook_urlstringNoWebhook URL for payment notifications
bank_codestringNoPayer's bank code (3 digits) for reference
bank_account_nostringNoPayer's bank account number (max 20 characters)
bank_account_namestringNoPayer's bank account holder name (max 100 characters)

Example Request ​

json
{
  "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.

json
{
  "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.

json
{
  "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 ​

FieldTypeDescription
idstringUnique transaction ID (UUID format)
reference_idstringYour order reference
typestringTransaction type (deposit)
statusstringPayment status
amountnumberPayment amount
currencystringCurrency code
company_accountbooleanCompany 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_codestringCompany account bank code (3 digits) (only present when company_account = true)
company_account_bank_namestringCompany account bank name (only present when company_account = true)
company_account_namestringCompany account holder name (only present when company_account = true)
company_account_nostringCompany account number (only present when company_account = true)
qr_codestringPromptPay QR code string (only returned when company_account = false)
qr_code_imagestringBase64 encoded QR code image, PNG (only returned when company_account = false)
created_atstringCreation timestamp (ISO 8601)
expires_atstringExpiration timestamp (ISO 8601, 15 min default)

Create Withdraw ​

Create a new withdrawal/payout request to a bank account.

POST /v1/payments/withdraw

Request Body ​

FieldTypeRequiredDescription
amountnumberYesWithdrawal amount (> 0)
currencystringYesCurrency code (3 characters)
reference_idstringYesYour unique reference (1-64 characters)
bank_codestringYesBank code (3 digits, see table below)
account_numberstringYesBank account number (10-20 digits)
account_holder_namestringYesAccount holder name (1-100 characters)
descriptionstringNoTransfer description (max 255 characters)
webhook_urlstringNoWebhook URL for transfer notifications

Bank Codes ​

CodeThai NameEnglish 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 LengthProxy TypeDescription
10 digitsMOBILEMobile phone number
13 digitsTXIDNational ID or Tax ID

Example Request (PromptPay with Mobile):

json
{
  "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):

json
{
  "amount": 5000.00,
  "currency": "THB",
  "reference_id": "ORD888888891",
  "bank_code": "PROMPTPAY",
  "account_number": "1234567890123",
  "account_holder_name": "John Doe"
}

Example Request ​

json
{
  "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 ​

json
{
  "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 ​

ParameterTypeDescription
idstringPayment ID (UUID format)

Example Response ​

json
{
  "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 ​

ParameterTypeDescription
idstringPayment ID to cancel

Example Response ​

json
{
  "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 ​

StatusDescription
pendingPayment created, awaiting customer action
processingPayment is being processed
completedPayment successfully completed
failedPayment failed (includes expired transactions)
cancelledPayment was cancelled
refundedPayment 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):

json
{
  "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 ​

FieldTypeDescription
idstringTransaction ID
typestringTransaction type: deposit or withdraw
amountnumberOriginal payment amount
feenumberProcessing fee charged
netAmountnumberAmount after fee deduction
bankAccountNostringBank account number (for deposits with payer info)
bankAccountNamestringBank account holder name
bankCodestringBank code
bankNamestringBank name in Thai
currencystringCurrency code
referenceNostringYour reference ID
statusstringsuccess, failed, or expired
timestampstringWebhook sent timestamp (ISO 8601)
paidAtstringPayment completion time (empty if not paid)

Webhook Status Values ​

StatusDescription
successPayment completed successfully
failedPayment failed or expired
expiredPayment 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:

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature of the request body
X-Webhook-TimestampUnix timestamp of the webhook
Content-Typeapplication/json

Verify webhooks using the same HMAC method as API requests:

python
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:

json
{
  "code": "ERROR_CODE",
  "message": "Human-readable error message"
}

HTTP Status Codes ​

StatusDescription
200Success
201Created successfully
400Bad request - invalid parameters
401Unauthorized - invalid API key or signature
403Forbidden - merchant inactive or insufficient permissions
404Not found
409Conflict - duplicate reference_id
500Internal server error
504Gateway timeout

Error Codes ​

CodeDescription
INVALID_REQUESTRequest body validation failed
MISSING_API_KEYX-API-Key header is missing
MISSING_SIGNATUREX-Signature header is missing
MISSING_TIMESTAMPX-Timestamp header is missing
INVALID_API_KEYAPI key is invalid
INVALID_SIGNATURESignature verification failed
INVALID_TIMESTAMPTimestamp format is invalid
TIMESTAMP_EXPIREDRequest timestamp is outside valid window
MERCHANT_INACTIVEMerchant account is inactive
PAYMENT_NOT_FOUNDPayment not found
PAYMENT_ERRORGeneral payment processing error
DUPLICATE_REFERENCEReference ID already exists
INSUFFICIENT_BALANCEInsufficient balance for withdrawal
INVALID_BANK_CODEInvalid bank code
INVALID_ACCOUNTInvalid bank account (sandbox)
PAYMENT_CANNOT_BE_CANCELLEDPayment cannot be cancelled
TIMEOUTRequest 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:

AmountCurrencyBehaviorDescription
100.00THBauto_approvePayment completes immediately
200.00THBauto_pendingPayment stays pending (simulates delayed)
500.00THBauto_approvePayment completes immediately
888.88THBtimeoutPayment times out
999.99THBauto_failPayment fails immediately
1000.00THBauto_approvePayment completes immediately
OtherTHBpendingNormal payment flow

Test Scenarios ​

You can override the behavior using the X-Sandbox-Scenario header:

ScenarioDescription
auto_approveForce payment to complete immediately
auto_failForce payment to fail
auto_pendingForce payment to stay pending
timeoutSimulate a timeout
invalid_accountSimulate invalid bank account (withdraw only)

Example:

bash
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:

HeaderValueDescription
X-Environmentsandbox or productionIndicates 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 (not expired)
    • failure_reason is set to "Transaction expired"
    • Webhook is sent with status: "failed"
  • Expiry check runs every 1 minute

Rate Limits ​

EndpointRate Limit
All endpoints100 requests/min
Create Deposit30 requests/min
Create Withdraw20 requests/min

Rate limit headers are included in responses:

HeaderDescription
X-RateLimit-LimitMaximum requests per window
X-RateLimit-RemainingRemaining requests in window
X-RateLimit-ResetUnix timestamp when limit resets

Support ​

For technical support:

FacaiQQ Payment Gateway