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

# ShortPay Stripe 风格支付集成

使用当前服务 origin 下的 `/v1` 有限 Stripe 子集，而不是历史 `/management` 协议。完整公开契约：`/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/metadata。
3. 将 `url` 返回给买家，不要以 URL token 作为本地订单身份。
4. 保存 Session ID、PaymentIntent ID、你的订单 ID 和首次请求使用的幂等键。
5. webhook 接收端先读原始 UTF-8 body，再验证 `Stripe-Signature: t=...,v1=...`。使用 `timestamp + '.' + raw_body` 的 HMAC-SHA256、常量时间比较和时间窗。
6. 在一笔本地事务内去重 event ID，核对 event 类型、PaymentIntent/Session、金额、货币和本地订单绑定，然后发放权益。事件至少一次投递，订单发放也必须唯一。
7. 只在事件已经持久化后返回 2xx。退款的权益调整只在 `refund.updated` 表示确认成功后执行。

## 官方 SDK webhook 示例

使用官方 Python SDK 对**原始字节**验签；不要自行解析后再序列化，也不要使用旧管理协议的事件签名：

```python
import stripe

# body 是 request 的未改动 bytes；endpoint_secret 仅保存在服务端受控配置中。
event = stripe.Webhook.construct_event(
    payload=body,
    sig_header=request.headers["Stripe-Signature"],
    secret=endpoint_secret,
    tolerance=300,
)
if event["type"] in {"payment_intent.succeeded", "checkout.session.completed"}:
    record_and_fulfill_once(event)
```

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

## 读取与恢复

- `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。没有订阅、Connect、Stripe.js、保存卡、税/运费/折扣、任意 expand 或只读密钥。
