---
name: shortpay-payment-integration
description: Use when integrating a server-side one-time payment flow with RockPay. Create idempotent Checkout Sessions and fulfill only from verified RockPay events.
---

> 生产主站和 API 使用 https://rockpay.ai，托管结账使用 https://checkout.rockpay.ai。既有 shortpay.ai、checkout.shortpay.ai 和已配置的旧域名仍可直接访问；新接入使用 RockPay 域名。

# RockPay支付集成

生产服务地址：网站、公共文档与管理页面使用 `https://rockpay.ai`；服务端 API base 使用 `https://rockpay.ai`；托管结账、同源 `/checkout-api/*` 与 `/embed.js` 使用 `https://checkout.rockpay.ai`。管理浏览器请求保持与 `https://rockpay.ai` 同源。服务端 API 与网站、公共文档共用主域名。

使用 `https://rockpay.ai` 下的 `/v1` 有限 RockPay API，而不是历史 `/management` 协议。完整公开契约：`https://rockpay.ai/v1/docs?format=markdown`。

## 安全边界

- 只在你的服务端保存 `Authorization: Bearer <SITE_SERVER_KEY>`；绝不发送到浏览器、移动端、前端变量、日志、分析或提示词。
- 下单使用 `POST /v1/checkout/sessions` 和稳定 `Idempotency-Key`。超时后原样重试，不得用新键或新 Session 猜测结果。
- 把返回的 Session `url` 原样交给已验证买家。其 `token` 是短期能力，不是可记录或分享的标识。
- 浏览器回跳、iframe 消息、Session 轮询和 `processing` 状态都不能履约。只接受已验证的 `payment_intent.succeeded` / `checkout.session.completed` 事件，或服务端确认读取后的等价事实。
- 不要自己接收卡号/CVC。直接卡片结账由托管 checkout 页面和 capability 端点完成。

## 服务端流程

1. 先在本地创建不可变订单，保存商品、整数最小货币单位、货币、买家关联和稳定业务订单 ID。
2. 以 `application/x-www-form-urlencoded` 调用 `POST /v1/checkout/sessions`：`mode=payment`、连续编号的 `line_items`、允许来源内的 `success_url`/`cancel_url`，以及当前购买者的有效 `customer_email`。在业务侧购买流程中先取得邮箱，不发送空值或占位地址；不要把邮箱当成订单所有权证明。`client_reference_id`/metadata 可用于业务关联。现行接口仍允许省略邮箱，且 `customer` 与 `customer_email` 不能同传；若旧接入仅传 `customer`，先迁移该路径。RockPay 将在业务侧改造并验证后另行启用邮箱必填校验，不能提前假定服务端已拒绝缺失邮箱的请求。
3. 将 `url` 返回给买家，不要以 URL token 作为本地订单身份。
4. 保存 Session ID、PaymentIntent ID、你的订单 ID 和首次请求使用的幂等键。
5. 在站点配置中设置由你控制的 HTTPS `event_url`，服务端安全保存 `event_secret`。接收端接受 `POST application/json`，先读取原始 UTF-8 body，再验证 `ShortPay-Signature: t=...,v1=...`。使用 `timestamp + '.' + raw_body` 的 HMAC-SHA256、常量时间比较和时间窗；不使用旧管理协议的 `X-ShortPay-*` 头。
6. 在一笔本地事务内去重 event ID，核对 event 类型、PaymentIntent/Session、金额、货币和本地订单绑定，然后发放权益。同一付款可同时产生 `payment_intent.succeeded` 和 `checkout.session.completed`，两者与服务端查询共用按付款 ID 唯一的履约逻辑。对于完成的结账事件，还必须确认 `payment_status=paid`；仅凭事件类型不能履约。
7. 只在事件已经持久化后返回 2xx。退款的权益调整只在 `refund.updated` 的 `data.object.status=succeeded` 后执行。非 2xx、网络错误或超过 10 秒的响应会重投；当前最多尝试 10 次，之后须由授权方按事件 ID 检查和请求补送。完整事件示例、重试边界和恢复方法见[商户通知契约](https://rockpay.ai/v1/docs?format=markdown)。

支持失败通知的通道会另外通知 `payment_intent.payment_failed`。验签和核对本站绑定后，服务端重新读取原 Session / PaymentIntent，仅在确认失败且未收款时同步原单失败；这不是权益事件或自动重付指令。未知结果继续查询原单，迟到成功仍走现有幂等履约。用户主动再次购买且原单可安全重建时，才创建新订单和新幂等键；未支持该事件的通道及历史失败记录不变。

## 标准库 webhook 验签示例

使用 Python 标准库对**原始字节**验签；不要解析后再序列化。将原始 body、ShortPay-Signature 头（缺失时为空字符串）及服务端保存的 endpoint_secret 传给 verify_event；捕获 ValueError 并拒绝请求，不履约。示例拒绝缺失或无效签名，以及距当前时间超过 300 秒的过去或未来时间戳；先验签再解析 JSON，不使用管理协议签名：

```python
import hashlib
import hmac
import json
import re
import time


def verify_event(raw_body, signature_header, endpoint_secret, *, now=None, tolerance=300):
    if not isinstance(raw_body, bytes) or not endpoint_secret or tolerance <= 0:
        raise ValueError("Invalid verification input")
    match = re.fullmatch(r"t=([0-9]{1,12}),v1=([0-9a-f]{64})", signature_header or "")
    if match is None:
        raise ValueError("Missing or invalid ShortPay-Signature")
    timestamp, supplied = match.groups()
    current_time = time.time() if now is None else now
    if abs(current_time - int(timestamp)) > tolerance:
        raise ValueError("Timestamp outside tolerance")
    expected = hmac.new(
        endpoint_secret.encode("utf-8"),
        timestamp.encode("ascii") + b"." + raw_body,
        hashlib.sha256,
    ).hexdigest()
    if not hmac.compare_digest(expected, supplied):
        raise ValueError("Invalid signature")
    return json.loads(raw_body)

```

先对 event ID 和你的订单/付款绑定做事务性去重；验签通过不替代金额、币种和订单关联校验。

## 读取与恢复

- `GET /v1/account` 只验证站点身份；`charges_enabled`/`payouts_enabled` 为未知，不代表结算能力。
- `GET /v1/charges` 与 `/v1/events` 提供已确认收入和不可变 `charge.succeeded` 快照，支持时间筛选与游标。它们不是银行结算报告。
- `GET /v1/payment_intents/{id}` 可帮助显示状态，但 `requires_payment_method`、`processing`、`pending`、`unknown` 都不是成功。
- 创建退款使用 `POST /v1/refunds`。模糊或未完成结果不要创建第二笔退款；重用同一 Idempotency-Key，并等待确认事件或受控恢复流程。

## 当前限制

仅支持固定 `2025-03-31.basil` 的一次性支付子集：Customer、Checkout Session、PaymentIntent 读取、Refund、Account、Charges、Events。没有订阅、跨账户代理、浏览器支付 SDK、保存卡、税/运费/折扣、任意 expand 或只读密钥。

## 主动查询支付结果

`GET /v1/checkout/sessions/{id}` / `GET /v1/payment_intents/{id}`

在业务站服务端调用以下任一接口，使用创建 Session 时返回的 `id`（`cs_...`）或 `payment_intent`（`pi_...`）。业务站自己的订单号不能代替这两个 ID。

两个接口均使用本站点服务端 Bearer 密钥。存在支付尝试时，每次读取都会主动核验原支付结果并同步已确认状态，包括此前已成功的付款；尚未发起支付时，返回未支付对象，不发起扣款。

判断付款成功：Session 的 `payment_status` 为 `paid`，或 PaymentIntent 的 `status` 为 `succeeded`。不要仅凭 Session 的 `status` 判断付款成功；其值为 `open`、`complete` 或 `expired`。

查询超时、查询服务不可用、结果无法验证或暂时无法确认最终性时，返回 `502` / `api_error`，不是未支付或付款失败。错误响应：

```json
{"error":{"type":"api_error","message":"Payment inquiry unavailable or unverified","param":null,"code":null}}
```

稍后使用同一 ID 重试；不要因查询失败重新创建支付或扣款。查询确认成功后，核对金额、币种及本地订单归属，并与 Webhook 共用按支付 ID 去重的履约逻辑，防止重复发放。
