公共约定
当前版本
当前版本是 1.0.0。
约定
- 数据请求/响应类型统一为
application/json - 以下对外接口统一使用
POST - 字段名统一使用小写加下划线
- 本系统签名算法为 HMAC-SHA256 + JCS (RFC 8785) JSON 规范化,详见 签名
- 金额字段统一为字符串,提交时按两位小数处理
- 国家代码使用 ISO2,两位小写
- 货币代码使用大写
- 商户订单号请在商户侧自行保证唯一性
时间 / 时差处理
格式约定
- 时间字段(
ctm/utm/ 其它带时间含义字段)统一为 ISO 8601 带时区字符串,示例:"2026-10-08T03:51:29+08:00" - 数据库以东八区写入,尾缀恒为
+08:00;接入方不得把它当"本地时间"处理。 - 接入方必须使用标准 RFC 3339 / ISO 8601 解析器读取时区信息后再做本地化显示,严禁以下错误写法:
- ❌
s[:19]或Substring(0, 19)直接截掉时区后缀 - ❌ 使用
SimpleDateFormat("yyyy-MM-dd HH:mm:ss")(裸格式、不带时区语义) - ❌
new Date(s.substring(0,10) + "T" + ... )手动拼装再解析
- ❌
- 如果需要按"币种当地时区"展示,请先解析得到绝对时间点,再按本页下方「币种偏移表」查出对应
tz_offset重新格式化。
币种时区偏移表
以下值可直接作为 UTC+tz_offset 使用:
| 币种 | 代码 | UTC 偏移(小时) |
|---|---|---|
| 菲律宾比索 | PHP | 8 |
| 印度卢比 | INR | 5.5 |
| 孟加拉塔卡 | BDT | 6 |
| 印度尼西亚卢比 | IDR | 7 |
| 巴基斯坦卢比 | PKR | 5 |
| 泰铢 | THB | 7 |
| 越南盾 | VND | 7 |
多语言示例代码
不同语言正确解析 API 返回时间,并按币种当地时区格式化:
Go
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
}Python
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"))Node.js
/**
* 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:30PHP
<?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:30Java
import 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"
));
}
}C
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
}
}⚠️ 半时区币种(如
INR = UTC+5:30)不能写成整小时;PHP/Go/Python 的 offset 均支持浮点数或分钟级totalSeconds。
数据响应格式
http 状态码不是 200, 返回参数 code 大于 0 时,为错误响应。
错误响应示例:
{
"code": 506800,
"message": "Merchant code error"
}正常返回通常为单条 data:
{
"code": 0,
"data": {}
}列表返回通常为 list:
{
"code": 0,
"list": []
}正常情况 HTTP 状态为 200。
特殊情况下的"下单失败判定"
只有当您成功获得 http 状态以及正确的 JSON 数据之后,才能依照上述说明对是否下单进行判断。
由于网络波动、丢包、CDN 误封、DNS 不稳定等因素,可能导致未能得到正常结果(例如超时、空响应、非标准 JSON 的异常响应体)。 这不代表「下单失败」。
此种情况发生时,您应当使用存款/出款查询接口,传递 tno 进行查询,从而确定下单实际状态。
货币种类
| 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 |
错误代码
| 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 |
异步通知结构、参数和返回要求见 异步回调。
特别说明
精度长度
系统内部保留四位小数,对外显示通常保留两位小数。
精度算法
采用银行家舍入法,也即「四舍六入五成双」。
示例:
1.234保留两位为1.231.236保留两位为1.241.245保留两位为1.241.255保留两位为1.26
数据计算说明
订单涉及费率拆分时,系统按单笔订单独立计算,因此在极少数情况下,因不同环节的舍入方式不同,可能出现 0.01 的展示误差。对账时请以订单明细和结算数据为准。