← 商户中心

商户 API v1 接入文档

积分收款订单、托管收银台、订单查询与支付/退款 Webhook

1. 快速接入

  1. 登录平台,打开 /merchant,填写商户名称,自助开通。每个已注册账户可开通一个商户,无需管理员审批。
  2. 在商户中心创建 API 密钥。完整密钥仅显示一次,保存到你的服务端环境变量;不要放进网页、App 包、Git 或 URL。最多保留 5 个有效密钥,支持撤销与轮换。
  3. 服务端创建订单,将响应中的 checkoutUrl 提供给付款用户。
  4. 用户在平台登录并核对商户、标题和金额后确认支付积分。API 密钥不能代替用户确认、不能指定付款用户或自动扣款。
  5. 你的服务端查询订单,确认 status === "paid" 后按商户订单号幂等交付。收款进入开通者的积分账户,可站内转账或经现有审核流程提取为 USDC。

支持签名 Webhook 推送,服务端查询用于交付前核实和漏单补偿;不提供浏览器支付结果回调、部分退款或 API 链上提现。订单支付与退款均为平台内积分变动,无链上 Gas;USDC 提现使用平台已有的审核流程。

2. 基础地址、鉴权和通用约定

生产 Base URL:https://你的平台域名/api/merchant/v1。本地:http://localhost:8787/api/merchant/v1。文档中的 $BASE 包含 /api/merchant/v1,$KEY 为完整密钥。

Authorization: Bearer um_live_<64 位随机十六进制字符串>
Content-Type: application/json
Idempotency-Key: <UUID,仅创建订单必填>

3. 完整接口列表

方法路径用途
GET/account查询商户与收款账户可用/冻结余额
POST/orders创建订单,必填 Idempotency-Key
GET/orders倒序列出本商户订单,支持 before 分页
GET/orders/{id}按平台订单 UUID 查询
GET/orders/by-number/{outTradeNo}按商户订单号查询
POST/orders/{id}/cancel取消待付款订单

GET /account

{"merchantId":"商户UUID","name":"示例商店","status":"active",
 "balanceMicros":"10000000","reservedMicros":"0","currency":"POINTS","decimals":6}

余额与开通商户的用户账户共用;不是单独的商户钱包。密钥不会返回助记词、私钥、其他用户余额或提现权限。

POST /orders

curl "$BASE/orders" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $REQUEST_UUID" \
  -d '{"outTradeNo":"SHOP-20261104-001","title":"示例商品",
       "amount":"1.250001","expiresInSeconds":1800}'
请求字段类型必填规则
outTradeNostring是1–64 个字母、数字、下划线、点或短横线;本商户内永久唯一
titlestring是去除首尾空格后 1–120 字,向付款用户展示
amountstring是大于 0,最多 10 位整数和 6 位小数,不超过上述上限
expiresInSecondsinteger否60–86400,默认 1800。有效期自首次成功创建开始,不因重试延长

成功统一返回 HTTP 200 和下方订单对象。创建订单不扣任何用户积分。创建请求超时后,用同一 Idempotency-Key、同一商户订单号及完全相同的业务参数重试;也可通过商户单号查询是否已创建。

GET /orders?before={cursor}

不传 before 查询最新订单,每页最多 50 条。before 为正整数游标,使用上一页响应的 nextBefore;为 null 表示已无下一页。列表按创建顺序倒序,查询会更新已过期的未付订单状态。

{"data":[{"id":"订单UUID","cursor":"51","status":"pending","...":"完整订单字段"}],
 "nextBefore":"51"}

订单管理与筛选

订单管理页面为 /merchant/orders,支持关键词、状态、创建日期、详情、全额退款确认和 CSV 导出。创建入口保留在商户中心,可选 5 分钟、30 分钟、1 小时或 24 小时有效期。停用商户可使用登录账户查看和导出历史记录,但 Bearer API 仍整体禁止访问。

列表、统计与导出通用查询参数规则
q可选,去除首尾空格后最多 120 字。平台订单号、支付/退款流水精确匹配;商户订单号与标题包含匹配(ASCII 不区分大小写);%、_ 和引号均按普通文字处理
source可选:shop(商城订单)/ api(API 订单)。按商城购买记录关联判断,列表、统计、导出共用此筛选。
status可选:pending / paid / refunded / cancelled / expired
from / to可选的非负 Unix 秒整数,按 createdAt 筛选,范围为 from ≤ createdAt < to。两者同时提供时 from 必须小于 to;上限 253402300799。网页使用浏览器本地时区,将结束日期换算为次日零点
before仅列表分页参数;翻页时必须保留相同筛选。统计和导出忽略 before,覆盖所有筛选结果

GET /orders/summary

{"totalOrders":3,"counts":{"pending":1,"paid":1,"cancelled":0,"expired":0,"refunded":1},
 "paidAmountMicros":"5623456","refundedAmountMicros":"2500000","netAmountMicros":"3123456"}

paidAmountMicros 为筛选结果中已付款及已退款订单的原始收款总额;refundedAmountMicros 为已退款订单总额;netAmountMicros 为两者之差。金额为精确整数的字符串,累计总额可以大于单账户安全整数上限。统计按订单创建时间分组,后续支付或退款会使结果变化,不等同于支付日期结算报表。净收款不扣除账户转账、提现或其他余额变动,不能当作可用余额;列表与统计是各自查询时的状态,持续交易时可能短暂变化。

GET /orders/{id}/detail

返回完整订单对象,额外包含 payerAccountId: string | null 和 webhookEvents: []。付款账户 ID 仅用于核对原付款方,不能指定为退款参数;不返回邮箱或钱包私钥。每个回调摘要包含 id、type、status、totalAttempts、lastHttpStatus、lastError、createdAt、deliveredAt;可用回调详情接口查询投递日志,失败事件可通过既有 retry 接口补发。无回调配置时可能为空,不会追溯生成历史事件。

GET /orders/export

返回 UTF-8 BOM 的 text/csv,Content-Disposition 为附件 merchant-orders.csv;列为 id、outTradeNo、title、status、amount、amountMicros、currency、createdAt、expiresAt、paidAt、refundedAt、paymentReference、refundReference、source。所有时间为 Unix 秒,空字段用空单元格;引号和换行按 CSV 规则转义。可能被表格软件当作公式的内容会添加单引号前缀。最多 5000 条,超限返回 400 / export_limit,需缩小筛选,绝不静默截断。每商户每分钟最多导出 5 次,并计入 API 的 120 次总限额。表格软件导入时将编号和金额列设为文本以保留精度。Node.js 客户端支持 listOrders(before, filters)、orderSummary(filters)、getOrderDetail(id)、exportOrders(filters)(返回 CSV 文本)。

GET /orders/{id} 与 GET /orders/by-number/{outTradeNo}

响应为同一订单对象;不属于本商户或不存在返回 404,不泄露其他商户订单。平台 id 必须为 UUID,商户单号是创建时提交的 outTradeNo。请对商户单号进行 URL 路径编码。

POST /orders/{id}/cancel

发送 {}。仅 pending 可取消;同一已取消订单重复取消返回原订单(200)。已付款、过期或已退款返回 409。取消不涉及积分变动。

网页后台全额退款

商户公开 API 不提供退款操作,API 密钥不能发起退款。商户须登录 订单管理,打开已付款订单,点击全额退款,核对原付款账户和金额并确认。仅 paid 可全额退款,金额与收款人由原订单固定。商户可用积分不足或原付款人账户容量不足时退款失败,订单仍为 paid;补足余额后可在网页重试。成功后状态为 refunded,并生成 refundReference;重复或并发确认只退款一次。退款回到原付款人的平台积分账户,不是链上 USDC 退款。服务端可继续查询退款状态,或接收 order.refunded Webhook。

4. 订单响应字段

{
  "id":"0e4c7267-fcea-4b73-9f5f-e7be0e8597df",
  "cursor":"51",
  "merchantId":"7a08d8c2-a91c-44bb-8f4a-d1514906ca04",
  "outTradeNo":"SHOP-20261104-001",
  "title":"示例商品",
  "amount":"1.250001",
  "amountMicros":"1250001",
  "currency":"POINTS",
  "status":"paid",
  "checkoutUrl":"https://你的平台域名/checkout/0e4c7267-fcea-4b73-9f5f-e7be0e8597df",
  "paymentReference":"0d004a7d-09c3-45c2-8c39-c6130de8fcd0",
  "refundReference":null,
  "createdAt":1793750400,
  "expiresAt":1793752200,
  "paidAt":1793750420,
  "refundedAt":null
}
字段说明
id / merchantId平台订单 UUID / 商户 UUID
cursor创建顺序游标,分页使用
outTradeNo / title原商户单号 / 展示标题,创建后不能修改
amount / amountMicros积分十进制字符串 / 最小单位整数的字符串
source订单来源:shop 为商城订单,api 为 API / 商户收款订单;不能通过修改订单号前缀伪造来源。
currency固定 POINTS,不能传入其他币种
status见状态表
checkoutUrl平台托管收银台地址;必须使用部署后的真实域名配置
paymentReference付款转账流水 UUID,付款前为 null,退款后仍保留
refundReference退款转账流水 UUID,退款前为 null
createdAt / expiresAt创建及截止时间,Unix 秒
paidAt / refundedAt支付及退款完成时间,未发生时为 null

5. 状态、幂等和一致性

状态含义可操作
pending待用户确认用户付款、商户取消、到期转 expired
paid已完成积分扣款及商户入账登录网页后台全额退款
cancelled已取消,未扣款终态
expired已过期,未扣款终态;重新创建须使用新单号与新幂等键
refunded已全额退回原付款人终态

6. HTTP 状态和错误处理

{"error":{"code":"idempotency_conflict","message":"请求编号或商户订单号已用于其他订单",
          "requestId":"本次请求诊断UUID"}}
HTTPcode处理方式
400invalid_request修正参数、UUID、金额字符串或有效期
401invalid_api_key检查密钥是否完整、属于正确环境及未撤销
403merchant_disabled / merchant_required检查商户状态或联系管理员
404order_not_found / not_found检查单号及所属商户,不要切换其他用户密钥查询
409idempotency_conflict用原单号查询;不要为同一订单盲目改幂等键
409invalid_order_state重新查询订单,按状态表处理
429rate_limited停止高频调用,稍后指数退避重试
500internal_error保留原参数与幂等键,查询原订单后重试,记录 requestId

全局请求限制:JSON 请求体最多 8 KiB,超出返回 413;非 JSON POST 返回 415。这类请求在鉴权前被拒绝,可能返回简单 {"error":"说明"},接入方须同时检查 HTTP 状态,不要假设所有错误响应都是成功订单结构。超时或断网不等于订单创建失败、支付失败或退款失败,必须查询原订单确认。

7. 确认到账、轮询和业务交付

付款用户若在账户安全中启用了两步验证,邮箱或 Google 首次验证后还须完成动态码登录;已设置的支付密码及两步验证也用于收银台付款和网页退款确认。商户 API 不接收用户支付密码或动态码,不能代扣用户余额或发起退款。

  1. 将 outTradeNo、平台 id、预期 amountMicros、merchantId 和创建时的 Idempotency-Key 持久化到你的业务数据库。
  2. 用户在独立窗口或当前窗口打开 checkoutUrl;该地址支持未登录时登录后返回原订单。不要将 API 密钥拼到链接中。
  3. 你的后端按平台 id 查询,建议单订单 5–10 秒一次、逐步退避,并在所有订单之间共享 120 次/分钟预算。大量订单时使用集中队列;列表接口用于对账,不要让每个浏览器独立高频轮询。
  4. 只有后端使用 Bearer 查询得到 paid,且 merchantId、outTradeNo、amountMicros、currency 均与业务记录一致时才交付。用你的数据库唯一键/事务确保同一商户订单号只交付一次。
  5. pending 继续等待;expired / cancelled 停止等待;refunded 按你的售后规则处理。用户关闭收银台或浏览器显示“成功”均不能替代服务端查询。
  6. 需要退款时,商户登录网页订单后台确认全额退款;服务端通过订单查询或 order.refunded Webhook 确认结果并记录 refundReference,不能使用 API 密钥发起退款。

可通过签名 Webhook 触发后端查询,并用低频轮询补齐遗漏。请按下方 Webhook 协议验签与去重,不能直接信任浏览器或未验签的通知。网络失败时不要先发货,也不要新建多笔同额订单作为补偿。商户 API 的积分收款与链上充值、USDC 提现是不同流程。

8. Node.js 服务端示例

需要 Node.js 22+。下载上方客户端,环境变量 MERCHANT_API_BASE 包含 /api/merchant/v1,MERCHANT_API_KEY 为服务端密钥。

import { MerchantClient } from './merchant-client.mjs';
import { randomUUID } from 'node:crypto';
const client = new MerchantClient(process.env.MERCHANT_API_BASE,
                                  process.env.MERCHANT_API_KEY);
// 将请求编号与商户单号一起持久化;重试时从数据库读取同一个值。
const requestId = randomUUID();
const order = await client.createOrder({
  outTradeNo: 'SHOP-20261104-001', title: '示例商品',
  amount: '1.250001', expiresInSeconds: 1800
}, requestId);
console.log(order.checkoutUrl); // 提供给用户,不包含密钥
const current = await client.getOrder(order.id);
if (current.status === 'paid' && current.amountMicros === '1250001') {
  // 校验 merchantId 和商户单号,再在你的业务数据库中幂等交付。
}
// 退款请登录网页商户订单后台操作;API 仅查询退款结果。

curl 查询与取消

curl "$BASE/account" -H "Authorization: Bearer $KEY"
curl "$BASE/orders/$ORDER_ID" -H "Authorization: Bearer $KEY"
curl "$BASE/orders/by-number/SHOP-20261104-001" -H "Authorization: Bearer $KEY"
curl "$BASE/orders?before=51" -H "Authorization: Bearer $KEY"
curl "$BASE/orders/$ORDER_ID/cancel" -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -d '{}'

9. 密钥轮换、上线与验收

先创建新密钥、更新你的后端环境变量并确认查询正常,再撤销旧密钥。撤销后新的 API 请求立即拒绝,已经进入处理的请求可能完成。密钥不支持找回明文,泄露时立即撤销。

平台运营方为新数据库执行统一初始化文件 migrations/0001_initial.sql(本地 npm run db:local,线上 npm run db:remote;已有数据库按项目 README 的旧库接续说明处理),将 APP_ORIGIN 设置为真实 HTTPS 域名并部署代码及静态资源。商户端必须使用 HTTPS,localhost 仅用于本地测试。文档没有独立沙箱域名;本地测试请使用隔离数据库和虚拟积分。上线验收覆盖创建重试、登录返回收银台、确认付款、服务端查询交付、取消、过期、全额退款、余额不足、越权查询及密钥撤销。

本接口版本为 v1,新增可选响应字段应被客户端忽略。公开退款接口已按权限策略移除,退款统一在网页登录后操作;退款结果字段、查询和回调继续保留。

10. Webhook 配置、签名、重试与补偿

平台运营配置

  1. 新数据库执行 npm run db:local(线上 npm run db:remote),统一初始化已包含 Webhook 表和触发器。已有旧库先按 README 补齐原有迁移,保留业务数据和迁移记录。
  2. 用 node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))" 生成 32 字节随机十六进制加密密钥,本地填入 .dev.vars 的 WEBHOOK_ENCRYPTION_KEY;线上执行 npx wrangler secret put WEBHOOK_ENCRYPTION_KEY。该密钥用于 AES-GCM 加密商户签名密钥,必须备份、长期保留,不能直接替换。更换需先做密文重加密迁移。
  3. 在 wrangler.jsonc 的 WEBHOOK_ALLOWED_HOSTS 填入运营方审核的精确域名,用英文逗号分隔,例如 pay.your-shop.com,notify.another-shop.com。不支持通配符;请确认这些域名及其 DNS 指向由可信商户控制且始终为公网服务,不能加入内网转发域名。空值拒绝所有目标;设置启用或新地址时和每次投递前均检查。HTTPS 默认 443,拒绝 IP、账号密码、片段、localhost,且不跟随 3xx。不得加入泛域名代理、公共回显服务或会转发到内网的站点。
  4. 部署并启用每分钟 Cron。每轮最多 20 个事件,4 路并发,单次请求 10 秒超时;有积压时首投和重试会延迟,需监控投递记录。

商户设置接口

商户中心可直接配置,也可用 Bearer API:GET /webhook 返回当前配置;首次未配置返回 {"url":"","enabled":false,"version":null,"updatedAt":null}。POST /webhook 请求:

{"url":"https://pay.your-shop.com/webhooks/upoints","enabled":true,
 "version":null,"rotateSecret":false}

url、enabled、version 必填;首次 version 为 null,以后必须使用 GET 返回的 version,配置并发变化返回 409。rotateSecret 可选,默认 false。成功返回 url、enabled、version、updatedAt,首次配置、换地址或 rotateSecret=true 时额外返回 secret: "whsec_...",仅本次显示。此签名密钥与 API Bearer 密钥不同;完整 whsec_ 字符串作为 HMAC 密钥,不能去前缀或按十六进制解码。不保存于浏览器存储。

配置实际变更(包括启停、换地址和轮换)会取消之前未成功的事件;重复保存相同配置不会变更版本或取消事件,不能补发已取消事件;已经发出的网络请求无法撤回。停用商户会暂停后续投递。设置启用后仅收集新发生的支付与退款;不会追溯生成历史事件。变更时先暂停业务流量并通过订单查询补偿;生成新签名密钥后立即配置商户接收器,未配置期间请求可能验签失败,自动重试会恢复。配置响应超时导致密钥未收到时,GET 不返回明文,需刷新版本并主动轮换。

事件与 HTTP 协议

回调是 HTTPS POST、Content-Type: application/json,事件类型为 order.paid 和 order.refunded。无取消/过期事件。支付或退款的账本更新与事件写入在同一 D1 原子语句中,失败全部回滚;投递失败不会回退已成功付款或退款。

X-UPoints-Event-ID: evt_0123456789abcdef0123456789abcdef
X-UPoints-Timestamp: 1793750400
X-UPoints-Signature: v1=<64 位小写十六进制 HMAC-SHA256>
{
 "id":"evt_0123456789abcdef0123456789abcdef",
 "type":"order.paid","createdAt":1793750400,"merchantId":"商户UUID",
 "apiVersion":"2026-11-04",
 "data":{
  "id":"订单UUID","merchantId":"商户UUID","outTradeNo":"SHOP-001",
  "title":"示例商品","amount":"1.250001","amountMicros":"1250001",
  "currency":"POINTS","status":"paid","paymentReference":"转账UUID",
  "refundReference":null,"createdAt":1793750300,"expiresAt":1793752100,
  "paidAt":1793750400,"refundedAt":null
 }
}

data 是订单在该次事件发生时的固定快照(不包含列表 cursor、checkoutUrl);退款事件 type=order.refunded、data.status=refunded,refundReference/refundedAt 有值并保留原 paymentReference/paidAt。事件不含邮箱、钱包私钥或 API 密钥;所有时间均为 Unix 秒。

验签与安全交付

  1. 读取未经改写的原始 HTTP 请求体字节,保留中文、空格、字段顺序。用 HMAC-SHA256 对 timestamp + "." + rawBody 签名,密钥为完整 secret 的 UTF-8 字节,结果小写十六进制加 v1= 前缀。禁止将解析后的 JSON 重新序列化后验签。
  2. 严格解析 timestamp 和签名格式,将 timestamp 与当前 Unix 秒比较,时间差不得超过 300 秒,保持服务器校时;使用恒定时间比较。重试使用新的发送 timestamp 和签名,event.createdAt 仍是原时间。
  3. 验签后解析 JSON,核对头部事件 ID 与 body.id、merchantId 与 data.merchantId。以 body.id 唯一键持久化入队后返回任意 2xx,建议 204;重复事件返回 2xx。非 2xx、超时、网络异常均视为失败,响应体不作判断。
  4. 业务处理器按本地商户订单号核对订单、merchantId、currency、amountMicros,再通过 Bearer 查询当前订单。只有当前仍为 paid 才交付;refunded 按售后规则处理。同一订单的业务操作需唯一键/事务幂等,将业务更新与处理标记原子提交。未验证签名或金额不符绝不交付。

下载 merchant-webhook.mjs,用 Node.js 22.13+(推荐 24)运行:设置环境变量 WEBHOOK_SECRET、MERCHANT_ID,可选 WEBHOOK_DB、PORT,然后 node merchant-webhook.mjs。示例在 127.0.0.1:3001 的 /webhooks/upoints 接收并持久化至 SQLite inbox,需由你的 HTTPS 反向代理公开该路径。它不自动发货;请在你的业务处理器中消费 inbox 并完成上述校验和幂等事务。多实例接收器应使用共享持久数据库。

重试、顺序与人工补偿

系统提供至少一次投递,同一事件的 ID 和请求体保持不变,可能重复或乱序,尤其支付与紧随其后的退款可同时到达;不能依赖到达顺序或旧快照判定当前状态。持久化 inbox 后尽快返回成功,耗时业务异步处理。接收器异常前可能已经提交业务,所以即使上次超时仍必须去重。

每轮自动最多 8 次尝试(首次加 7 次重试),失败间隔依次为 60、300、900、3600、10800、21600、43200 秒。Cron 每分钟检查,实际执行不会早于 nextAttemptAt,有积压可更晚。第 8 次失败后 status=failed;投递进程崩溃的租约 120 秒后恢复,已消耗的尝试次数不回退。

接口(均需本商户 Bearer)说明
GET /webhook/events?before={cursor}倒序列表,每页最多 50 条;data、nextBefore 分页
GET /webhook/events/{eventId}包含 event 原始快照及最近 50 次 deliveryAttempts
POST /webhook/events/{eventId}/retry,JSON {}仅失败且仍为当前启用配置的事件可重新入队;pending 重复调用幂等;已成功、取消、处理中返回 409。每商户 10 次/分钟

列表字段:id、cursor、orderId、type、url、status(pending/processing/delivered/failed/cancelled)、attempts(本轮次数)、totalAttempts(终身次数)、nextAttemptAt(仅待发送/处理中,否则 null)、lastHttpStatus、lastError、createdAt、deliveredAt。详情额外 event、deliveryAttempts;每次记录 attempt、startedAt、finishedAt、httpStatus、error。手动补发重置本轮 attempts,保留总次数和旧日志;不会改变订单或重复扣款。平台不保存响应体、签名或明文签名密钥。processing 的 nextAttemptAt 是本轮原定时间,lease 到期恢复不代表接收器未处理。

错误码:400 invalid_webhook_url / webhook_host_not_allowed / invalid_request;409 webhook_config_conflict / webhook_retry_conflict;404 webhook_event_not_found;503 webhook_not_configured;429 rate_limited。投递日志常见原因 timeout、http_XXX、network_or_signing_error、lease_expired、configuration_changed。配置变更、服务中断、未启用回调或队列积压时,用订单接口低频查询对账补齐;不要将通知作为唯一账本。

商户名称与商城库存

在平台网页的商户中心可修改显示名称,商城链接、商户 ID 和 API 密钥不变,收银台将显示新名称。商城购买支持每单 1–100 件;同一买家重复点击完全相同且未过期的未付款订单时复用原单,整单扣款并按数量自动发货。商城订单查询额外返回 quantity(件数)和 unitAmountMicros(下单时单价的最小单位字符串),amountMicros 为整单总额。商品与库存页面提供状态筛选、数量统计、编号搜索、未售卡密核对及批量停用/恢复;停用库存不再参与发货,已售库存和退款后的原交付记录保持锁定。敏感操作沿用已开启的安全验证并记录审计。

以上功能仅在登录后的网页后台操作,不属于本 Bearer API。公开订单、Webhook 和商品列表均不返回卡密内容。