商户 API 对接手册
适用对象:接入本平台代收(payin)与代付(payout)能力的商户技术团队。
版本 v1.2 · 2026-09-08 · 新增通道列表接口与 10006 错误码
环境地址、app_id、api_key、api_secret 由平台运营在开通时单独提供,不在本文档内。可用的 channel_code 与币种请用第 7 节的通道列表接口自行查询。
01 接口总览
| 方法 | 路径 | 用途 |
|---|---|---|
POST | /api/v1/payin/create | 创建收款订单 |
GET | /api/v1/payin/{payin_order_no} | 查询收款订单 |
POST | /api/v1/payout/create | 创建出款订单 |
GET | /api/v1/payout/{payout_order_no} | 查询出款订单 |
GET | /api/v1/balance | 查询商户余额 |
GET | /api/v1/channel/list | 查询本商户已开通的通道 |
所有接口均需按第 2 节的签名规范鉴权。查询接口是订单最终状态的权威来源;异步通知可能重复、延迟或耗尽重试,收到通知后仍建议查单核对。
02 签名与鉴权
2.1 请求头
每个请求必须携带以下 5 个 Header:
| Header | 说明 |
|---|---|
X-App-Id | 平台分配的应用 ID |
X-Api-Key | 平台分配的 API Key |
X-Timestamp | Unix 秒级时间戳,与服务器时间差不得超过 300 秒 |
X-Nonce | 随机字符串,单次使用,短期内重复会被拒绝 |
X-Sign | 按 2.2 计算的签名,小写十六进制 |
2.2 签名算法
算法:HMAC-SHA256,密钥为 api_secret,结果取小写 hex。待签名串固定为 8 段,以换行符 \n 连接。某段为空时该段留空,但分隔符不可省略:
规范化规则:
METHOD大写(如POST、GET)。PATH为实际请求路径,含路径参数的真实值(如/api/v1/payin/PO20260829000123),不是路由模板。QUERY为规范化后的查询串:参数按键升序、同键多值按值升序,键和值分别做百分号编码后以k=v用&连接;无查询参数时为空串。- 最后一段是原始请求体字节的 SHA-256 小写 hex;GET 等无 body 请求对空字节串取哈希,即
e3b0c442…7852b855。不要先反序列化再重新序列化 body,必须对实际发送的字节计算。
2.3 Python 签名示例
import hashlib, hmac, time, uuid, requests
from urllib.parse import urlencode
APP_ID = "your_app_id"
API_KEY = "your_api_key"
API_SECRET = "your_api_secret"
BASE = "https://<platform-host>"
def canonical_query(params: dict) -> str:
if not params:
return ""
pairs = []
for k in sorted(params):
vs = params[k] if isinstance(params[k], list) else [params[k]]
for v in sorted(map(str, vs)):
pairs.append((str(k), v))
# 必须使用 urlencode:它与平台的 URL QueryEscape 一致,空格编码为 +。
return urlencode(pairs)
def sign(method: str, path: str, query: dict, body: bytes, ts: str, nonce: str) -> str:
body_sha = hashlib.sha256(body).hexdigest()
source = "\n".join([
method.upper(), path, canonical_query(query),
APP_ID, API_KEY, ts, nonce, body_sha,
])
return hmac.new(API_SECRET.encode(), source.encode(), hashlib.sha256).hexdigest()
def request(method: str, path: str, query: dict | None = None, body: bytes = b""):
ts = str(int(time.time()))
nonce = uuid.uuid4().hex
headers = {
"X-App-Id": APP_ID,
"X-Api-Key": API_KEY,
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Sign": sign(method, path, query or {}, body, ts, nonce),
"Content-Type": "application/json",
}
url = BASE + path + (("?" + urlencode(query)) if query else "")
return requests.request(method, url, headers=headers, data=body, timeout=10)
2.4 注意事项
- 商户身份由平台从已验签的凭证解析,请求体中不接受、也无法传入商户号。
- 防重放存储不可用时平台会直接拒绝请求(返回服务不可用类错误码),请退避重试,勿降级绕过签名。
03 统一响应与错误码
所有接口返回统一信封:
{
"code": 0,
"message": "成功",
"data": { },
"trace_id": "…",
"timestamp": 1756450000
}
code = 0表示成功;失败时data省略。- 请按
code分支处理,不要解析message文案,文案会本地化、会调整。 - 联系平台排障时请提供
trace_id。
| code | HTTP | 含义 | 商户侧处理 |
|---|---|---|---|
| 10000 | 500 | 系统错误 | 平台内部异常;持续出现请联系平台 |
| 10001 | 400 | 参数错误 | 修正参数后重发;重试不会改变结果 |
| 10002 | 404 | 资源不存在 | 确认单号/商户号后重查 |
| 10003 | 409 | 重复/冲突 | 同一 merchant_request_id 已受理,改用查单接口取结果 |
| 10004 | 503 | 数据库错误 | 平台存储层不可用,退避后重试 |
| 10005 | 400 | 币种小数位不受支持 | amount 小数位需与该币种精度一致,平台不做四舍五入 |
| 10006 | 400 | 通道未开通 | 该 channel_code 未对本商户开通,联系平台开通后再下单;重发不会改变结果。用第 7 节接口确认可用通道 |
| 20001 | 401 | 未鉴权/签名验证失败 | 检查 app_id 与签名算法 |
| 20002 | 401 | 登录失效 | 管理后台场景,网关接口一般不会返回 |
| 20003 | 403 | 权限不足 | 管理后台场景,网关接口一般不会返回 |
| 20004 | 403 | 账户被冻结/商户不可用 | 联系平台 |
| 20005 | 403 | 账户被禁用 | 联系平台 |
| 20006 | 429 | 触发限流 | 退避后重试 |
04 金额、币种与幂等
- 金额一律为主单位十进制字符串,如
"100.00";小数位必须与币种精度一致(各币种精度由平台提供),多余小数或非法格式会被拒绝,平台不做四舍五入。 channel_code由商户显式指定,一个通道唯一对应一个方向和一个币种。- 创建幂等键为
merchant_request_id(商户维度唯一):- 完全相同的请求重发 → 返回首次创建的订单,不会产生第二笔;
- 同一
merchant_request_id修改了金额、通道、收款人等任一关键要素 → 返回10003冲突,不会创建新订单,此时应改用查单接口获取原订单结果; - 因此不要复用
merchant_request_id来换金额/换通道/换收款人;重新下单请换新的merchant_request_id。
05 代收 Payin
5.1 创建订单 POST /api/v1/payin/create
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
merchant_order_no | string(≤64) | 是 | 商户订单号 |
merchant_request_id | string(≤64) | 是 | 幂等键,见第 4 节 |
amount | string(≤32) | 是 | 主单位金额字符串,如 "100.00" |
currency | string | 是 | 币种代码 |
channel_code | string | 是 | 通道代码 |
product_name | string(≤128) | 否 | 商品名 |
notify_url | string(≤512) | 否 | 本单通知地址;为空则用商户默认收款通知地址 |
return_url | string(≤512) | 否 | 支付完成跳转地址 |
响应 data:
{
"url": "https://…收银台地址…",
"payin_order_no": "PO…",
"merchant_order_no": "…",
"amount": "100.00",
"currency": "PKR",
"channel_code": "…",
"pay_method": "…",
"status": 2,
"channel_order_no": "上游订单号,可能为 null",
"provider_pay_url": "上游支付链接,可能为 null",
"provider_qr_code": "上游二维码内容,可能为 null",
"fail_code": "",
"fail_reason": null
}
status = 2(PENDING):上游已受理,provider_pay_url 或 provider_qr_code 至少其一有值,引导付款人支付。
status = 5(FAIL):明确失败,fail_code 给出可编程原因;如需重试请更换 merchant_request_id 重新下单。
status = 1 或 3(INIT/PROCESSING,少数情况):上游响应慢或结果不确定,请轮询查单接口,或引导付款人访问 url 收银台兜底。
下单失败不影响信封语义:订单已受理即返回 code=0,失败通过 status 与 fail_code 表达。
5.2 查询订单 GET /api/v1/payin/{payin_order_no}
响应 data 字段:payin_order_no、merchant_no、merchant_order_no、merchant_request_id、status、amount、currency、channel_code、pay_method、channel_order_no、provider_pay_url、provider_qr_code(仅可继续支付状态返回)、notify_url、return_url、expire_time、success_time、fail_code、fail_reason、created_at。时间字段为 RFC3339 格式。
5.3 收款订单状态
| 值 | 状态 | 含义 |
|---|---|---|
| 1 | INIT | 已建单,尚未触达上游 |
| 2 | PENDING | 上游已受理,等待支付 |
| 3 | PROCESSING | 正在创建/确认中 |
| 4 | SUCCESS | 支付成功(终态,已入账) |
| 5 | FAIL | 失败(终态) |
| 6 | CLOSED | 超时/关闭(终态) |
| 7 | PENDING_VERIFY | 结果待人工核验;不要当作成功或失败处理,等待终态通知或轮询查单 |
06 代付 Payout
6.1 创建订单 POST /api/v1/payout/create
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
merchant_order_no | string(≤64) | 是 | 商户订单号 |
merchant_request_id | string(≤64) | 是 | 幂等键 |
amount | string(≤32) | 是 | 主单位金额字符串 |
currency | string | 是 | 币种代码 |
channel_code | string | 是 | 通道代码 |
receiver_name | string(≤128) | 是 | 收款人姓名 |
receiver_account | string(≤128) | 是 | 收款账号 |
receiver_bank_code | string(≤64) | 否 | 收款银行代码,部分通道必需,以通道说明为准 |
receiver_email | string(≤128) | 否 | 收款人邮箱,做格式校验 |
响应 data:
{ "payout_order_no": "PT…", "status": 1 }
下单成功即从商户可用余额冻结 订单金额 + 手续费;失败(终态 5)自动全额解冻;成功(终态 4)从冻结余额扣除。余额不足时下单整体失败,不产生订单。
6.2 查询订单 GET /api/v1/payout/{payout_order_no}
响应 data 字段:payout_order_no、merchant_order_no、amount、currency、status、status_text、fail_code、fail_reason、receiver_account(脱敏)、success_time、created_at。
6.3 出款订单状态
| 值 | 状态 | 含义 |
|---|---|---|
| 1 | INIT | 已建单并冻结资金 |
| 3 | PROCESSING | 出款处理中 |
| 4 | SUCCESS | 出款成功(终态,已结算) |
| 5 | FAIL | 失败(终态,已解冻) |
| 6 | CLOSED | 已关闭(终态) |
| 7 | PENDING_VERIFY | 结果不明,资金保持冻结,等待核验;不要当作失败重新下单 |
PENDING_VERIFY(7)期间平台不会通知成功或失败。若此时用新的 merchant_request_id 重新下单,一旦原单最终成功会造成重复出款。请等待原单进入终态。
07 通道列表 GET /api/v1/channel/list
返回本商户已开通的通道。下单用的 channel_code 从这里取;平台关闭某条通道后它会从列表中消失,用已消失的通道下单返回 10006。
查询参数:biz_type(1=代收,2=代付,缺省 1)、page(默认 1)、size(默认 20,最大 100;超过 100 按 100 处理,不报错)。
响应 data:page、size、total、list。list 每项包含:
| 字段 | 说明 |
|---|---|
channel_code | 通道代码,下单时按它指定通道 |
biz_type | 1=代收,2=代付 |
method_code / method_name | 支付方式代码与名称 |
currency_code | 该通道的币种,一条通道只对应一个币种 |
fee_rate | 平台对本商户在该通道上的费率,百分比字符串,如 "2.5000" |
fixed_fee | 固定手续费,最小货币单位;不收固定费时返回 0 |
单笔金额上下限不在本接口返回:它是上游的技术约束、会随上游调整而变,以下单接口的实时判定为准。建议在本地缓存一段时间后刷新,不要每笔订单调用一次。
08 余额查询 GET /api/v1/balance
无请求参数,身份来自签名凭证。响应 data:
{
"as_of": "2026-08-29T12:00:00Z",
"balances": [
{
"currency": "PKR",
"available_balance": "12345.00",
"frozen_balance": "100.00",
"total_balance": "12445.00"
}
]
}
金额均为主单位字符串。总余额 = 可用 + 冻结。
09 异步通知
订单进入可通知终态(SUCCESS/FAIL)后,平台向商户推送通知:
- 代收:优先使用下单时的
notify_url,为空则用商户默认收款通知地址; - 代付:使用商户默认出款通知地址;
- 地址未配置则跳过通知,请依赖查单。
9.1 通知报文
POST,JSON body:
{
"biz_type": "PAYIN",
"order_no": "PO…",
"merchant_order_no": "…",
"merchant_no": "…",
"amount": "100.00",
"currency": "PKR",
"status": "SUCCESS",
"fail_code": "",
"fail_reason": "",
"finished_at": "2026-08-29T12:00:00Z",
"notified_at": "2026-08-29T12:00:05Z"
}
biz_type:PAYIN或PAYOUT;status:SUCCESS或FAIL。- 通知携带与商户请求完全相同规范的 5 个签名 Header(第 2 节),对通知目标 URL 的实际路径和查询参数签名。商户可复用自己的签名代码验签,必须验签后再处理。
9.2 应答约定
商户收到并处理后必须返回:HTTP 200,响应体为纯文本 success(去除首尾空白后严格等于)。其他任何响应视为投递失败。平台请求超时 5 秒,不跟随重定向,响应体最多读取 1 KiB。
9.3 重试与幂等
- 至少一次(at-least-once)语义,通知可能重复。请以
biz_type + order_no幂等处理,重复通知直接返回success。 - 投递失败最多重试 3 次(总计 4 次),间隔约 15 秒 / 1 分钟 / 5 分钟;超过 30 分钟未成功则停止。
- 通知耗尽后订单状态不变;最终状态以查单接口为准,建议对长时间未收到通知的订单主动轮询。
9.4 商户侧验签示例(Python / Flask)
@app.post("/notify")
def on_notify():
body = request.get_data() # 原始字节,勿先解析再序列化
ts = request.headers.get("X-Timestamp", "")
nonce = request.headers.get("X-Nonce", "")
got = request.headers.get("X-Sign", "")
want = sign("POST", request.path, dict(request.args), body, ts, nonce)
if not hmac.compare_digest(got, want):
return "invalid sign", 401
if abs(time.time() - int(ts)) > 300:
return "expired", 401
data = json.loads(body)
handle_idempotently(data["biz_type"], data["order_no"], data["status"])
return "success", 200
10 对接最佳实践
- 以查单为准:通知用于加速,查单接口是最终事实。
- 幂等三原则:新订单换新
merchant_request_id;收到10003冲突去查单而不是重试;通知按订单号幂等消费。 - 按码编程:状态用数值枚举、失败原因用
fail_code判断;message、status_text、fail_reason仅用于展示。 - PENDING_VERIFY 不是失败:收付款订单进入 7 时保持等待,切勿重复下单,代付侧重复下单可能造成双重打款。
- 可用性类错误退避重试(10000/10004/20006),参数类错误(10001/10005/10006)修正后再发。
- 保护好
api_secret:仅存服务端;怀疑泄露立即联系平台轮换。
11 联调闭环与验收
完成以下 5 项即可进入验收:
- 使用平台提供的测试凭据完成一次代收和一次代付。
- 对创建请求做同幂等键重发,确认不会产生第二笔订单。
- 接收并验签通知;故意让首次应答失败,确认重复通知可被幂等消费。
- 对每笔订单以查单接口确认最终状态和金额。
- 验收通过后,由平台单独开通生产凭据、IP 白名单、通道和币种;测试凭据不得复用。
Merchant API Handbook
For the engineering teams of merchants integrating this platform's collection (payin) and disbursement (payout) capabilities.
Version 1.2 · 2026-09-08 · Adds the channel list endpoint and error code 10006
The environment host, app_id, api_key and api_secret are issued separately by platform operations at onboarding and are not part of this document. Query your available channel_code values and currencies yourself through the channel list endpoint in section 7.
01 Endpoints
| Method | Path | Purpose |
|---|---|---|
POST | /api/v1/payin/create | Create a collection order |
GET | /api/v1/payin/{payin_order_no} | Query a collection order |
POST | /api/v1/payout/create | Create a disbursement order |
GET | /api/v1/payout/{payout_order_no} | Query a disbursement order |
GET | /api/v1/balance | Query merchant balances |
GET | /api/v1/channel/list | List the channels opened to you |
Every endpoint is authenticated by the signature scheme in section 2. The query endpoints are the authoritative source of an order's final state. Asynchronous notifications may repeat, arrive late, or exhaust their retries, so reconcile with a query even after a notification arrives.
02 Signing and authentication
2.1 Request headers
Every request must carry these five headers:
| Header | Description |
|---|---|
X-App-Id | Application ID issued by the platform |
X-Api-Key | API key issued by the platform |
X-Timestamp | Unix timestamp in seconds, within 300 seconds of server time |
X-Nonce | Random string, single use; a repeat within the replay window is rejected |
X-Sign | Signature from 2.2, lowercase hexadecimal |
2.2 Signature algorithm
HMAC-SHA256 keyed with api_secret, output as lowercase hex. The string to sign is always eight segments joined by newline \n. An empty segment stays empty, but the separator is never omitted:
Canonicalization rules:
METHODis uppercase, for examplePOSTorGET.PATHis the actual request path with real path parameter values, for example/api/v1/payin/PO20260829000123, not the route template.QUERYis the canonicalized query string: parameters sorted by key ascending, repeated keys sorted by value ascending, keys and values percent-encoded separately, joined ask=vwith&. Empty when there are no query parameters.- The last segment is the lowercase SHA-256 hex of the raw request body bytes. Requests without a body, such as GET, hash the empty byte string, giving
e3b0c442…7852b855. Never deserialize and re-serialize the body first; hash exactly the bytes you send.
2.3 Python signing example
import hashlib, hmac, time, uuid, requests
from urllib.parse import urlencode
APP_ID = "your_app_id"
API_KEY = "your_api_key"
API_SECRET = "your_api_secret"
BASE = "https://<platform-host>"
def canonical_query(params: dict) -> str:
if not params:
return ""
pairs = []
for k in sorted(params):
vs = params[k] if isinstance(params[k], list) else [params[k]]
for v in sorted(map(str, vs)):
pairs.append((str(k), v))
# Use urlencode: it matches the platform's URL QueryEscape, encoding spaces as +.
return urlencode(pairs)
def sign(method: str, path: str, query: dict, body: bytes, ts: str, nonce: str) -> str:
body_sha = hashlib.sha256(body).hexdigest()
source = "\n".join([
method.upper(), path, canonical_query(query),
APP_ID, API_KEY, ts, nonce, body_sha,
])
return hmac.new(API_SECRET.encode(), source.encode(), hashlib.sha256).hexdigest()
def request(method: str, path: str, query: dict | None = None, body: bytes = b""):
ts = str(int(time.time()))
nonce = uuid.uuid4().hex
headers = {
"X-App-Id": APP_ID,
"X-Api-Key": API_KEY,
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Sign": sign(method, path, query or {}, body, ts, nonce),
"Content-Type": "application/json",
}
url = BASE + path + (("?" + urlencode(query)) if query else "")
return requests.request(method, url, headers=headers, data=body, timeout=10)
2.4 Notes
- Your merchant identity is resolved by the platform from the verified credentials. A merchant number in the request body is neither accepted nor possible.
- When the replay-protection store is unavailable the platform rejects the request outright with an availability error code. Back off and retry; never downgrade to bypass signing.
03 Response envelope and error codes
Every endpoint returns the same envelope:
{
"code": 0,
"message": "成功",
"data": { },
"trace_id": "…",
"timestamp": 1756450000
}
code = 0means success;datais omitted on failure.- Branch on
code, never parsemessage: the text is localized and subject to change. - Quote
trace_idwhen contacting the platform about a request.
| code | HTTP | Meaning | What to do |
|---|---|---|---|
| 10000 | 500 | System error | Platform-internal fault; contact the platform if it persists |
| 10001 | 400 | Invalid parameter | Fix the parameters and resend; retrying will not change the result |
| 10002 | 404 | Not found | Check the order or merchant number and query again |
| 10003 | 409 | Duplicate or conflict | This merchant_request_id was already accepted; use the query endpoint for the result |
| 10004 | 503 | Database error | Platform storage is unavailable; back off and retry |
| 10005 | 400 | Unsupported currency scale | amount must match the currency's decimal precision; the platform never rounds |
| 10006 | 400 | Channel not opened | This channel_code is not opened to you; ask the platform to open it. Resending will not help. Confirm your channels with section 7 |
| 20001 | 401 | Unauthenticated or bad signature | Check app_id and the signing algorithm |
| 20002 | 401 | Session expired | Admin console scenario; the gateway endpoints do not normally return it |
| 20003 | 403 | Insufficient permission | Admin console scenario; the gateway endpoints do not normally return it |
| 20004 | 403 | Account frozen or merchant unavailable | Contact the platform |
| 20005 | 403 | Account disabled | Contact the platform |
| 20006 | 429 | Rate limited | Back off and retry |
04 Amounts, currencies and idempotency
- Amounts are always major-unit decimal strings such as
"100.00". The number of decimals must match the currency's precision, which the platform provides. Extra decimals or malformed values are rejected, and the platform never rounds. channel_codeis chosen explicitly by you. One channel maps to exactly one direction and one currency.- The creation idempotency key is
merchant_request_id, unique per merchant:- Resending an identical request returns the order created the first time; no second order is produced.
- Reusing the same
merchant_request_idwith a changed amount, channel, recipient or any other key element returns10003and does not create a new order. Use the query endpoint to retrieve the original order instead. - So never reuse a
merchant_request_idto change an amount, channel or recipient. Place a new order with a new key.
05 Payin
5.1 Create POST /api/v1/payin/create
| Field | Type | Required | Description |
|---|---|---|---|
merchant_order_no | string(≤64) | yes | Your order number |
merchant_request_id | string(≤64) | yes | Idempotency key, see section 4 |
amount | string(≤32) | yes | Major-unit amount string, e.g. "100.00" |
currency | string | yes | Currency code |
channel_code | string | yes | Channel code |
product_name | string(≤128) | no | Product name |
notify_url | string(≤512) | no | Notification URL for this order; falls back to your default payin URL |
return_url | string(≤512) | no | Where to send the payer after payment |
Response data:
{
"url": "https://…cashier page…",
"payin_order_no": "PO…",
"merchant_order_no": "…",
"amount": "100.00",
"currency": "PKR",
"channel_code": "…",
"pay_method": "…",
"status": 2,
"channel_order_no": "upstream order number, may be null",
"provider_pay_url": "upstream payment link, may be null",
"provider_qr_code": "upstream QR payload, may be null",
"fail_code": "",
"fail_reason": null
}
status = 2 (PENDING): the upstream accepted the order. At least one of provider_pay_url and provider_qr_code is present; send the payer there.
status = 5 (FAIL): a definite failure, with a machine-readable reason in fail_code. To retry, place a new order with a new merchant_request_id.
status = 1 or 3 (INIT / PROCESSING, uncommon): the upstream is slow or the outcome is undetermined. Poll the query endpoint, or send the payer to the cashier url as a fallback.
A failed order does not change the envelope: once the order is accepted the envelope is code=0, and failure is expressed through status and fail_code.
5.2 Query GET /api/v1/payin/{payin_order_no}
Response data fields: payin_order_no, merchant_no, merchant_order_no, merchant_request_id, status, amount, currency, channel_code, pay_method, channel_order_no, provider_pay_url, provider_qr_code (returned only while the order is still payable), notify_url, return_url, expire_time, success_time, fail_code, fail_reason, created_at. Timestamps are RFC3339.
5.3 Payin order states
| Value | State | Meaning |
|---|---|---|
| 1 | INIT | Created, not yet sent upstream |
| 2 | PENDING | Accepted upstream, awaiting payment |
| 3 | PROCESSING | Being created or confirmed |
| 4 | SUCCESS | Paid (terminal, credited) |
| 5 | FAIL | Failed (terminal) |
| 6 | CLOSED | Expired or closed (terminal) |
| 7 | PENDING_VERIFY | Awaiting manual verification. Do not treat as success or failure; wait for the terminal notification or poll |
06 Payout
6.1 Create POST /api/v1/payout/create
| Field | Type | Required | Description |
|---|---|---|---|
merchant_order_no | string(≤64) | yes | Your order number |
merchant_request_id | string(≤64) | yes | Idempotency key |
amount | string(≤32) | yes | Major-unit amount string |
currency | string | yes | Currency code |
channel_code | string | yes | Channel code |
receiver_name | string(≤128) | yes | Recipient name |
receiver_account | string(≤128) | yes | Recipient account |
receiver_bank_code | string(≤64) | no | Recipient bank code; required by some channels, per that channel's notes |
receiver_email | string(≤128) | no | Recipient email, format validated |
Response data:
{ "payout_order_no": "PT…", "status": 1 }
A successful creation immediately freezes order amount + fee from your available balance. A terminal failure (5) unfreezes the full amount automatically; a terminal success (4) deducts from the frozen balance. If the balance is insufficient the creation fails outright and no order is produced.
6.2 Query GET /api/v1/payout/{payout_order_no}
Response data fields: payout_order_no, merchant_order_no, amount, currency, status, status_text, fail_code, fail_reason, receiver_account (masked), success_time, created_at.
6.3 Payout order states
| Value | State | Meaning |
|---|---|---|
| 1 | INIT | Created, funds frozen |
| 3 | PROCESSING | Disbursement in progress |
| 4 | SUCCESS | Paid out (terminal, settled) |
| 5 | FAIL | Failed (terminal, unfrozen) |
| 6 | CLOSED | Closed (terminal) |
| 7 | PENDING_VERIFY | Outcome unknown, funds stay frozen pending verification. Do not treat as failure and re-submit |
While an order is in PENDING_VERIFY (7) the platform sends no success or failure notification. Submitting a new order with a fresh merchant_request_id at that point will cause a double disbursement if the original order eventually succeeds. Wait for the original order to reach a terminal state.
07 Channel list GET /api/v1/channel/list
Returns the channels opened to you. Take the channel_code you order with from here. When the platform closes a channel for you it disappears from this list, and ordering on it returns 10006.
Query parameters: biz_type (1 = payin, 2 = payout, default 1), page (default 1), size (default 20, maximum 100; a larger value is clamped to 100 rather than rejected).
Response data: page, size, total, list. Each item of list contains:
| Field | Description |
|---|---|
channel_code | Channel code; name it when creating an order |
biz_type | 1 = payin, 2 = payout |
method_code / method_name | Payment method code and display name |
currency_code | The channel's currency; one channel serves exactly one currency |
fee_rate | Your rate on this channel as a percentage string, e.g. "2.5000" |
fixed_fee | Fixed fee in the currency's minor unit; 0 when none is charged |
Per-order amount limits are not returned here: they are an upstream technical constraint that changes as the upstream adjusts, so the create endpoint's live check governs. Cache this list for a while and refresh periodically rather than calling it per order.
08 Balance GET /api/v1/balance
No request parameters; your identity comes from the signed credentials. Response data:
{
"as_of": "2026-08-29T12:00:00Z",
"balances": [
{
"currency": "PKR",
"available_balance": "12345.00",
"frozen_balance": "100.00",
"total_balance": "12445.00"
}
]
}
All amounts are major-unit strings. Total = available + frozen.
09 Asynchronous notifications
Once an order reaches a notifiable terminal state (SUCCESS or FAIL) the platform pushes a notification:
- Payin: the
notify_urlgiven at creation, falling back to your default payin notification URL. - Payout: your default payout notification URL.
- If no URL is configured the notification is skipped; rely on the query endpoint.
9.1 Notification payload
POST with a JSON body:
{
"biz_type": "PAYIN",
"order_no": "PO…",
"merchant_order_no": "…",
"merchant_no": "…",
"amount": "100.00",
"currency": "PKR",
"status": "SUCCESS",
"fail_code": "",
"fail_reason": "",
"finished_at": "2026-08-29T12:00:00Z",
"notified_at": "2026-08-29T12:00:05Z"
}
biz_typeisPAYINorPAYOUT;statusisSUCCESSorFAIL.- Notifications carry the same five signature headers under exactly the same rules as merchant requests (section 2), signed over the notification URL's actual path and query. Reuse your own signing code to verify, and always verify before acting.
9.2 Acknowledgement
After receiving and processing a notification you must return HTTP 200 with the plain-text body success, matched exactly after trimming whitespace. Anything else counts as a failed delivery. The platform times out after 5 seconds, does not follow redirects, and reads at most 1 KiB of the response.
9.3 Retries and idempotency
- Delivery is at-least-once, so notifications may repeat. Deduplicate on
biz_type + order_noand returnsuccessfor repeats. - A failed delivery is retried up to 3 times (4 attempts in total) at roughly 15 seconds, 1 minute and 5 minutes, and stops after 30 minutes.
- Exhausted notifications do not change the order state. The query endpoint is authoritative; poll orders whose notification has not arrived for a long time.
9.4 Verification example (Python / Flask)
@app.post("/notify")
def on_notify():
body = request.get_data() # raw bytes; never parse then re-serialize
ts = request.headers.get("X-Timestamp", "")
nonce = request.headers.get("X-Nonce", "")
got = request.headers.get("X-Sign", "")
want = sign("POST", request.path, dict(request.args), body, ts, nonce)
if not hmac.compare_digest(got, want):
return "invalid sign", 401
if abs(time.time() - int(ts)) > 300:
return "expired", 401
data = json.loads(body)
handle_idempotently(data["biz_type"], data["order_no"], data["status"])
return "success", 200
10 Integration practices
- Trust the query, not the notification. Notifications only make you faster; the query endpoint is the final truth.
- Three idempotency rules. New order, new
merchant_request_id. On10003, query instead of retrying. Consume notifications idempotently by order number. - Program against codes. Use the numeric state enums and
fail_code;message,status_textandfail_reasonare for display only. - PENDING_VERIFY is not a failure. Keep waiting when an order reaches 7 and never re-submit; on the payout side a re-submission can pay twice.
- Back off on availability errors (10000, 10004, 20006). Fix parameter errors (10001, 10005, 10006) before resending.
- Protect
api_secret. Keep it server-side only, and contact the platform to rotate it the moment you suspect exposure.
11 Integration test and acceptance
Complete these five steps to enter acceptance:
- Use the platform's test credentials to complete one payin and one payout.
- Resend a create request with the same idempotency key and confirm no second order appears.
- Receive and verify a notification; deliberately fail the first acknowledgement and confirm the repeat is consumed idempotently.
- Confirm each order's final state and amount through the query endpoint.
- After acceptance the platform issues production credentials, IP allowlist, channels and currencies separately. Test credentials are never reused.