接入概览
星潮提供两个收款入口,均为服务端对服务端接入,需要先由星潮为你开通接入方并发放密钥:
| 入口 | 适用场景 | 金额单位(下单) | 能否指定用户 |
|---|---|---|---|
| H5 收银台 | 你的系统给星潮 App 用户代充:传入用户 ID,买家在手机上打开我们的收银页付款,到账后金币直接加到该用户 | 元(字符串,如 "10.00") | 可传 uid;不传则按付款人自动生成账号 |
| 星潮第三方 | 你的商城/业务系统收款:我们返回收银页链接,买家付款后异步通知你 | 分(整数,如 1000) | 不指定用户 |
你需要向星潮申请
app_id:接入方标识app_secret:签名密钥(仅服务端保存,严禁写入前端、App 或日志)- 开通的入口(H5 收银台 / 星潮第三方)
- 到账通知地址(可选,也可以每笔下单时传
notify_url)
接口地址:https://appapi.cstfparty.com(下文接口路径均相对此域名)。返回的 pay_url 是收银页链接,域名由星潮分配(H5 收银台为 cashier.xcparty.com,星潮第三方为 pay.xcparty.com),请直接使用返回值,不要自行拼接或写死域名。无需提供 IP 白名单;如需限定来源 IP,可让星潮在后台为你配置。
鉴权与签名
所有接入方接口(下单、查单)都需要在请求头携带:
| 请求头 | 说明 |
|---|---|
X-App-Id | 接入方标识 |
X-Timestamp | 当前 Unix 时间戳(秒)。与服务器时间相差超过 300 秒会被拒绝,请保持服务器时钟准确(NTP) |
X-Sign | 签名,见下 |
Content-Type | application/json |
签名算法
X-Sign = UPPER( MD5( 请求体原文 + X-Timestamp + app_secret ) )
- 请求体原文:你发出去的那串 JSON 字符串,逐字节参与签名。不要在签名后再重新序列化、格式化或调整字段顺序,否则验签必失败。
- 三段直接拼接,中间没有分隔符;MD5 结果取 32 位十六进制并转为大写(服务端大小写不敏感)。
- 字符编码统一 UTF-8。
测试向量(自检用)
以下数据可用来验证你的签名实现是否正确,app_secret 为演示值 demo_secret_123456,X-Timestamp 为 1760860800:
| 请求体原文 | 期望的 X-Sign |
|---|---|
{"out_order_no":"ORDER20261019001","amount":"10.00","uid":"10001","subject":"充值10元"} | AF5801E7C32EC2EE326AC42DF0BA1237 |
{"out_order_no":"ORDER20261019002","amount":1000,"subject":"充值10元"} | 87F82C843AFEBFC1CED42E20325C6AEB |
{"out_order_no":"ORDER20261019001"} | A91E24D27BC1BC997DDE7C8D8C7C8297 |
通用约定
幂等
以 (接入方, out_order_no) 为幂等键。同一个 out_order_no 重复下单:参数一致则返回原订单(含同一个收银页链接),不会重复生成订单;金额或用户与首次不一致,返回 12002。因此网络超时后可以放心用同一单号重试。
订单状态(查单、回调通用)
| status | 含义 |
|---|---|
| 1 | 支付中(未支付) |
| 2 | 支付成功 |
| 6 | 已关闭(超时未支付) |
请以 status=2 作为发货/到账的唯一依据,不要以买家是否跳转回你的页面为准。
H5 收银台 · 流程
- 你的服务端调用下单,传入金额、
uid、你方单号。 - 我们返回
pay_url(收银页链接)。 - 买家在手机浏览器打开
pay_url,完成支付宝付款(收银页免登录、免签名)。 - 付款成功后,金币自动加到
uid对应用户;同时我们向你的notify_url发送到账回调。 - 你也可以随时调用查单主动核对。
expire_at 为准)。过期后需用新的单号重新下单。uid 下单意味着密钥持有方可以给任意用户充值。请严格保管 app_secret,一旦泄露立即联系星潮重置。H5 收银台 · 下单
POST/cashier/v1/create
请求体(JSON)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
out_order_no | string | 是 | 你方订单号,≤64 位,仅限字母、数字及 _ - . :。同一接入方内唯一,用于幂等 |
amount | string | 是 | 金额,单位元,最多两位小数,如 "10.00"。带 uid 时必须为整数元。单笔范围 1 ~ 5000 元(实际以星潮配置为准) |
uid | string | 否 | 星潮用户 ID(纯数字)。用户须存在且状态正常。金币按 1 元 = 10 金币 到账。不传则按付款人自动生成账号并发放金币 |
subject | string | 否 | 商品标题,≤64 位 |
notify_url | string | 否 | 到账回调地址,必须是公网可访问的 http(s)。不传则使用接入方默认通知地址 |
device_id | string | 否 | 买家设备标识,用于风控,≤128 位 |
请求示例
POST /cashier/v1/create HTTP/1.1
Host: appapi.cstfparty.com
Content-Type: application/json
X-App-Id: shop_a
X-Timestamp: 1760860800
X-Sign: AF5801E7C32EC2EE326AC42DF0BA1237
{"out_order_no":"ORDER20261019001","amount":"10.00","uid":"10001","subject":"充值10元"}
响应
{
"code": 200,
"message": "success",
"data": {
"order_no": "RC_4afc1326-84b3-4cac-bed4-ac3b019eab2d",
"out_order_no": "ORDER20261019001",
"token": "p3Qx...(43 位)",
"pay_url": "https://cashier.xcparty.com/p/p3Qx...",
"amount": 1000,
"amount_yuan": "10.00",
"expire_at": 1760860920000,
"expire_seconds": 120
}
}
| 字段 | 说明 |
|---|---|
order_no | 星潮订单号 |
pay_url | 收银页链接,让买家在手机浏览器打开 |
amount | 金额,单位分 |
expire_at | 过期时间,Unix 毫秒 |
code: 200(数字)判断成功;星潮第三方接口使用 "code":"200"(字符串)。两者格式不同,请分别处理。H5 收银台 · 查单
POST/cashier/v1/query
请求体 {"out_order_no":"ORDER20261019001"},同样需要签名。仅能查询自己名下的订单。
{
"code": 200,
"message": "success",
"data": {
"out_order_no": "ORDER20261019001",
"order_no": "RC_4afc1326-...",
"amount": 1000,
"status": 2,
"paid_at": 1760860860
}
}
amount 单位分;paid_at 为 Unix 秒,未支付时为 0。
星潮第三方 · 流程
- 你的服务端调用下单,传入金额(分)与你方单号。
- 我们返回
pay_url(收银页链接)。 - 买家在手机浏览器打开链接付款。
- 付款成功后我们向
notify_url发送到账回调。
星潮第三方 · 下单
POST/openapi/v1/pay/create
请求体(JSON)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
out_order_no | string | 是 | 你方订单号,≤64 位,同一接入方内唯一,用于幂等 |
amount | integer | 是 | 金额,单位分,正整数(如 10 元传 1000) |
subject | string | 否 | 商品标题,≤128 位 |
notify_url | string | 否 | 到账回调地址,必须是公网可访问的 http(s)。不传则使用接入方默认通知地址 |
client_ip | string | 否 | 买家真实 IP,用于风控 |
device_id | string | 否 | 买家设备标识,用于风控 |
请求示例
POST /openapi/v1/pay/create HTTP/1.1
Host: appapi.cstfparty.com
Content-Type: application/json
X-App-Id: shop_a
X-Timestamp: 1760860800
X-Sign: 87F82C843AFEBFC1CED42E20325C6AEB
{"out_order_no":"ORDER20261019002","amount":1000,"subject":"充值10元"}
响应
{
"code": "200",
"msg": "success",
"content": {
"out_order_no": "ORDER20261019002",
"mch_order_no": "RC_8d1f...",
"pay_url": "https://pay.xcparty.com/p/Zk3...",
"cashier_url": "",
"scan_url": ""
}
}
让买家打开 pay_url 付款。cashier_url / scan_url 为保留字段,通常为空,请勿依赖。
星潮第三方 · 查单
POST/openapi/v1/pay/query
请求体 {"out_order_no":"ORDER20261019002"},同样需要签名。
{
"code": "200",
"msg": "success",
"content": {
"out_order_no": "ORDER20261019002",
"mch_order_no": "RC_8d1f...",
"amount": 1000,
"status": 2,
"paid_at": 1760860860
}
}
到账回调
订单支付成功后,我们向 notify_url 发送一次 HTTP POST(JSON)。两个入口的回调格式一致。
请求头
| 请求头 | 说明 |
|---|---|
X-App-Id | 你的接入方标识 |
X-Timestamp | Unix 秒,等于请求体中的 timestamp |
X-Sign | UPPER(MD5(请求体原文 + X-Timestamp + app_secret)),与下单签名同一算法 |
请求体
{"app_id":"shop_a","out_order_no":"ORDER20261019001","mch_order_no":"RC_4afc1326-...","amount":1000,"status":2,"paid_at":1760860860,"timestamp":1760860861}
| 字段 | 说明 |
|---|---|
out_order_no | 你方订单号 |
mch_order_no | 星潮订单号 |
amount | 实付金额,单位分 |
status | 固定为 2(支付成功) |
paid_at | 支付时间,Unix 秒 |
你需要做的
- 验签:用请求体原文重新计算签名并与
X-Sign比对,不一致一律丢弃。 - 幂等处理:同一订单可能收到多次回调,以
out_order_no去重。 - 处理成功后返回 HTTP 2xx,且响应体为纯文本
SUCCESS(不区分大小写,首尾空白会被忽略)。
notify_url 必须是公网可访问的 http(s) 地址,内网、本机地址会被拒绝。错误码
失败时 H5 收银台返回 {"code":错误码,"message":"..."};星潮第三方返回 {"code":"错误码","msg":"..."}。
| 错误码 | 含义 | 处理建议 |
|---|---|---|
| 400 | 参数错误(缺字段、金额格式不对、notify_url 不合法、out_order_no 格式不对等) | 按 message 修正后重试 |
| 401 | 鉴权失败:缺请求头、接入方不存在、时间戳过期、签名错误 | 核对 app_id、密钥、时钟、签名基串 |
| 403 | 接入方被禁用 / 来源 IP 不在白名单 / 未开通该入口 | 联系星潮开通 |
| 404 | 订单不存在(查单时) | 确认 out_order_no 与入口一致 |
| 10001 | 用户不存在(H5 下单传了 uid) | 核对用户 ID |
| 12002 | 同一 out_order_no 已存在,但金额或用户与首次不一致 | 换新单号 |
| 12003 | 支付渠道繁忙 | 稍后重试 |
| 12004 | 被风控拦截 | 联系星潮 |
| 12005 | 订单已过期 | 用新单号重新下单 |
| 500 | 系统繁忙 | 稍后用同一单号重试(幂等) |
示例代码
以下示例演示下单签名。请求体字符串必须与参与签名的字符串完全一致。
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public class StarTidePay {
static String sign(String body, String ts, String secret) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] d = md.digest((body + ts + secret).getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : d) sb.append(String.format("%02X", b));
return sb.toString();
}
public static void main(String[] a) throws Exception {
String appId = "shop_a", secret = "你的密钥";
String body = "{\"out_order_no\":\"ORDER20261019001\",\"amount\":\"10.00\",\"uid\":\"10001\"}";
String ts = String.valueOf(System.currentTimeMillis() / 1000);
HttpRequest req = HttpRequest.newBuilder(URI.create("https://appapi.cstfparty.com/cashier/v1/create"))
.header("Content-Type", "application/json")
.header("X-App-Id", appId).header("X-Timestamp", ts)
.header("X-Sign", sign(body, ts, secret))
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)).build();
System.out.println(HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()).body());
}
}import hashlib, time, requests
app_id, secret = "shop_a", "你的密钥"
body = '{"out_order_no":"ORDER20261019001","amount":"10.00","uid":"10001"}'
ts = str(int(time.time()))
sign = hashlib.md5((body + ts + secret).encode("utf-8")).hexdigest().upper()
r = requests.post(
"https://appapi.cstfparty.com/cashier/v1/create",
data=body.encode("utf-8"), # 注意:发送的就是参与签名的那串字节
headers={"Content-Type": "application/json",
"X-App-Id": app_id, "X-Timestamp": ts, "X-Sign": sign})
print(r.json())<?php
$appId = 'shop_a'; $secret = '你的密钥';
$body = '{"out_order_no":"ORDER20261019001","amount":"10.00","uid":"10001"}';
$ts = (string) time();
$sign = strtoupper(md5($body . $ts . $secret));
$ch = curl_init('https://appapi.cstfparty.com/cashier/v1/create');
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json',
"X-App-Id: $appId", "X-Timestamp: $ts", "X-Sign: $sign"],
]);
echo curl_exec($ch);const crypto = require('crypto');
const appId = 'shop_a', secret = '你的密钥';
const body = '{"out_order_no":"ORDER20261019001","amount":"10.00","uid":"10001"}';
const ts = String(Math.floor(Date.now() / 1000));
const sign = crypto.createHash('md5').update(body + ts + secret, 'utf8').digest('hex').toUpperCase();
fetch('https://appapi.cstfparty.com/cashier/v1/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-App-Id': appId, 'X-Timestamp': ts, 'X-Sign': sign },
body, // 与参与签名的字符串完全一致
}).then(r => r.json()).then(console.log);APP_ID=shop_a; SECRET='你的密钥'
BODY='{"out_order_no":"ORDER20261019001","amount":"10.00","uid":"10001"}'
TS=$(date +%s)
SIGN=$(printf '%s%s%s' "$BODY" "$TS" "$SECRET" | md5sum | cut -d' ' -f1 | tr 'a-f' 'A-F')
curl -s https://appapi.cstfparty.com/cashier/v1/create \
-H 'Content-Type: application/json' \
-H "X-App-Id: $APP_ID" -H "X-Timestamp: $TS" -H "X-Sign: $SIGN" \
--data-binary "$BODY"回调验签示例(Python / Flask)
from flask import Flask, request
import hashlib
app = Flask(__name__)
SECRET = "你的密钥"
@app.post("/pay/notify")
def notify():
raw = request.get_data(as_text=True) # 必须用原文,不要先解析再序列化
ts = request.headers.get("X-Timestamp", "")
sign = hashlib.md5((raw + ts + SECRET).encode()).hexdigest().upper()
if sign != request.headers.get("X-Sign", "").upper():
return "bad sign", 400
data = request.get_json()
# 以 data["out_order_no"] 幂等处理;status == 2 才发货/记账
return "SUCCESS" # 纯文本,2xx
常见问题
总是提示签名错误(401)?
- 签名用的字符串与实际发送的字节是否一致?最常见的原因是签名后又被 HTTP 客户端重新序列化/格式化 JSON。
X-Timestamp必须是秒而非毫秒,且与服务器相差不超过 300 秒。- 签名结果是否转大写、是否用了正确的
app_secret。用上方「测试向量」先验证你的实现。
H5 下单提示「充值到账号时金额须为整元」?
传了 uid 时,金币按 1 元 = 10 金币整数到账,因此金额必须是整数元(如 "10.00"、"38.00")。
买家付款了但我没收到回调?
先调用查单确认 status。回调失败会自动重试,但你的服务必须按要求返回 SUCCESS;同时请确认回调地址公网可达、证书有效。
收银页过期了怎么办?
已过期的订单不能再支付。请使用新的 out_order_no 重新下单。若买家在过期前已发起付款,到账仍会正常处理并通知你,请以查单/回调结果为准。
可以用同一个密钥同时接两个入口吗?
可以,前提是星潮已为你的接入方开通了对应入口。签名规则完全相同,只是请求路径和金额单位不同。