概览

StablePay 商户开放接口提供 代收(Payment) 与 代付(Payout) 两类业务能力。所有接口为 POST application/json,使用 MD5 签名认证。

接入前请向 StablePay 商务申请商户账号,登录商户后台 → API 对接 页面获取以下凭据:

  • merchantId  —  商户 ID(请求必带)
  • apiKey  —  商户密钥(仅用于本地签名,不要传输)
  • aesKey / aesIv  —  部分敏感字段加密用(如启用)
安全提醒:apiKey 永远不要出现在请求体或 URL 里。如不慎泄露,立即在后台重置。

联调流程

  1. 商户后台获取 merchantId + apiKey
  2. 在后台 API 对接 页配置 支付 URL / 代付 URL(接收异步通知)
  3. (可选)在 IP 白名单 配置允许调用的 IP;不配置则放行所有 IP
  4. 若回调接口启用了入站 IP 白名单,请联系商务获取平台回调出口 IP,并将商务提供的 IP 加入入站白名单
  5. 按本文档签名规则组装请求 → 调用 create 接口下单
  6. 使用返回的 orderNo 或自有 merchantOrderNo 调用 query 查询订单状态
  7. 代收成功、代付成功或失败时,平台会向您配置的回调 URL 推送异步通知(按相同签名规则验签);代付失败会退回本金和手续费

公共说明

请求方式

  • 方法:POST
  • Content-Type:application/json
  • 字符集:UTF-8

请求 base URL

由 StablePay 商务为您单独提供(如 https://api.example.com)。本文档中接口路径均为相对路径。

统一响应结构

{
  "code": 0,           // 0=成功,非0=失败
  "message": "success",
  "data": { ... }      // 业务数据,仅 code=0 时有意义
}

金额格式

所有金额字段在 请求体 中以 number 类型传输(如 500.00),在 签名计算 时必须格式化为保留两位小数的字符串(如 "500.00",而不是 "500" 或 "500.000")。

时间字段

所有时间字段为 Unix 时间戳(秒级,int64)。前端展示请自行转换。

签名规则

所有写接口(create)和查询接口(query)都需带 sign 字段。算法如下:

  1. 取出请求体中所有非空参数(值为 ""、null 或缺省的字段不参与签名),剔除 sign 本身。
  2. 金额类字段(如 amount)必须先转成固定两位小数的字符串:100 → 100.00,100.5 → 100.50,再参与排序和签名。整数金额也不能省略 .00。
  3. 把参数按 key 升序(ASCII)排序,拼接为 k1=v1&k2=v2&...&kn=vn。
  4. 末尾追加 &key={apiKey}。
  5. 对最终字符串做 MD5(32 位),转大写,作为 sign。
重要:请求金额与签名金额的格式不同。请求 JSON 可以传数字 "amount": 100,但签名原文必须使用 amount=100.00,不能使用 amount=100、100.0 或 100.000。服务端会把请求金额格式化成两位小数后验签;格式不一致会返回 401 签名验证失败。

整数金额签名示例

以下代收请求的金额仍以 JSON 数字传输:

{
  "merchantId":      "SP20260101000001",
  "merchantOrderNo": "DEMO_IN_100",
  "amount":          100,
  "notifyUrl":       "https://your.site/notify",
  "sign":            "<由本算法计算>"
}

正确签名原文(注意 amount=100.00):

amount=100.00&merchantId=SP20260101000001&merchantOrderNo=DEMO_IN_100&notifyUrl=https://your.site/notify&key=<您的 apiKey>

不要使用 amount=100&...。签名必须基于最终发送的参数生成:merchantOrderNo、notifyUrl 等字段必须与请求体完全一致;修改参数后需要重新计算 sign。不要使用另一笔订单的签名。

示例

请求体(amount=500.00,merchantOrderNo=E2E_OUT_001):

{
  "merchantId":      "SP20260101000001",
  "merchantOrderNo": "E2E_OUT_001",
  "amount":          500.00,
  "beneName":        "Raj Kumar",
  "beneAccount":     "1234567890123456",
  "beneIfsc":        "HDFC0001234",
  "benePhone":       "9876543210",
  "notifyUrl":       "https://your.site/notify",
  "sign":            "<由本算法计算>"
}

排序拼接(注意 amount 用 500.00):

amount=500.00&beneAccount=1234567890123456&beneIfsc=HDFC0001234&beneName=Raj Kumar&benePhone=9876543210&merchantId=SP20260101000001&merchantOrderNo=E2E_OUT_001&notifyUrl=https://your.site/notify&key=<您的 apiKey>

对上述字符串 MD5 → 转大写 → 即为 sign。

注意:拼接时 = 和 & 之间不留空格;URL 字符(如 : /)不做编码;中文/空格保持原样。

创建代收订单

POST /api/payment/create

商户向用户收款时调用。下单成功后平台返回平台订单号;用户付款 / 平台对账完成后通过异步通知或主动查询接口获取最终状态。

请求参数

字段类型必填说明
merchantIdstring必填商户 ID
merchantOrderNostring必填商户系统订单号,单商户内唯一
amountnumber必填下单金额,> 0,保留两位小数
customerNamestring选填付款人姓名
customerEmailstring选填付款人邮箱
customerPhonestring选填付款人手机号
notifyUrlstring选填异步通知地址,覆盖商户后台默认配置
returnUrlstring选填付款完成后浏览器跳回地址
remarkstring选填备注,原样回显
signstring必填签名,详见签名规则

响应 data

字段类型说明
orderNostring平台订单号(以 STABLEPAYIN 开头)
merchantOrderNostring回显商户订单号
txRefstring平台交易参考号(6 位大写字母/数字)。用户 UPI 转账时必须原样填入「备注」,平台按它回扫收款银行短信 Payer Remark 匹配订单
amountstring商户下单原始金额,保留两位小数
payableAmountstring订单实际应付金额,保留两位小数;通道金额匹配可能使其与 amount 不同,用户须按此金额付款
statusstring初始状态固定 PENDING
expiryDateint64订单过期时间(Unix 秒),默认 30 分钟
remarksstringUPI 转账备注,值等于 txRef,用户转账时须原样填写
beneNamestring收款人姓名
beneAccountstring收款人银行账号
beneIfscstring收款人银行 IFSC
bankstring收款银行名称
upiVpastring收款 UPI VPA(收银台/UPI 拉起用)
upiStrstring本订单收款 UPI 的完整深链接(upi://pay?...),可生成支付二维码或拉起 UPI 应用。pa 为订单绑定的 upiVpa,am 为 payableAmount,tr/tn 为 txRef。上游收银台通道无法提供深链接时返回空字符串,请使用 payUrl
payUrlstring平台收银台地址,可直接跳转给终端用户扫码 / 拉起 UPI 应用付款

说明:若商户自建收银页,可直接使用 upiStr 生成二维码或拉起 UPI 应用,链接中的金额和备注已按订单填写;手动转账时,须按 payableAmount 付款并把 txRef 原样填入备注。若直接使用平台收银台,跳转 payUrl 即可。

返回示例

{
  "code": 0,
  "message": "success",
  "data": {
    "orderNo": "STABLEPAYIN20260428143022000123",
    "merchantOrderNo": "E2E_IN_001",
    "txRef": "K7M9P2",
    "amount": "500.00",
    "payableAmount": "500.00",
    "status": "PENDING",
    "expiryDate": 1714203600,
    "remarks": "K7M9P2",
    "beneName": "Raj Kumar",
    "beneAccount": "1234567890123456",
    "beneIfsc": "HDFC0001234",
    "bank": "HDFC Bank",
    "upiVpa": "raj@okhdfc",
    "upiStr": "upi://pay?cu=INR&pa=raj@okhdfc&pn=Raj+Kumar&tr=K7M9P2&am=500.00&tn=K7M9P2&mc=7407&cu=INR&url=&mode=02&purpose=00&orgid=159002",
    "payUrl": "https://pay.stablepay.example/?merchantId=SP20260101000001&orderNo=STABLEPAYIN20260428143022000123"
  }
}

查询代收订单

POST /api/payment/query

请求参数

字段类型必填说明
merchantIdstring必填商户 ID
merchantOrderNostring二选一商户订单号
orderNostring二选一平台订单号
signstring必填签名

merchantOrderNo 与 orderNo 至少传其一。

响应 data

字段类型说明
orderNostring平台订单号
merchantOrderNostring商户订单号
amountstring下单金额
paidAmountstring实际到账金额
statusstring订单状态,见订单状态
utrstring银行端流水号(成功后才有)
createTimeint64创建时间(Unix 秒)
updateTimeint64最近更新时间(Unix 秒)

UTR 代收补单

POST /api/payment/supplement

三方对接本平台作为四方时,提交 UTR 和订单号即可同步获取补单成功或失败结果。核验规则与 TG /bu UTR 订单号 相同:核实收款通道、到账金额及真实成功流水后才结算,不要求匹配付款备注。仅支持当前商户自己的代收订单。

补单地址单独提供:本接口使用商务提供的商户后台域名,例如 https://merchant.example.com/api/payment/supplement;下单、查单仍使用商户 API 域名。请勿直接把下单域名拼接为补单地址。

请求参数

字段类型必填说明
merchantIdstring必填商户 ID,沿用下单的商户账号及 apiKey
merchantOrderNostring二选一三方自己的商户订单号
orderNostring二选一平台订单号;与 merchantOrderNo 至少填一个,同时填写必须指向同一订单
utrstring必填6–15 位数字,或 8–32 位包含字母的字母数字参考号;须保留前导零
signstring必填按签名规则计算,merchantId、所填订单号及 utr 均参与签名;使用实际发送的原始值签名

沿用商户启用状态和 API IP 白名单,无需后台登录 Token 或谷歌验证码。

请求示例

{
  "merchantId": "SP20260101000001",
  "merchantOrderNo": "M202610170001",
  "utr": "628808285413",
  "sign": "按下方原文计算的MD5大写值"
}

以上请求的签名原文(将 YOUR_API_KEY 替换为商户密钥):

merchantId=SP20260101000001&merchantOrderNo=M202610170001&utr=628808285413&key=YOUR_API_KEY

补单成功

{
  "code": 0,
  "message": "success",
  "data": {
    "success": true,
    "orderNo": "PAY202610170001",
    "merchantOrderNo": "M202610170001",
    "status": "SUCCESS",
    "utr": "628808285413",
    "amount": "100.00"
  }
}

amount 为商户原始下单金额。仅当 code=0 才表示已结算成功。同一订单、同一 UTR 重复请求返回成功,不会重复入账;已成功订单提交不同 UTR 返回失败。

补单失败

{
  "code": 500,
  "message": "已查询上游流水,暂未匹配到当前订单的到账交易,请核对 UTR、金额和收款通道后重试(补单无需匹配订单备注)"
}

HTTP 200 中的非零 code 表示本次请求失败,原因见 message:400 参数缺失;401 签名错误;403 商户不可用或 IP 不在白名单;500 订单不存在、UTR 已占用、流水不符或上游查询失败等业务异常。失败响应不代表订单永远失败,最终状态可通过查单接口确认。

  • 支持对待支付、已过期及失败但尚未结算的订单重新核验;不支持绕过 UTR 核验、免核销补余额等后台专用操作。
  • 同一 UTR 或到账流水只能用于一笔订单;金额或收款通道不匹配时不会入账。
  • 建议 HTTP 读取超时设为 180 秒。超时或连接中断时,先查单,再使用同一订单和 UTR 重试。
  • 本接口直接返回本次核验结果,不创建 TG 自动重试任务。成功后的商户异步回调沿用原订单通知流程。

创建代付订单

POST /api/payout/create

商户向第三方账户付款时调用。下单瞬间扣减商户可用余额(金额 + 手续费),订单失败后退款回账。

请求参数

字段类型必填说明
merchantIdstring必填商户 ID
merchantOrderNostring必填商户系统订单号,单商户内唯一
amountnumber必填付款金额,> 0,保留两位小数
beneNamestring条件必填收款人姓名,银行卡必填;UPI 可不传
beneAccountstring必填银行卡账号;UPI 模式填写收款 VPA,如 name@handle
beneIfscstring条件必填银行卡必填;UPI 不传
benePhonestring选填收款人手机号
beneEmailstring选填收款人邮箱
bankNamestring选填收款银行名
transferTypestring选填转账方式,BANK_TRANSFER(缺省)或 UPI
notifyUrlstring选填异步通知地址,覆盖商户后台默认配置
remarkstring选填备注
signstring必填签名
UPI 示例:{"transferType":"UPI","beneAccount":"name@handle",...}。收款姓名可空,不传 IFSC。当前自有钱包 UPI 代付仅支持 MobiKwik Pocket UPI 通道;签名算法、查单和成功回调与银行卡模式相同。

响应 data

字段类型说明
orderNostring平台订单号(以 STABLEPAYOUT 开头)
merchantOrderNostring回显商户订单号
amountstring金额
processFeestring手续费
statusstring初始状态固定 PENDING
余额校验:下单金额 + 手续费必须 ≤ 商户可用余额,否则返回 余额不足。

查询代付订单

POST /api/payout/query

请求参数

同代收查询:传 merchantId + (merchantOrderNo 或 orderNo) + sign。

响应 data

字段类型说明
orderNostring平台订单号
merchantOrderNostring商户订单号
amountstring付款金额
processFeestring手续费
statusstring订单状态,见订单状态
utrstring银行端流水号
createTimeint64创建时间
updateTimeint64更新时间

查询商户余额

POST /api/balance/query

查询当前 API 密钥对应商户的实时余额。沿用商户启用状态、API 密钥和商户 API IP 白名单校验;无需后台登录 Token 或订单号。

请求参数

字段类型必填说明
merchantIdstring是商户号,使用该商户的 API 密钥签名
signstring是MD5 签名,规则见下方

签名与请求示例

仅 merchantId 参与签名;拼接 merchantId=YOUR_MERCHANT_ID&key=YOUR_API_KEY 后按 UTF-8 计算 MD5 并转大写。不要对参数值做 URL 编码。

curl -X POST "$API_BASE_URL/api/balance/query" \
  -H 'Content-Type: application/json' \
  -d '{"merchantId":"YOUR_MERCHANT_ID","sign":"CALCULATED_MD5_SIGN"}'
import hashlib
import requests

merchant_id = "YOUR_MERCHANT_ID"
api_key = "YOUR_API_KEY"
sign = hashlib.md5(f"merchantId={merchant_id}&key={api_key}".encode("utf-8")).hexdigest().upper()
response = requests.post(f"{api_base_url}/api/balance/query",
                         json={"merchantId": merchant_id, "sign": sign}, timeout=15)
print(response.json())

成功响应

{
  "code": 0,
  "message": "success",
  "data": {
    "merchantId": "YOUR_MERCHANT_ID",
    "currency": "INR",
    "balance": "10000.25",
    "freezeBalance": "500.00",
    "depositBalance": "100.00"
  }
}
字段类型说明
balancestring可用余额,单位 INR;已扣除冻结金额,不能再次减去 freezeBalance
freezeBalancestring冻结余额,单位 INR
depositBalancestring押金余额,单位 INR

金额返回十进制字符串以保留精度。查询不会扣款、冻结、解冻或发起付款。缺少参数返回 400,签名不符返回 401,商户不存在、停用、未配置密钥或 IP 不在白名单返回 403。

回调通知

代收订单达到 SUCCESS、代付订单达到 SUCCESS 或 FAILED 后,平台会向您的回调地址主动推送异步通知(POST application/json)。代付失败会先退回本金和手续费,再发送失败通知;处理中或等待上游确认的代付不会提前通知失败。请在收到通知后验签并检查 status,按订单号幂等处理,不要仅凭通知到达就判定付款成功。

回调地址

  • 优先使用下单请求里传的 notifyUrl;
  • 未传则回退商户后台配置的默认地址(代收 = paymentUrl,代付 = payoutUrl);
  • 两者都为空则不推送(订单会停留在后台「回调失败」列表,可人工处理)。

回调 IP 白名单

平台回调出口 IP:请联系商务获取。若您的回调接口通过服务器防火墙、WAF、CDN 或网关限制来源 IP,请将商务提供的 IP 加入入站白名单,否则平台回调可能被拦截并触发重试。

此回调出口 IP 与商户后台的 IP 白名单 配置用途不同:后台 IP 白名单用于限制商户调用平台 API 的来源 IP,不限制平台向商户发送回调。

触发时机

业务触发的终态
代收SUCCESS
代付SUCCESS / FAILED

通知体(代收示例)

{
  "merchantId":      "SP20260101000001",
  "orderNo":         "STABLEPAYIN20260101000001",
  "merchantOrderNo": "E2E_IN_001",
  "amount":          "500.00",
  "paidAmount":      "499.50",
  "status":          "SUCCESS",
  "utr":             "1234567890",
  "sign":            "<平台按签名规则计算>"
}

代收回调金额说明

字段说明
amount商户原始下单金额。严格校验订单金额及给玩家上分时,请使用此字段。
paidAmount玩家享受平台优惠后的实际支付(到账)金额,与 amount 的差额在 1 卢比以内。
金额校验与上分:如需严格校验金额,请将商户订单金额与 amount 比较,并按 amount 给玩家上分。例如,下单金额为 500.00 卢比,优惠后 paidAmount 为 499.50 卢比,仍按 amount = 500.00 卢比上分。

代付通知体字段为 merchantId / orderNo / merchantOrderNo / amount / processFee / status / utr / sign。所有字段均以字符串下发,金额为两位小数字符串。

代付 FAILED 通知额外包含 failReason(失败原因,参与签名)。失败退款金额为 amount + processFee;后台“未结算”表示未成功出款,不表示未退款,可按订单号核对 PAYOUT_REFUND 资金流水。

验签

与请求签名规则完全一致:取通知体里所有非空字段(剔除 sign)按 key 升序拼成 k1=v1&k2=v2&...&key=<您的 apiKey>,MD5 转大写,与 sign 比对一致才可信任。

应答与重试

成功应答:HTTP 200 且响应体包含 success(或纯文本 ok)。任何其它情况(非 2xx、连接拒绝、超时、响应体未表示成功)均视为失败。
  • 每笔订单平台总共最多推送 3 次:首次推送失败后,每隔 60 秒再推一次,累计 3 次仍未收到成功应答即停止。
  • 停止后订单进入后台「回调失败」列表,运营可手动「重新发送」(会重置次数并立即再推一轮,同样最多 3 次)。
  • 请保证回调接口幂等:同一订单可能被推送多次。

订单状态

status说明
PENDING已创建,等待处理
SUCCESS成功(代收=已到账,代付=已打款)
FAILED失败(代付会自动解冻并退回余额)
EXPIRED已过期(仅代收)

业务错误码

code含义
0成功
400参数错误(缺字段、字段类型不对等)
401签名验证失败
403商户不存在 / IP 不在白名单 / 功能未开通
500服务端错误(含余额不足、订单号重复等业务异常)

cURL 示例

以代付下单为例(请把 merchantId、sign、baseURL 替换为您自己的):

curl -X POST https://api.example.com/api/payout/create \
  -H "Content-Type: application/json" \
  -d '{
    "merchantId":      "SP20260101000001",
    "merchantOrderNo": "E2E_OUT_001",
    "amount":          500.00,
    "beneName":        "Raj Kumar",
    "beneAccount":     "1234567890123456",
    "beneIfsc":        "HDFC0001234",
    "benePhone":       "9876543210",
    "notifyUrl":       "https://your.site/notify",
    "sign":            "ABCDEF0123456789ABCDEF0123456789"
  }'

Python 示例

展示完整签名 + 下单流程;可直接保存为 demo.py 运行。

import hashlib, requests, time

BASE_URL = "https://api.example.com"
MERCHANT_ID  = "SP20260101000001"
API_KEY      = "<在商户后台 API 对接页复制>"
AMOUNT       = 500.00

def compute_sign(params: dict, api_key: str) -> str:
    filtered = {k: str(v) for k, v in params.items() if k != "sign" and str(v) != ""}
    raw = "&".join(f"{k}={filtered[k]}" for k in sorted(filtered)) + f"&key={api_key}"
    return hashlib.md5(raw.encode()).hexdigest().upper()

def create_payout():
    sign_params = {
        "merchantId":      MERCHANT_ID,
        "merchantOrderNo": f"DEMO_{int(time.time())}",
        "amount":          f"{AMOUNT:.2f}",   # 签名时用字符串
        "beneName":        "Raj Kumar",
        "beneAccount":     "1234567890123456",
        "beneIfsc":        "HDFC0001234",
        "benePhone":       "9876543210",
        "notifyUrl":       "https://your.site/notify",
    }
    body = dict(sign_params)
    body["sign"]   = compute_sign(sign_params, API_KEY)
    body["amount"] = AMOUNT                   # 请求体里 amount 是 float
    return requests.post(f"{BASE_URL}/api/payout/create", json=body, timeout=15).json()

print(create_payout())