Basic Conventions
Current Version
The current version is 1.0.0.
Basic Conventions
- The request and response content type is
application/json. - All public APIs use
POST. - Field names use lowercase with underscores.
- Signing algorithm: HMAC-SHA256 over JCS (RFC 8785) canonical JSON. See Signature.
- Amount fields use strings. Submit them with two decimals.
- Country codes use ISO2 in lowercase.
- Currency codes use uppercase.
- Merchant order numbers must be unique on the merchant side.
Time / Timezone Handling
Format Contract
- All time fields (
ctm/utm/ any other timestamp) are returned as ISO 8601 strings with an explicit timezone offset, e.g."2026-10-08T03:51:29+08:00" - Records are always persisted against UTC+8, so the suffix is always
+08:00. Consumers MUST NOT interpret this string as if it were in their own local clock. - Consumers MUST parse the value through a standard RFC 3339 / ISO 8601 parser that honors the offset. The following patterns are STRICTLY FORBIDDEN:
- ❌
s[:19]/Substring(0, 19)— drop the suffix by hand - ❌
SimpleDateFormat("yyyy-MM-dd HH:mm:ss")— bare pattern with no timezone - ❌
new Date(s.substring(0,10) + "T" + ... )— rebuild the string by hand then parse
- ❌
- When you need to render timestamps in the local wall clock of a specific currency, parse to an absolute instant first, then re-format with that currency's
tz_offsetfrom the table below.
Currency ↔ UTC Offset Table
Use these values directly as UTC+tz_offset for formatting:
| Currency | Code | UTC offset (hours) |
|---|---|---|
| Philippine Peso | PHP | 8 |
| Indian Rupee | INR | 5.5 |
| Bangladeshi Taka | BDT | 6 |
| Indonesian Rupiah | IDR | 7 |
| Pakistan Rupee | PKR | 5 |
| Thai Baht | THB | 7 |
| Vietnamese Dong | VND | 7 |
Code Examples by Language
Parse the API timestamp correctly and re-format it in the target currency's local wall clock:
package main
import (
"fmt"
"time"
)
// TZ_OFFSET:按币种查表(见 /snippets/currency-tz-table.zh.md)
// PHP=8, INR=5.5, BDT=6, IDR=7, PKR=5, THB=7, VND=7
var curTzOffset = map[string]float64{
"PHP": 8, "INR": 5.5, "BDT": 6, "IDR": 7,
"PKR": 5, "THB": 7, "VND": 7,
}
// ParseApiTime:API 返回永远是 RFC3339 +08:00(如 "2026-10-08T20:49:30+08:00"),
// 必须用 time.RFC3339 解析,不要用字符串截取。
func ParseApiTime(s string) (time.Time, error) {
return time.Parse(time.RFC3339, s)
}
// FormatLocalTime:把 API 时间按"币种当地时区"格式化显示
// 例:BDT cur, "2026-10-08T20:49:30+08:00" -> "2026-10-08 18:49:30"
func FormatLocalTime(apiTime, cur, layout string) (string, error) {
t, err := ParseApiTime(apiTime)
if err != nil {
return "", err
}
off, ok := curTzOffset[cur]
if !ok {
off = 8 // 兜底:东八区
}
// 把绝对时间点 + 目标时区偏移,重建一个带自定义 offset 的 Location
loc := time.FixedZone(fmt.Sprintf("UTC%+g", off), int(off*3600))
return t.In(loc).Format(layout), nil
}
func main() {
out, _ := FormatLocalTime("2026-10-08T20:49:30+08:00", "BDT", "2006-01-02 15:04:05")
fmt.Println(out) // 2026-10-08 18:49:30
}from datetime import datetime, timezone, timedelta
from zoneinfo import ZoneInfo # Python 3.9+
CUR_TZ_OFFSET = {
"PHP": 8, "INR": 5.5, "BDT": 6, "IDR": 7,
"PKR": 5, "THB": 7, "VND": 7,
}
def parse_api_time(s: str) -> datetime:
"""永远按 RFC 3339 解析;严禁 s[:19] 截断时区。"""
# datetime.fromisoformat 在 3.11+ 原生支持 RFC3339; 旧版本用下面替代
return datetime.fromisoformat(s.replace("Z", "+00:00"))
def format_local_time(api_time: str, cur: str, layout: str = "%Y-%m-%d %H:%M:%S") -> str:
"""
把 API 返回的 +08:00 绝对时间点,按币种当地时区格式化。
例:BDT -> "2026-10-08T20:49:30+08:00" → "2026-10-08 18:49:30"
"""
t = parse_api_time(api_time) # aware datetime(带 offset)
off = CUR_TZ_OFFSET.get(cur, 8)
# 方法 A(推荐,利用 IANA tzdata):
cur_zone = timezone(timedelta(hours=off))
return t.astimezone(cur_zone).strftime(layout)
# 方法 B(已知具体地区):
# INR -> ZoneInfo("Asia/Kolkata"),直接 astimezone(ZoneInfo("Asia/Kolkata"))
if __name__ == "__main__":
print(format_local_time("2026-10-08T20:49:30+08:00", "BDT"))/**
* API ctm/utm 永远是 RFC3339 +08:00 字符串(例: "2026-10-08T20:49:30+08:00")。
* 直接 new Date(s) 即可,Date 内部保存"绝对毫秒",天然带时区语义。
* 严禁:s.slice(0,19) 再 new Date()(会丢时区,解析成你本地时区)。
*/
const CUR_TZ_OFFSET = {
PHP: 8, INR: 5.5, BDT: 6, IDR: 7,
PKR: 5, THB: 7, VND: 7,
}
function formatLocalTime(apiTime, cur, fmt = 'YYYY-MM-DD HH:mm:ss') {
const t = new Date(apiTime) // 读入 +08:00 → 内部为 UTC epoch ms
if (Number.isNaN(t.getTime())) return '--'
const off = CUR_TZ_OFFSET[cur] ?? 8
// 技巧:epoch ms + (off * 3600 * 1000) 后用 getUTC*() 系列,
// 可以得到"目标时区的墙上年月日时分秒"(避免依赖 Intl 的复杂行为)
const wall = new Date(t.getTime() + off * 3_600_000)
const pad = (n) => String(n).padStart(2, '0')
const parts = {
YYYY: wall.getUTCFullYear(),
MM: pad(wall.getUTCMonth() + 1),
DD: pad(wall.getUTCDate()),
HH: pad(wall.getUTCHours()),
mm: pad(wall.getUTCMinutes()),
ss: pad(wall.getUTCSeconds()),
}
return fmt.replace(/YYYY|MM|DD|HH|mm|ss/g, (m) => parts[m])
}
// == 或者用原生 Intl(支持夏令时、直接按 IANA 名写更好)
function formatLocalTimeIntl(apiTime, cur, locale = 'en-GB') {
const off = CUR_TZ_OFFSET[cur] ?? 8
// 把 UTC+off 写成 Etc/GMT±H(注意符号反向,例如 UTC+8 = Etc/GMT-8)
const sign = off >= 0 ? '-' : '+'
const tzName = `Etc/GMT${sign}${Math.abs(off)}`
const formatter = new Intl.DateTimeFormat(locale, {
timeZone: tzName,
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false,
})
return formatter.format(new Date(apiTime))
}
console.log(formatLocalTime('2026-10-08T20:49:30+08:00', 'BDT'))
// 2026-10-08 18:49:30<?php
/**
* 注意要点:
* 1) API 的 ctm 永远是 RFC3339 字符串(例 "2026-10-08T20:49:30+08:00"),
* 必须用 DateTime::RFC3339 或 DATE_RFC3339 常量解析。
* 2) 不要做 substr($s, 0, 19) 截断,丢时区会错。
* 3) PHP 的 \DateTimeZone 接受 "UTC", "Asia/Dhaka", "Etc/GMT-6" 等 IANA 名。
*/
$CUR_TZ_OFFSET = [
'PHP' => 8, 'INR' => 5.5, 'BDT' => 6, 'IDR' => 7,
'PKR' => 5, 'THB' => 7, 'VND' => 7,
];
function curTimeZone(float $off): \DateTimeZone {
// 用 Etc/GMT±N(符号反向)表达固定偏移;知道 IANA 名直接填更好:
// INR -> new \DateTimeZone("Asia/Kolkata"), BDT -> "Asia/Dhaka"...
$sign = $off >= 0 ? '-' : '+';
$abs = abs($off);
$h = (int)$abs;
$m = ($abs - $h) * 60;
if ($m == 0) {
return new \DateTimeZone("Etc/GMT{$sign}{$h}");
}
// 半时区(如 INR 5.5)自己构造一个带偏移秒的 named 区,PHP 支持 "UTC+H:i"
$sign2 = $off >= 0 ? '+' : '-';
$name = sprintf("UTC%s%02d:%02d", $sign2, $h, (int)$m);
return new \DateTimeZone($name);
}
function formatLocalTime(string $apiTime, string $cur, string $fmt = 'Y-m-d H:i:s'): string {
global $CUR_TZ_OFFSET;
$t = \DateTimeImmutable::createFromFormat(\DateTimeInterface::RFC3339, $apiTime);
if ($t === false) return '--';
$off = $CUR_TZ_OFFSET[$cur] ?? 8;
return $t->setTimezone(curTimeZone($off))->format($fmt);
}
echo formatLocalTime("2026-10-08T20:49:30+08:00", "BDT") . "\n";
// 2026-10-08 18:49:30import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Map;
public final class ApiTimeUtil {
private static final Map<String, Double> CUR_TZ_OFFSET = Map.of(
"PHP", 8.0, "INR", 5.5, "BDT", 6.0, "IDR", 7.0,
"PKR", 5.0, "THB", 7.0, "VND", 7.0
);
/**
* 解析 API 返回的 RFC3339 时间。
* 禁止用 java.text.SimpleDateFormat + "yyyy-MM-dd HH:mm:ss" 裸解析(丢时区)。
*/
public static OffsetDateTime parseApiTime(String s) {
return OffsetDateTime.parse(s); // 默认 DateTimeFormatter.ISO_OFFSET_DATE_TIME
}
/**
* 按币种当地时区格式化。
* 例:BDT, "2026-10-08T20:49:30+08:00" -> "2026-10-08 18:49:30"
*/
public static String formatLocalTime(String apiTime, String cur, String pattern) {
OffsetDateTime t = parseApiTime(apiTime);
double off = CUR_TZ_OFFSET.getOrDefault(cur, 8.0);
// 把 8h5.5h 拆成小时(整数)+分钟
int totalSec = (int) Math.round(off * 3600);
ZoneId zone = ZoneOffset.ofTotalSeconds(totalSec);
// 知道 IANA 区名推荐直接用(带夏令时):
// INR -> ZoneId.of("Asia/Kolkata"), BDT -> ZoneId.of("Asia/Dhaka")
return t.atZoneSameInstant(zone)
.format(DateTimeFormatter.ofPattern(pattern));
}
public static void main(String[] args) {
System.out.println(formatLocalTime(
"2026-10-08T20:49:30+08:00",
"BDT",
"yyyy-MM-dd HH:mm:ss"
));
}
}using System;
using System.Collections.Generic;
using System.Globalization;
public static class ApiTimeUtil
{
private static readonly IReadOnlyDictionary<string, double> CurTzOffset =
new Dictionary<string, double>
{
["PHP"] = 8, ["INR"] = 5.5, ["BDT"] = 6, ["IDR"] = 7,
["PKR"] = 5, ["THB"] = 7, ["VND"] = 7,
};
/// <summary>
/// API 返回格式为 RFC3339/ISO8601 带偏移(例 "2026-10-08T20:49:30+08:00")。
/// 必须用 DateTimeStyles.RoundtripKind / AssumeUniversal 的严格方式解析,
/// 禁止 "yyyy-MM-dd HH:mm:ss" 裸匹配。
/// </summary>
public static DateTimeOffset ParseApiTime(string s) =>
DateTimeOffset.ParseExact(
s,
new[] { "yyyy-MM-ddTHH:mm:ssK", "yyyy-MM-ddTHH:mm:ss.fffK" },
CultureInfo.InvariantCulture,
DateTimeStyles.RoundtripKind);
public static string FormatLocalTime(string apiTime, string cur, string fmt = "yyyy-MM-dd HH:mm:ss")
{
var t = ParseApiTime(apiTime);
double off = CurTzOffset.TryGetValue(cur, out var o) ? o : 8.0;
var ts = TimeSpan.FromHours(off);
// .NET 6+: 直接 TimeZoneInfo.CreateCustomTimeZone("UTC+X", ts, ...)
var local = t.ToOffset(ts);
return local.ToString(fmt, CultureInfo.InvariantCulture);
}
public static void Main()
{
Console.WriteLine(FormatLocalTime("2026-10-08T20:49:30+08:00", "BDT"));
// 2026-10-08 18:49:30
}
}⚠️ Half-hour offsets (e.g.
INR = UTC+5:30) MUST NOT be rounded to a whole hour. All languages shown support fractional hours or minute-based total-second offsets.
Response Format
A response is treated as an error when the HTTP status code is NOT 200 AND the response field code is greater than 0.
Error response example:
{
"code": 506800,
"message": "Merchant information error"
}Normal single-record responses usually return data:
{
"code": 0,
"data": {}
}List responses usually return list:
{
"code": 0,
"list": []
}message is returned in English by default. To receive Chinese messages, set:
Accept-Language: zh-CNTo keep English messages, leave it unset or set:
Accept-Language: en-USThe normal HTTP status is 200.
Order-Failure Judgment Under Anomalies
Only after you successfully receive an HTTP status 200 and a valid JSON body can you determine whether an order was placed according to the rules above.
Due to network jitter, packet loss, CDN false blocks, DNS instability, or similar issues, you may not receive a normal response. Examples include timeouts, empty bodies, or malformed (non-standard JSON) payloads.
These cases DO NOT mean the order has failed.
When this happens, you must call the deposit / withdraw query endpoint with the tno parameter to verify the actual order status.
Supported Currencies
| Code | Symbol | 含义 | Meaning | Country / Region |
|---|---|---|---|---|
| PHP | ₱ | 菲律宾比索 | Philippine Peso | PH |
| PKR | Rs | 巴基斯坦卢比 | Pakistan Rupee | PK |
| BDT | Tk | 孟加拉塔卡 | Bangladeshi Taka | BD |
| IDR | Rp | 印度尼西亚卢比 | Indonesian Rupiah | ID |
| VND | ₫ | 越南盾 | Vietnamese Dong | VN |
| THB | ฿ | 泰铢 | Thai Baht | TH |
| JPY | ¥ | 日元 | Japanese Yen | JP |
| INR | ₹ | 印度卢比 | Indian Rupee | IN |
Error Codes
| Code | 含义 | Meaning |
|---|---|---|
| 506413 | 参数错误 | Invalid parameters |
| 506800 | 商户号错误 | Invalid merchant code |
| 506801 | 商户密钥空 | Merchant key is empty |
| 506802 | 签名错误 | Invalid signature |
| 506803 | 渠道获取错误 | Failed to get channel |
| 506804 | 无渠道分配 | No channel assigned |
| 506805 | 商户单号重复 | Duplicate merchant order number |
| 506806 | 渠道获取失败 | Channel fetch failed |
| 506807 | 渠道无效 | Invalid channel |
| 506808 | 金额参数不对 | Invalid amount |
| 506809 | 查询记录不存在 | Record not found |
| 506810 | 无分组信息 | No group information |
| 506812 | 上游通道异常 | Upstream channel exception |
| 506813 | 商户通道已被后台清空 | Merchant channel was cleared by backend |
| 506815 | 上游通道返回错误 | Upstream channel returned an error |
| 506817 | 货币类型错误 | Invalid currency type |
| 506818 | 提交金额超限 | Amount exceeds the allowed limit |
| 506900 | 请求 IP 不在白名单中 | Request IP is not in the whitelist |
| 563602 | 银行代码和渠道代码不匹配 | Bank code and channel code do not match |
| 563201 | 余额不足 | Insufficient balance |
See Async Callback for the notification payload, parameters and required response.
Special Notes
Precision Length
The system keeps four decimal places internally, while external display usually keeps two.
Rounding Rule
The system uses banker's rounding, also known as round half to even.
Examples:
1.234becomes1.231.236becomes1.241.245becomes1.241.255becomes1.26
Calculation Notes
When fee splitting is involved, the system calculates each order independently. In rare cases, different rounding steps may produce a display difference of 0.01. Reconciliation should follow the order details and settlement data.