NAPAY钠云付

NAPAY-DOC-01 接口版本 v1 更新 2026-01-08 适用环境 沙箱 / 生产

接入手册

一套签名规则、一个下单接口、一条签名回调。跑通这三件事,收款链路就闭环了。 沙箱不涉及真实资金,可以随便试。

§01 Quickstart

快速开始

四步,二十分钟。全程在沙箱里做,不需要提交任何资质材料。

  1. 注册并创建应用在控制台填应用名称与回调地址,拿到 app_idapp_secret。密钥只展示一次。
  2. 发一笔下单请求照 §03 算签名,POST 到 /merchant/orders,同步拿到收银台链接。
  3. 在沙箱里模拟付款打开收银台链接,点「模拟支付成功」。
  4. 接住回调并验签服务端收到 payment.succeeded,验签通过后发货,返回 success
curl 最小可跑请求 先用控制台的「签名调试」生成 sign,再替换下面的值
curl -X POST https://api-sandbox.nayunfu.cn/api/v1/merchant/orders \
  -H 'Content-Type: application/json' \
  -d '{
    "app_id": "ap_sandbox_8f2c41",
    "out_trade_no": "AI202601080001",
    "amount": "19.90",
    "subject": "AI 绘画 100 次额度",
    "channel_code": "sandbox_qr",
    "notify_url": "https://your-app.com/napay/notify",
    "expires_in": 900,
    "timestamp": 1767838471,
    "nonce": "9d4c1f7b2a6e",
    "sign": "a3f1c9…"
  }'

§02 Environment

环境与凭证

两套环境互不相通,密钥也不通用。沙箱密钥前缀 ap_sandbox_,生产密钥前缀 ap_live_,写代码时用前缀判断环境最省事。

沙箱与生产环境的地址与状态
环境接口基址收银台域名状态
沙箱 https://api-sandbox.nayunfu.cn/api/v1 pay-sandbox.nayunfu.cn 已开放,不涉及真实资金
生产 https://api.nayunfu.cn/api/v1 pay.nayunfu.cn 随真实通道开通后可用

凭证怎么放

  • app_id 可以出现在请求体里,属于公开标识。
  • app_secret 只用于服务端算签名。不要放进前端代码、小程序包、公开仓库。
  • 轮换密钥时新旧两把并行 24 小时,期间两把都能验签,够你滚动发布。
  • 怀疑泄露就在控制台立即吊销,吊销即时生效,已在途的请求会返回 40100

回调地址:每个应用一个 notify_url,必须是 https 且公网可达。下单时传的 notify_url 优先于应用配置,方便你按环境分流。

§03 Signature

签名规则

请求和回调用同一套规则,方向相反。算法是 HMAC-SHA256,输出小写十六进制。

  1. 挑字段取报文里全部非空字段,排除 sign 本身。null 和空字符串跳过。
  2. 排序拼接按 key 的 ASCII 升序排列,拼成 k=v,用 & 连接,得到 stringA。
  3. 计算sign = HMAC-SHA256(stringA, app_secret),取小写 hex。
  4. 放回报文把结果写进 sign 字段发出。验签时用常量时间比较,别用 ==
stringA 拼接结果示例值不做 URL 编码
amount=19.90&app_id=ap_sandbox_8f2c41&channel_code=sandbox_qr&expires_in=900
&nonce=9d4c1f7b2a6e&notify_url=https://your-app.com/napay/notify
&out_trade_no=AI202601080001&subject=AI 绘画 100 次额度&timestamp=1767838471

三个容易踩的点。

金额一律用字符串,两位小数。用浮点数序列化出 19.919.900000001 都会验签失败。

嵌套对象(如 metadata)取紧凑 JSON 作为值:无空格、key 升序、中文不转义。建议 metadata 只放扁平字符串,绕开序列化差异。

拼接和摘要都按 UTF-8 字节处理。

Node.js 签名与下单Node 18+,无需额外依赖
import crypto from 'node:crypto';

const BASE = 'https://api-sandbox.nayunfu.cn/api/v1';
const APP_ID = 'ap_sandbox_8f2c41';
const SECRET = process.env.NAPAY_APP_SECRET;

// stringA:非空字段按 key 升序拼接,排除 sign
function stringA(p) {
  return Object.keys(p)
    .filter(k => k !== 'sign' && p[k] !== '' && p[k] != null)
    .sort()
    .map(k => `${k}=${typeof p[k] === 'object' ? JSON.stringify(p[k]) : p[k]}`)
    .join('&');
}

function sign(p) {
  return crypto.createHmac('sha256', SECRET).update(stringA(p), 'utf8').digest('hex');
}

const body = {
  app_id: APP_ID,
  out_trade_no: 'AI202601080001',
  amount: '19.90',
  subject: 'AI 绘画 100 次额度',
  channel_code: 'sandbox_qr',
  notify_url: 'https://your-app.com/napay/notify',
  expires_in: 900,
  timestamp: Math.floor(Date.now() / 1000),
  nonce: crypto.randomBytes(6).toString('hex')
};
body.sign = sign(body);

const res = await fetch(`${BASE}/merchant/orders`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(body)
});
const { data } = await res.json();
console.log(data.trade_no, data.checkout_url);
Python 回调验签标准库即可
import hmac, hashlib, json, time

def string_a(payload: dict) -> str:
    items = []
    for k in sorted(payload):
        if k == "sign":
            continue
        v = payload[k]
        if v is None or v == "":
            continue
        if isinstance(v, (dict, list)):
            v = json.dumps(v, separators=(",", ":"), sort_keys=True, ensure_ascii=False)
        items.append(f"{k}={v}")
    return "&".join(items)

def verify(payload: dict, secret: str) -> bool:
    if abs(time.time() - int(payload["timestamp"])) > 300:
        return False                      # 时间戳超出允许偏差
    expect = hmac.new(secret.encode(), string_a(payload).encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expect, payload.get("sign", ""))

§04 Create order

创建订单

POST /merchant/orders 同步返回收银台链接与平台订单号。支付结果一律以回调为准,不要拿这一步的返回当成已付款。

请求字段

创建订单请求字段
字段类型必填说明
app_idstring必填应用标识,控制台可见。
out_trade_nostring必填你的订单号,同一应用内唯一。1–64 位,允许字母、数字、-_
amountstring必填单位元,两位小数。范围 0.01–50000.00。
subjectstring必填商品或服务名称,最长 64 字。展示在收银台和账单上。
channel_codestring必填通道标识。沙箱用 sandbox_qr;真实通道开通后取值见控制台通道台账。
notify_urlstring必填接收回调的 https 地址,只接 POST。
timestampint必填秒级 Unix 时间戳,与平台时间偏差需在 ±300 秒内。
noncestring必填随机串,8–32 位。5 分钟内不可重复。
signstring必填按 §03 算出的签名。
currencystring可选目前只支持 CNY,缺省即 CNY
expires_inint可选有效期秒数,默认 900,范围 300–7200。到期自动关单。
return_urlstring可选付款完成后浏览器跳转地址。仅用于体验,不能当支付凭据。
descriptionstring可选订单备注,最长 128 字,只在你的账单里可见。
metadataobject可选最多 20 个键,键值均为字符串。原样在回调里回传。

响应

200 下单成功business code 为 0 才算成功
{
  "code": 0,
  "message": "ok",
  "request_id": "req_01HQ8F3M7X",       // 提工单时带上这个
  "data": {
    "trade_no": "NP-2601-0001",        // 平台订单号
    "out_trade_no": "AI202601080001",
    "status": "created",
    "amount": "19.90",
    "checkout_url": "https://pay-sandbox.nayunfu.cn/c/NP-2601-0001",
    "expires_at": "2026-01-08T10:39:31+08:00"
  }
}

幂等:24 小时内用同一个 out_trade_no 且金额一致重复下单,返回原订单,不会产生第二笔。金额不一致返回 40009。所以下单接口可以安全重试。

§05 Query & close

查询与关单

GET 接口把签名字段放在 query 上,规则与请求体一致。查询是你的兜底手段:回调迟迟不到时,按业务需要主动查一次,不要让用户干等。

查询、关单与回调重推接口
动作接口说明
GET/merchant/orders/{trade_no}按平台订单号查。也可用 /merchant/orders?out_trade_no=… 按你的订单号查。
POST/merchant/orders/{trade_no}/close主动关单。只有 created 状态可关,已付款的订单关单返回 40900
POST/merchant/notifications/{trade_no}/retry手动重推回调,用于修好接口后补通知。控制台也有同样的按钮。

轮询节奏:建议 5 秒起、指数退避到 30 秒,最长跟到订单 expires_at。已经收到回调就停止轮询。

§06 Webhook

回调通知

付款成功后我们 POST 一份带签名的 JSON 到你的 notify_url。顺序很重要:先验签,再改订单状态,最后回 success

POST your-app.com/napay/notifyContent-Type: application/json
{
  "app_id": "ap_sandbox_8f2c41",
  "event": "payment.succeeded",
  "trade_no": "NP-2601-0001",
  "out_trade_no": "AI202601080001",
  "channel_code": "sandbox_qr",
  "amount": "19.90",
  "fee": "0.32",                   // 19.90 × 1.6%,四舍五入到分
  "settle_amount": "19.58",          // 商户实收
  "paid_at": "2026-01-08T10:24:31+08:00",
  "timestamp": 1767838471,
  "nonce": "9d4c1f7b2a6e",
  "sign": "a3f1c9…"
}

你要做的四件事

  • 验签失败就返回 HTTP 400,不要处理业务,也不要泄露失败原因给调用方。
  • 核对 out_trade_noamount 是否与你本地订单一致,金额不符按异常处理并提工单。
  • trade_no 做幂等:同一笔重复到达时直接返回 success,不要重复发货。
  • 5 秒内返回,响应体是纯文本 success(或 JSON {"code":0})。耗时的发货动作丢进队列。

重推节奏

回调重推退避策略
次数距付款成功备注
第 1 次立即正常情况这一次就成功
第 2 次+15 秒前一次未在 5 秒内返回 success 时继续
第 3 次+1 分钟
第 4 次+5 分钟
第 5 次+15 分钟
第 6 次+1 小时
第 7 次+3 小时之后停止自动重推,可在控制台手动重推

§07 State

订单状态

正向链路四个状态,每一次跃迁都有对应报文;另有一个终止态 closed,表示这笔订单不会再收到钱。

订单状态与对应事件
状态含义对应事件
created已下单,拿到收银台链接,等待用户付款。
paid通道确认收款,平台完成金额与订单号校验。payment.succeeded
notified回调已被你的服务端确认收到。回调返回 success 后置位
reconciled已进入次日账单,逐笔可核,费率单列。账单可下载
closed超时或主动关单,终止态。order.closed

退款暂未提供接口,随真实通道一起开放。沙箱阶段请按「不可退」设计你的业务流程,需要试退款流程可以提工单沟通。

§08 Errors

错误码

HTTP 状态码表示请求层面的结果,业务结果看响应体的 codemessage 是给人读的,不要用来做分支判断。

错误码与处理建议
code含义怎么处理
40001参数校验失败data.fields 里指出的字段,修好再发。不要重试。
40009out_trade_no 重复但金额不一致换一个订单号,或先关掉原订单。
40100签名验证失败对照 §03 逐字段核 stringA,重点查金额格式与嵌套序列化。
40101时间戳超出 ±300 秒校准服务器时间,用 NTP。
40102nonce 重复每次请求都生成新的随机串。
40300应用未开通该通道查通道台账当前状态,沙箱期请用 sandbox_qr
40400订单不存在核对订单号与环境,沙箱订单在生产环境查不到。
40900订单状态不允许该操作先查询当前状态再决定下一步。
42900请求频率超限按响应头 Retry-After 退避后重试。
50000平台内部错误可重试。带 request_id 提工单,2 小时内响应。
50200通道暂时不可用可重试或换通道下单,订单不会重复扣款。

§09 Limits

约定与限制

接口约定与限制
项目取值说明
金额格式字符串,两位小数0.01–50000.00,禁止浮点数。
时间格式ISO 8601 带时区2026-01-08T10:24:31+08:00
下单频率沙箱 10 次 / 秒生产按商户核定,控制台可见当前额度。
幂等窗口24 小时out_trade_no 计。
签名有效期±300 秒nonce 5 分钟内不可重复。
回调超时连接 2 秒 / 总 5 秒超时按失败进入重推队列。
notify_urlhttps,443 端口不支持自签证书,不支持 302 跳转。
接口调用费¥0下单、查询、回调重推都不计费。

本页描述 v1 接口。字段只增不删,新增字段一律可选,破坏性变更会另起版本号并提前书面通知。

§10 For your assistant

粘给编码助手

下面这段是整份手册的压缩版,字段和规则都在里面。复制给编码助手,让它直接生成对接代码,比让它去猜要准。

napay-v1.spec 结构化摘要
NAPAY 钠云付 v1 支付接口摘要(用于生成对接代码)

BASE_SANDBOX = https://api-sandbox.nayunfu.cn/api/v1
BASE_LIVE    = https://api.nayunfu.cn/api/v1
AUTH         = 报文内签名,无 Bearer token
CREDENTIALS  = app_id(可公开) + app_secret(仅服务端)

SIGN
  1. 取报文全部非空字段,排除 sign
  2. key 按 ASCII 升序,拼 "k=v",以 & 连接 -> stringA(值不做 URL 编码)
  3. 嵌套对象取紧凑 JSON:无空格、key 升序、中文不转义
  4. sign = hex_lower(HMAC_SHA256(stringA, app_secret))
  5. 验签用常量时间比较;timestamp 容差 ±300s;nonce 5 分钟去重

CREATE ORDER  POST /merchant/orders
  必填 app_id, out_trade_no, amount(string,2dp), subject, channel_code,
       notify_url(https), timestamp(sec), nonce, sign
  可选 currency(CNY), expires_in(300-7200,默认900), return_url, description,
       metadata(object,<=20 键,值为字符串)
  返回 code=0, request_id, data{trade_no, status=created, checkout_url, expires_at}
  幂等 同 out_trade_no + 同金额 24h 内返回原订单;金额不同报 40009

QUERY   GET  /merchant/orders/{trade_no} 或 ?out_trade_no=xxx(签名字段放 query)
CLOSE   POST /merchant/orders/{trade_no}/close(仅 created 可关)
RETRY   POST /merchant/notifications/{trade_no}/retry

WEBHOOK POST notify_url,JSON,字段:
  app_id, event, trade_no, out_trade_no, channel_code, amount, fee,
  settle_amount, paid_at, timestamp, nonce, sign
  event: payment.succeeded | order.closed
  处理顺序:验签 -> 核对 out_trade_no 与 amount -> 按 trade_no 幂等 -> 发货
  响应:5 秒内返回纯文本 success;失败重推 7 次(0s,15s,1m,5m,15m,1h,3h)

STATE   created -> paid -> notified -> reconciled;终止态 closed
FEE     交易费率 1.6%,settle_amount = amount - fee;接口调用不计费
ERRORS  40001 参数 / 40009 订单号重复 / 40100 验签失败 / 40101 时间戳 /
        40102 nonce 重复 / 40300 通道未开通 / 40400 订单不存在 /
        40900 状态不允许 / 42900 限流 / 50000 内部错误(可重试) /
        50200 通道不可用(可重试)

约定  金额一律字符串两位小数,禁止 float;支付结果只认回调,不认前端跳转;
      沙箱 channel_code = sandbox_qr,不涉及真实资金。