# ShortPay 公共 API 文档

ShortPay 提供一个**有限的 Stripe 风格一次性支付协议**。本文公开可读；调用其中的业务 API 仍必须在受信任服务端使用本站点的 Bearer 密钥。它不是完整 Stripe 实现，也不提供浏览器密钥、订阅、Connect、结算或任意资源列表。

- 人类文档：`/docs`
- 原始 Markdown：`/v1/docs?format=markdown`
- 机器目录：`/v1/docs?format=catalog`
- 集成 skill：`/v1/docs?format=integration-skill`
- 数据读取 skill：`/v1/docs?format=data-skill`

所有 URL 均为相对路径；以当前服务 origin 作为 API base URL。

## 快速开始

服务端创建 Checkout Session，随后只把响应中的 `url` 交给购买者：

```bash
export SHORTPAY_API_BASE='https://payments.example'
export SHORTPAY_SITE_KEY='<SITE_SERVER_KEY>'

curl -X POST "$SHORTPAY_API_BASE/v1/checkout/sessions" \
  -H "Authorization: Bearer $SHORTPAY_SITE_KEY" \
  -H 'Idempotency-Key: order-2026-0001' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'mode=payment' \
  --data-urlencode 'line_items[0][price_data][currency]=usd' \
  --data-urlencode 'line_items[0][price_data][unit_amount]=1200' \
  --data-urlencode 'line_items[0][price_data][product_data][name]=Example credits' \
  --data-urlencode 'line_items[0][quantity]=1' \
  --data-urlencode 'success_url=https://merchant.example/order/complete?session_id={CHECKOUT_SESSION_ID}' \
  --data-urlencode 'cancel_url=https://merchant.example/order/cancelled'
```

典型响应（字段会因资源状态变化）：

```json
{"id":"cs_example","object":"checkout.session","mode":"payment","status":"open","payment_status":"unpaid","amount_total":1200,"currency":"usd","payment_intent":"pi_example","url":"https://payments.example/checkout/cs_example?token=<SHORT_LIVED_CAPABILITY>"}
```

`url` 是短期购买者能力链接，不是 API 密钥：只交给已验证的购买者；不要写入日志、分析系统或长期存储。回跳、嵌入页消息和浏览器轮询都不是付款确认。履约必须来自已验证的事件或服务端读取的确认状态。

## 认证、版本与通用规则

- 所有 `/v1/...` 业务端点均要求 `Authorization: Bearer <SITE_SERVER_KEY>`；只接受恰好对应一个站点的服务端密钥。缺失或无效密钥为 `401`，非站点身份为 `403`。
- 固定兼容版本为 `2025-03-31.basil`。可省略 `Stripe-Version`；若发送，必须完全相同。每个成功或错误响应都会带回该版本头。
- 写操作使用 `application/x-www-form-urlencoded`。GET 不接受未列出的查询参数。表单最大 64 KiB，重复参数、冲突嵌套结构、非法编码和未支持字段都会被拒绝。
- 金额为最小货币单位的十进制整数；创建 Session 的 `currency` 必须为小写，且一个订单只能有一种货币。
- 成功读写均返回 Stripe 风格 JSON 对象。错误统一为：

```json
{"error":{"type":"invalid_request_error","message":"Invalid or unsupported parameter","param":"field","code":"parameter_invalid"}}
```

常见状态：`400` 参数/版本/状态无效，`401` 认证失败，`403` 站点权限不足，`404` 资源不存在或不属于本站点，`409` 幂等键与请求体不一致，`413` 请求过大，`503` 当前退款能力不可用。

### 幂等性

写请求可带 `Idempotency-Key`（1–200 个可打印 ASCII 字符）。键按“站点 + 操作”隔离：相同键和完全相同表单会返回第一次保存的对象；相同键配不同参数返回 `409` / `idempotency_key_in_use`。网络超时后必须重用同一键和同一请求，不能新建订单或退款。

## 公共 Stripe 子集

### Account 与收入读取

| 方法 | 路径 | 用途 |
|---|---|---|
| GET | `/v1/account` | 返回稳定的本站点 account 对象；`charges_enabled`、`payouts_enabled` 始终为 `null`（未知）。 |
| GET | `/v1/charges` | 返回已确认收款的 Charge 列表。 |
| GET | `/v1/events` | 返回不可变的 `charge.succeeded` 收入快照列表，不是 webhook 投递列表。 |

`/v1/charges` 和 `/v1/events` 支持 `limit`（1–100，默认 10）、互斥的 `starting_after` / `ending_before`，以及 `created` 精确 Unix 秒或 `created[gt]`、`[gte]`、`[lt]`、`[lte]`。结果为最新优先的 `{ "object":"list", "data":[], "has_more":false }`。Events 只接受可选的单一 `types[]=charge.succeeded`。游标必须属于当前站点和当前集合。

Account 对象恰含 `id`、`object:"account"`、`charges_enabled:null`、`payouts_enabled:null`；不接受查询参数。

Charge 对象包含 `id`、`object:"charge"`、`created`、`amount`、`currency`、`paid:true`、`status:"succeeded"`、`customer`、`billing_details.email`、`receipt_email:null`、`refunded`、`amount_refunded`、`livemode`。`created` 是首次验证支付成功的时间；Customer/邮箱可能为空。`refunded` 只有全额退款才为 true，部分退款仍须读取 `amount_refunded`。

收入 Event 包含 `id`、`object:"event"`、`api_version`、`created`、`type:"charge.succeeded"`、`livemode`、`data.object`（首次确认的 Charge 快照）、`pending_webhooks:0`、`request:null`。它不是 webhook 投递历史，不会为此生成新的商户履约通知。列表还返回 `url`，分别为 `/v1/charges` 或 `/v1/events`。

Charge 反映已确认付款；`amount_refunded` 只统计已确认退款。Event 是首次确认时的不可变快照，之后的退款不会改写它。两者都不是结算、打款或银行对账证据。

`types[0]=charge.succeeded` 也可用。`created` 不与范围条件混用，`gt/gte` 及 `lt/lte` 各自互斥，时间范围必须有效。Charge 按确认时间和稳定顺序倒序，Event 按记录顺序倒序；时间过滤分别针对各自的 `created`。`starting_after` 向后翻页，`ending_before` 向前翻页，返回页内仍保持倒序。

### Customers

| 方法 | 路径 |
|---|---|
| POST | `/v1/customers` |
| GET | `/v1/customers/{customer_id}` |
| POST | `/v1/customers/{customer_id}` |

创建和更新只接受以下表单字段：

| 字段 | 说明 |
|---|---|
| `email` | 可为空以清除；非空时必须是有效邮箱。 |
| `name` | 可为空以清除；最长 500 字符。 |
| `address[country]` | 可选 ISO 3166-1 alpha-2 国家码。 |
| `address[postal_code]`、`line1`、`line2`、`city`、`state` | 可选地址字符串。 |
| `metadata[key]` | 最多 50 个字符串值；键最长 40 字符。传空 `metadata` 清空全部；空值删除单个键。 |

Customer 响应包含 `id`、`object:"customer"`、`created`、`livemode`、`email`、`name`、`address` 和 `metadata`。不支持 Customer 列表、删除或任意字段扩展。

### Checkout Sessions

| 方法 | 路径 |
|---|---|
| POST | `/v1/checkout/sessions` |
| GET | `/v1/checkout/sessions/{session_id}` |
| GET | `/v1/checkout/sessions/{session_id}/line_items` |
| POST | `/v1/checkout/sessions/{session_id}/expire` |

创建 Session 必须有：

| 字段 | 规则 |
|---|---|
| `mode` | 必须为 `payment`。 |
| `line_items[N][price_data][currency]` | 小写、受支持货币；所有条目一致。 |
| `line_items[N][price_data][unit_amount]` | 最小单位整数，0 至 1,000,000,000。 |
| `line_items[N][price_data][product_data][name]` | 商品名；不支持 `#`、`&`、`+`。 |
| `line_items[N][quantity]` | 正整数。 |
| `success_url`、`cancel_url` | 必填 HTTPS 回跳地址，origin 必须属于站点预配置允许来源；无凭据、无 fragment。 |

可选字段：`customer`（已有 Customer ID）、`customer_email`、`customer_creation`（`if_required` 或 `always`）、`customer_update[address]`（`auto` 或 `never`）、`client_reference_id` 和 `metadata`。`customer_update[address]` 仅可与 `customer` 一起使用；`customer` 不能与 `customer_email` 或 `customer_creation` 同时使用。Session 最多 100 个连续编号的条目（从 `0` 开始）。总金额必须在 1 至 1,000,000,000。

Session 对象包含：`id`、`object`、`mode`、`created`、`expires_at`、`livemode`、`status`、`payment_status`、`amount_subtotal`、`amount_total`、`currency`、`customer`、`customer_email`、`customer_creation`、`customer_details`、`client_reference_id`、`metadata`、`payment_intent`、`success_url`、`cancel_url`、`url`。GET Session 仅可用 `expand[]=line_items` 或 `expand[0]=line_items` 展开条目；完整条目也可使用专用 line-items 路径读取。

`POST .../expire` 发送空表单。它只能使 `open` Session 过期，返回 `status:"expired"` 且 `url:null`。它不取消已经开始的付款；晚到的已确认付款仍可成功。

### PaymentIntents 与 Refunds

| 方法 | 路径 |
|---|---|
| GET | `/v1/payment_intents/{intent_id}` |
| POST | `/v1/refunds` |
| GET | `/v1/refunds/{refund_id}` |

PaymentIntent 包含 `id`、`amount`、`amount_received`、`currency`、`customer`、`metadata`、`status`。状态如 `requires_payment_method`、`processing` 并不代表成功；只有已确认结果才可履约。

创建 Refund 的表单：

| 字段 | 规则 |
|---|---|
| `payment_intent` | 必填，必须属于当前站点。 |
| `amount` | 可选正整数；省略时申请当前未保留余额。 |
| `reason` | 可选：`duplicate`、`fraudulent`、`requested_by_customer`。 |
| `metadata[key]` | 可选，遵循 Customer metadata 规则。 |

Refund 返回 `id`、`object:"refund"`、`created`、`amount`、`currency`、`payment_intent`、`status`、`reason`、`metadata`。`pending`、`failed` 或未知结果都不是完成退款；不要以新请求重试模糊结果。确认成功后再调整权益。

## 托管直接卡片结账

买家仅通过 Session `url` 打开 `/checkout/{session_id}?token=...`。页面在受控同源请求中向 `/checkout-api/{session_id}/pay-card` 提交卡片信息；卡号与 CVC 不应由商户服务器、日志或分析工具接收。请求还需 Session capability、精确同源 `Origin` 和 JSON body：`email`、`name`、`billing_address`、`os_type`、`card`。服务端派生金额和账户，单个 Session 只允许一次尝试。

支付结果只能作为页面显示：`open` / `pending` / `unknown` 不是付款成功；可能的认证跳转也不是成功证明。商户不要实现自己的前端卡片提交，也不要从浏览器回调履约。

`GET /embed.js` 提供 `ShortPay.open({ checkoutUrl, onStatus, onClose })` 作为托管页面的可选展示工具。`checkoutUrl` 必须是未修改的 ShortPay Session URL；`onStatus` 仅用于显示，不能授权发货。

## 事件与履约

当确认付款后，ShortPay 生成 Stripe 风格事件：`payment_intent.succeeded`，并在 Session 结账完成且未提前过期时生成 `checkout.session.completed`。显式过期产生 `checkout.session.expired`。确认退款产生 `refund.updated`；退款失败产生 `refund.failed`。

商户 webhook 必须在**解析 JSON 前**验证原始 UTF-8 body 的 `Stripe-Signature`。签名形如 `t=<unix>,v1=<hex>`，计算为 HMAC-SHA256(`event_secret`, `timestamp + "." + raw_body`)；使用常量时间比较并按自身策略拒绝陈旧时间戳。每个 event ID 及业务订单/付款 ID 都应在同一数据库事务内去重。仅在持久化完成后返回 2xx。重投可能重复投递，因此重复 event 不能重复履约。

事件对象包含 `id`、`object:"event"`、`api_version`、`created`、`livemode`、`type`、`data.object`、`pending_webhooks`、`request`。先核对对象、金额、货币和本地订单绑定，再发放商品或权益。

## 认证管理参考（不公开 API）

以下是完整部署中的认证管理面，**不是**公共 Stripe 协议；仍要求现有 Bearer/Cookie/CSRF/站点范围认证，本文不提供敏感凭据、回调或部署资料。

- `/management/...`：历史客户、支付、退款、发票、事件投递与对账操作。
- `/admin/...`：管理登录会话。
- `/v1/merchants`、`/v1/sites` 和凭据管理：受控目录与管理资源。
- `/checkout-api/...`：由短期买家 capability 保护的托管结账内部接口。

不要把“文档公开”误解成上述端点可匿名访问。请使用 API base URL 上的真实认证策略和最小站点范围。

## 不支持的能力

订阅/循环扣款、保存卡、授权捕获、Stripe.js、Connect、Product/Price 管理、税/运费/折扣、任意 `expand`、任意分页资源、结算报告与 read-only API key 均不在此协议中。未列字段和端点不应被推测为可用。
