接入支付宝收款
BBpay 替你处理支付宝的应用申请、签名验签和异步通知。你只需要一个 API Key:后端调一次接口拿到收银台链接, 买家付完款,BBpay 用 Webhook 通知你的网站。
bbp_test_ 开头的 Key 走支付宝沙箱,不扣真钱,不用绑定就能跑通流程;bbp_live_ 开头的 Key 真实收款,需要先在后台「收款账户」绑定你自己的支付宝。两种 Key 的接口完全一样。快速开始
- 开通并拿到 API Key。在 bbpay.uk/signup 付款开通,开通后会直接显示一个
bbp_test_开头的 Key(只显示一次)。之后可以在后台「API 密钥」页随时新建或作废。 - 后端创建订单。用 Key 调
POST /api/vibe/orders,拿到pay_url,把买家带过去付款。 - 确认付款。在后台「Webhook」页填你的接收地址,付款成功时 BBpay 会 POST 通知你;也可以随时调查询接口确认。
curl -X POST https://bbpay.uk/api/vibe/orders \
-H "Authorization: Bearer $BBPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"out_trade_no": "ORDER-1001",
"amount": "9.90",
"subject": "AI 简历润色 · 单次",
"return_url": "https://your.site/thanks",
"channel": "page"
}'让 AI 帮你接
我们把这份文档整理成了一份给 AI 读的纯文本:https://bbpay.uk/llms.txt。在 Cursor、Claude Code、 Windsurf 里把下面这段话发给 AI,它会自己读文档并写好代码。
读一下 https://bbpay.uk/llms.txt ,用 BBpay 给我的网站接入支付宝付款: 1. 后端加一个下单接口,调 BBpay 创建订单,返回 pay_url,前端跳过去付款; 2. 加一个 Webhook 接口,验签后把订单标记为已付款; 3. API Key 和 Webhook 密钥从环境变量 BBPAY_KEY、BBPAY_WEBHOOK_SECRET 读。
鉴权
所有接口都用请求头带 API Key:
Authorization: Bearer bbp_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- Key 只能放在你的服务器端(环境变量),不要写进前端代码或提交到 Git。
- 泄露了就到后台「API 密钥」页作废,再建一个新的,旧 Key 立即失效。
- 套餐到期后有 3 天宽限期,之后接口返回
402;在后台「账号与续费」续费后立刻恢复,Key 不用换。
创建订单
POST/api/vibe/orders
创建一笔支付宝订单,返回收银台链接。买家打开链接后用支付宝扫码或登录付款,订单 15 分钟内未付自动关闭。
请求参数(JSON)
| 参数 | 必填 | 说明 |
|---|---|---|
out_trade_no | 是 | 你的订单号,在你自己的订单里唯一即可,和其他商户重复也没关系。只能用字母、数字、_、-,最长 64 位,不能以 SUB 开头(BBpay 内部保留)。建议带上你的前缀,例如 LF-20261101-0042。 |
amount | 是 | 金额,单位元,最多两位小数,例如 "9.90"。范围 0.01 ~ 100000000。 |
subject | 是 | 商品名称,会显示在支付宝收银台和买家账单里,最长 256 字。 |
return_url | 否 | 买家付完款后跳回的地址。跳回只代表买家离开了收银台,是否付款以 Webhook 或查询结果为准。 |
channel | 否 | page(电脑网页,默认,显示二维码)或 wap(手机网页,唤起支付宝 App)。 |
notify_url | 否 | 这一单的回调地址。带了它,这一单的结果改为以表单格式推到这里,不再推你在后台填的 Webhook 地址。适合只认表单回调的商城系统,见 按单回调。 |
返回
{
"out_trade_no": "ORDER-1001",
"alipay_out_trade_no": "ORDER-1001",
"livemode": false,
"pay_url": "https://openapi-sandbox.dl.alipaydev.com/gateway.do?..."
}- 同一个
out_trade_no在未付款时重复调用(金额相同),会返回新的收银台链接,方便买家重新付款。 - 订单号已付款、已关闭,或金额不同,返回
409,请换一个新订单号。 - 返回里的
alipay_out_trade_no是发给支付宝的单号,一般和你的out_trade_no一样;万一和其他商户撞号,BBpay 会另生成一个。只有去支付宝账单里对账时才用得到它,查单和 Webhook 都用你自己的订单号。
查询订单
GET/api/vibe/orders/:out_trade_no
查一笔订单的最新状态。还没付款的订单,BBpay 会实时向支付宝补查一次,所以即使 Webhook 还没到,也能马上知道买家是否付了款。 适合在买家跳回 return_url 时调用。
curl https://bbpay.uk/api/vibe/orders/ORDER-1001 \ -H "Authorization: Bearer $BBPAY_KEY"
trade_status
| 值 | 含义 |
|---|---|
WAIT_BUYER_PAY | 等待付款 |
TRADE_SUCCESS | 付款成功,可以发货 |
TRADE_FINISHED | 交易完结(付款成功且不可退款),同样可以发货 |
TRADE_CLOSED | 超时未付或已关闭 |
只能查到用自己 Key 创建的订单,别人的订单一律返回 404。返回里的 refunded_amount 是已退成功的总额,refunds 是这笔订单的退款记录。
退款
POST/api/vibe/refunds
把一笔已付款订单的钱原路退给买家。钱从你自己的支付宝账户退出,BBpay 不经手资金;用 bbp_test_ Key 下的单在沙箱里退。
| 参数 | 必填 | 说明 |
|---|---|---|
out_trade_no | 是 | 要退款的订单号(下单时你传的那个) |
out_refund_no | 是 | 你的退款单号,同一笔订单里唯一。字母、数字、_、-,最长 64 位。 |
refund_amount | 是 | 这次退多少,单位元,最多两位小数 |
reason | 否 | 退款原因,买家在支付宝账单里能看到 |
curl -X POST https://bbpay.uk/api/vibe/refunds \
-H "Authorization: Bearer $BBPAY_KEY" \
-H "Content-Type: application/json" \
-d '{"out_trade_no":"ORDER-1001","out_refund_no":"ORDER-1001-R1","refund_amount":"9.90","reason":"买家申请退款"}'status:succeeded退款成功;pending支付宝还在处理或结果未知;failed没退成(HTTP422,原因在error,例如支付宝账户余额不足)。- 遇到
pending、超时或网络错误,用同一个out_refund_no和金额再调一次即可,不会重复退款; 也可以过十几秒调查询订单接口,看refunds里的状态。 - 可以分多次部分退款,累计不能超过订单金额。全额退完后支付宝会关闭这笔交易,你还会收到
order.closed。 - 退款成功会推
refund.succeeded事件(下单带了notify_url的推表单)。 - 交易完结(
TRADE_FINISHED,超过退款期限)后支付宝不允许再退。沙箱里只能全额退一次。
Webhook 通知
在后台「Webhook」页填一个 https 地址,订单状态变化时 BBpay 会向它发 POST。页面上还能发测试事件、看每次推送的结果、手动重发。
| 事件 | 什么时候 |
|---|---|
payment.succeeded | 买家付款成功 |
order.closed | 订单超时未付或被关闭(全额退款后也会关闭) |
refund.succeeded | 一笔退款成功。data.refund 是这次的退款,data.refunded_amount 是累计已退 |
webhook.test | 你在后台点「发送测试事件」 |
请求内容
POST https://your.site/api/bbpay-webhook
BBpay-Signature: t=1793608212,v1=5f3c…e81a
BBpay-Event: payment.succeeded
BBpay-Delivery: evt_9Qm3xK2pLw8r
{
"id": "evt_9Qm3xK2pLw8r",
"type": "payment.succeeded",
"created": "2026-11-02T08:30:12.000Z",
"livemode": false,
"data": {
"out_trade_no": "ORDER-1001",
"trade_no": "2026110222001498290509998118",
"trade_status": "TRADE_SUCCESS",
"amount": "9.90",
"subject": "AI 简历润色 · 单次",
"channel": "page",
"buyer_logon_id": "kve***@sandbox.com",
"paid_at": "2026-11-02T08:30:10.000Z",
"created_at": "2026-11-02T08:29:40.000Z"
}
}验证签名
每个请求都带 BBpay-Signature: t=时间戳,v1=签名。签名是用你的 Webhook 密钥(whsec_ 开头,在后台查看) 对 ${t}.${原始请求体} 做 HMAC-SHA256 得到的十六进制串。请务必:
- 用原始请求体验签,不要先解析成 JSON 再序列化;
- 拒绝
t与当前时间相差超过 5 分钟的请求,防止重放; - 按
data.out_trade_no去重,同一事件可能送达多次。
// Next.js:app/api/bbpay-webhook/route.ts
import crypto from "node:crypto";
export async function POST(req: Request) {
const body = await req.text(); // 一定要用原始请求体验签
const header = req.headers.get("bbpay-signature") ?? "";
const { t, v1 } = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
const expected = crypto.createHmac("sha256", process.env.BBPAY_WEBHOOK_SECRET!)
.update(`${t}.${body}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300; // 拒绝 5 分钟前的请求,防重放
const ok = fresh && v1?.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(expected, "hex"));
if (!ok) return new Response("bad signature", { status: 400 });
const event = JSON.parse(body);
if (event.type === "payment.succeeded") {
// 按 out_trade_no 去重:同一事件可能送达多次
await markOrderPaid(event.data.out_trade_no, event.data.amount);
}
return new Response("ok"); // 8 秒内返回 2xx,否则会重试
}送达与重试
8 秒内返回 2xx 算送达(不跟随重定向)。失败后按 1 分钟、5 分钟、30 分钟、2 小时、6 小时、12 小时、24 小时重试,共 8 次, 重试时事件 id 不变。测试事件不重试。
按单回调(notify_url)
下单时带 notify_url,这一单付款成功或关闭时,BBpay 向它 POST application/x-www-form-urlencoded 表单, 重试规则同上。要先在后台「Webhook」页保存过一次地址,签名用的是那里的 whsec_ 密钥。
- 字段:
event、out_trade_no、trade_no、trade_status、amount、livemode(true/false)、paid_at、notify_id、timestamp(秒)、sign。 退款成功(event=refund.succeeded)时另有refund_no、refund_amount、refunded_amount。 - 验签:去掉
sign和值为空的字段,按字段名升序拼成k=v&k=v(不做 URL 编码), 用whsec_密钥做 HMAC-SHA256,得到的小写十六进制和sign比对;并拒绝timestamp超过 5 分钟的请求。 - 返回任意 2xx(例如
success)算送达。按out_trade_no去重,核对amount后再发货。
错误码
出错时返回对应的 HTTP 状态码,响应体是 { "error": "说明" },支付宝报错时还会带 sub_code。
| 状态码 | 原因 | 怎么办 |
|---|---|---|
400 | 参数缺失或格式不对 | 按 error 的说明修正参数 |
401 | API Key 无效或已作废 | 检查请求头,或在后台新建 Key |
402 | 套餐已到期且过了 3 天宽限期 | 在后台「账号与续费」续费 |
403 | 用正式 Key 下单,但还没绑定支付宝收款账户 | 在后台「收款账户」扫码绑定 |
404 | 订单不存在 | 确认订单号,以及是不是用同一个账号的 Key 创建的 |
409 | 订单号已存在(金额不同或已付款 / 已关闭) | 换一个新的订单号 |
409 | 退款:订单未付款 / 已关闭 / 已完结,或累计退款超过订单金额 | 查一下订单状态和已退金额 |
422 | 退款没退成(原因在 error,例如支付宝账户余额不足) | 处理后用同一个退款单号重试 |
502 | 支付宝返回错误 | 看 sub_code;稍后重试 |
503 | 服务暂时不可用 | 稍后重试 |
打开 pay_url 后,支付宝收银台如果显示 insufficient-user-permissions(用户权限不足), 说明你绑定的支付宝还没签约对应的产品:channel=page 要签「电脑网站支付」,channel=wap 要签「手机网站支付」。 在支付宝商家平台 b.alipay.com →「产品中心」签约后,重新下单即可,不用重新绑定。
测试和正式
- 用
bbp_test_Key 下的单走支付宝沙箱,pay_url打开的是沙箱收银台,不会扣真钱,也收不到真钱。 - 沙箱收银台只能用支付宝沙箱买家账号付款,普通支付宝 App 扫码会提示无效。
- 用
bbp_live_Key 下的单是真实收款,钱直接进你绑定的支付宝。没绑定会返回 403,先去后台「收款账户」扫码绑定。 - 接口地址和参数两种 Key 完全一样。订单和 Webhook 里的
livemode字段告诉你这笔是正式(true)还是测试(false), 你的网站只在livemode为 true 时给买家真实发货。 - 在后台「API 密钥」页可以分别新建测试 Key 和正式 Key,开发时用测试 Key,上线时换成正式 Key 即可。
开通正式收款
正式收款时,钱直接从买家进到你自己的支付宝,BBpay 只代你调用支付宝接口,不经手资金。 所以你的支付宝需要自己具备收款产品。按你的情况选一条路:
企业
准备企业支付宝账号。在支付宝商家平台 b.alipay.com →「产品中心」签约「手机网站支付」和「电脑网站支付」, 然后在后台「收款账户」扫码绑定,新建正式 Key。
个体户
准备营业执照,支付宝账号名称要和执照一致。之后同企业:先签约手机网站支付 / 电脑网站支付,再扫码绑定。
个人(没有营业执照)
支付宝目前一般不给个人开通网站支付。我们正在接入支付宝「即刻收」:开通后,个人用实名认证的支付宝、设置好价格就能收款, 钱同样直接进你的支付宝。在此之前,可以先用测试 Key 把接入做完,开通后换成正式 Key 即可。
- 手机上付款用
channel=wap(需要「手机网站支付」),电脑上用channel=page(需要「电脑网站支付」)。只签了其中一个,就只用对应的channel。 - 电脑网站支付通常要求网站能正常访问,并有和支付宝账号主体一致的 ICP 备案;没有备案时,支付宝可能只给试用期或单笔、单日限额,以签约页面为准。
- 没签约也能绑定,但买家打开收银台会看到「用户权限不足」(
insufficient-user-permissions)。签约后重新下单即可,不用重新绑定。 - 小程序里的 JSAPI 支付只能在支付宝小程序内拉起,网站和手机网页用不了,BBpay 暂不支持。
- BBpay 不会用自己的账户替你代收再转账给你。资金始终由支付宝直接结算到你的账户。