# 支付链接提交 API 文档

更新日期：2026-07-21

## 基础信息

- 基础地址：`https://tiboai.top`
- 支付链接提交页面：`https://tiboai.top/links`
- 请求体格式：`Content-Type: application/json`
- 支付链接必须使用 `https://payments.stripe.com/` 域名。
- 单次请求支持提交 1-50 条支付链接。
- 这里只接受用途为 `stripe_payment_link` 的 Stripe 支付链接 CDK；Access token CDK 与它不通用。
- 建议每 2-5 秒查询一次任务状态，直到进入终态。
- 创建任务返回的 `accessKey` 只在创建响应中返回，调用方必须保存。查询和取消任务时需要通过 `X-Submission-Key` 请求头提交。

## 1. 校验次数 CDK

### 请求

```http
POST /api/cdks/validate
Content-Type: application/json
```

```json
{
  "code": "LINK-XXXX-XXXX-XXXX-XXXX",
  "purpose": "stripe_payment_link"
}
```

### 成功响应

```http
HTTP/1.1 200 OK
```

```json
{
  "ok": true,
  "valid": true,
  "data": {
    "id": "CDK_ID",
    "prefix": "LINK",
    "purpose": "stripe_payment_link",
    "last4": "ABCD",
    "status": "active",
    "createdAt": "2026-07-20T06:00:00.000Z",
    "usageLimit": 10,
    "usedCount": 3,
    "reservedCount": 1,
    "remainingUses": 6,
    "note": ""
  }
}
```

### 校验失败响应

```http
HTTP/1.1 409 Conflict
```

```json
{
  "ok": false,
  "valid": false,
  "reason": "CDK 不存在"
}
```

`purpose` 必须传 `stripe_payment_link`。使用 Access token CDK 会返回 `cdk_purpose_mismatch`。`remainingUses` 已扣除正在处理任务预占的次数，可直接用于判断本次批量提交数量是否足够。

## 2. 创建支付链接任务

### 请求

```http
POST /api/link-submissions
Content-Type: application/json
```

单条提交：

```json
{
  "paymentCdk": "LINK-XXXX-XXXX-XXXX-XXXX",
  "paymentLink": "https://payments.stripe.com/upi/instructions/PAYMENT_LINK_ID"
}
```

`paymentCdk` 也可以使用字段名 `cdk`。

批量提交：

```json
{
  "paymentCdk": "LINK-XXXX-XXXX-XXXX-XXXX",
  "paymentLinks": [
    "https://payments.stripe.com/upi/instructions/PAYMENT_LINK_ID_1",
    "https://payments.stripe.com/upi/instructions/PAYMENT_LINK_ID_2"
  ]
}
```

### 提交前支付状态检查

服务器会在创建任务和预占 CDK 之前逐条检查 Stripe 当前状态：

- `requires_action`、`requires_payment_method`、`requires_confirmation`、`pending`：视为待支付，创建扫码任务。
- `succeeded`：返回 `payment_already_succeeded`，不创建任务、不预占 CDK。
- 已超过 `expires_at` 或 Stripe 返回失效：返回 `payment_expired`，不创建任务、不预占 CDK。
- `canceled`：返回 `payment_canceled`，不创建任务、不预占 CDK。
- 其他状态或查询异常：返回对应检查结果，不创建任务、不预占 CDK。

批量请求允许部分成功，调用方需要同时读取 `tasks` 和 `rejected`。

### 创建成功响应

```http
HTTP/1.1 201 Created
```

```json
{
  "ok": true,
  "data": {
    "tasks": [
      {
        "id": "TASK_ID",
        "status": "awaiting_scan",
        "submissionType": "payment_link",
        "paymentExpiresAt": "2026-07-20T06:05:00.000Z",
        "createdAt": "2026-07-20T06:00:00.000Z",
        "updatedAt": "2026-07-20T06:00:00.000Z",
        "accessKey": "TASK_ACCESS_KEY",
        "paymentLink": "https://payments.stripe.com/upi/instructions/PAYMENT_LINK_ID"
      }
    ],
    "submittedCount": 1,
    "duplicateCount": 0,
    "rejectedCount": 1,
    "rejected": [
      {
        "paymentLink": "https://payments.stripe.com/upi/instructions/ALREADY_PAID_LINK_ID",
        "code": "payment_already_succeeded",
        "message": "支付已经成功，无需再次提交 / Payment already succeeded",
        "intentState": "succeeded"
      }
    ]
  }
}
```

字段说明：

| 字段 | 说明 |
| --- | --- |
| `data.tasks` | 本次实际创建的任务。每条链接对应一个独立任务。 |
| `data.tasks[].id` | 任务 ID，用于查询和取消。 |
| `data.tasks[].accessKey` | 任务查询密钥，只在创建响应中返回。 |
| `data.tasks[].paymentExpiresAt` | 支付任务预计到期时间，ISO 8601 格式。 |
| `data.submittedCount` | 本次实际创建的任务数量。 |
| `data.duplicateCount` | 本次被合并或跳过的重复链接数量。 |
| `data.rejectedCount` | 提交前检查后未进入扫码队列的链接数量。 |
| `data.rejected` | 未入队链接及其 `code`、中英文 `message`、`intentState`。 |

### 全部为重复链接

同一个请求内的重复链接，以及系统中仍处于 `awaiting_scan` 的相同链接，均不会重复创建任务，也不会重复预占 CDK。

```http
HTTP/1.1 200 OK
```

```json
{
  "ok": true,
  "data": {
    "tasks": [],
    "submittedCount": 0,
    "duplicateCount": 1
  }
}
```

调用方必须以 `submittedCount` 和 `tasks` 为准，不能只根据 HTTP 200/201 判断是否创建了新任务。

## 3. 查询任务状态

### 请求

```http
GET /api/submissions/{TASK_ID}
X-Submission-Key: TASK_ACCESS_KEY
```

### 等待接取

```json
{
  "ok": true,
  "data": {
    "id": "TASK_ID",
    "status": "awaiting_scan",
    "submissionType": "payment_link",
    "paymentExpiresAt": "2026-07-20T06:05:00.000Z",
    "createdAt": "2026-07-20T06:00:00.000Z",
    "updatedAt": "2026-07-20T06:00:00.000Z"
  }
}
```

扫码账号接取后，响应会增加 `claimedAt`：

```json
{
  "ok": true,
  "data": {
    "id": "TASK_ID",
    "status": "awaiting_scan",
    "claimedAt": "2026-07-20T06:01:00.000Z",
    "paymentExpiresAt": "2026-07-20T06:05:00.000Z"
  }
}
```

查询接口不会返回二维码、Stripe client secret 或扫码账号信息。

### 支付成功

```json
{
  "ok": true,
  "data": {
    "id": "TASK_ID",
    "status": "completed",
    "submissionType": "payment_link",
    "completedAt": "2026-07-20T06:02:00.000Z",
    "createdAt": "2026-07-20T06:00:00.000Z",
    "updatedAt": "2026-07-20T06:02:00.000Z"
  }
}
```

只有 Stripe 状态确认是 `succeeded` 才会进入 `completed`，并正式扣除 1 次 CDK。

### 支付超时等待扫码端下架

支付链接超过 `paymentExpiresAt` 后不会由系统自动结束。任务继续保持 `awaiting_scan`，并保留 CDK/AT 预占，直到扫码账号在扫码端点击“确认下架”。调用方在此期间会继续查询到 `awaiting_scan`，可通过 `paymentExpiresAt` 判断显示时间已经到期。

扫码端手动下架后返回：

```json
{
  "ok": true,
  "data": {
    "id": "TASK_ID",
    "status": "failed",
    "submissionType": "payment_link",
    "error": {
      "code": "payment_expired",
      "message": "支付任务已超时，由扫码端下架 / Payment expired and removed by scanner"
    },
    "createdAt": "2026-07-20T06:00:00.000Z",
    "updatedAt": "2026-07-20T06:05:00.000Z"
  }
}
```

扫码账号主动标记失败时，状态为 `scan_failed`：

```json
{
  "ok": true,
  "data": {
    "id": "TASK_ID",
    "status": "scan_failed",
    "scanFailedAt": "2026-07-20T06:03:00.000Z",
    "error": {
      "code": "scan_failed",
      "message": "二维码失效 / QR code expired"
    }
  }
}
```

`failed`、`scan_failed` 和 `stopped` 均会释放预占次数，不扣 CDK。支付超时后只有扫码端手动下架才会进入 `failed` 并释放预占。

## 4. 取消任务

### 请求

```http
POST /api/submissions/{TASK_ID}/cancel
X-Submission-Key: TASK_ACCESS_KEY
```

不需要请求体。

### 成功响应

```json
{
  "ok": true,
  "data": {
    "id": "TASK_ID",
    "status": "stopped",
    "error": {
      "code": "stopped",
      "message": "支付链接任务已取消"
    },
    "createdAt": "2026-07-20T06:00:00.000Z",
    "updatedAt": "2026-07-20T06:01:00.000Z"
  }
}
```

只有 `awaiting_scan` 状态的支付链接任务会执行取消。已经结束的任务会直接返回当前状态，不重复处理。

## 5. 状态与扣次规则

| 状态 | 说明 | CDK |
| --- | --- | --- |
| `awaiting_scan` | 等待接取、等待支付，或支付终态等待扫码端手动下架 | 预占 1 次 |
| `completed` | Stripe 已确认 `succeeded`，且扫码端已手动下架 | 正式扣除 1 次 |
| `failed` | 超时/取消任务已由扫码端手动下架，或其他终止错误 | 释放预占，不扣次 |
| `scan_failed` | 扫码账号标记扫码失败 | 释放预占，不扣次 |
| `stopped` | 提交方取消任务 | 释放预占，不扣次 |

扫码账号接取任务前，服务端会再次检查 Stripe：

- 仍处于待支付状态：允许接取并下发二维码与原始支付链接。
- 已经支付成功：扫码端显示“已支付，待下架”；任务继续保留，扫码账号确认下架后进入 `completed` 并扣除 1 次。
- 已超时或已取消：扫码端显示“已超时/已取消，待下架”；任务继续保留，扫码账号确认下架后进入 `failed` 并释放预占。
- 支付处理中或 Stripe 状态暂时不可用：暂不允许接取，任务保留等待后续重试。

## 6. HTTP 状态码与错误格式

| HTTP 状态码 | 说明 |
| --- | --- |
| `200` | 查询、取消或全重复提交成功 |
| `201` | 至少创建了一个新任务 |
| `400` | 请求字段、链接格式或 CDK 字段错误 |
| `404` | 任务不存在，或 `X-Submission-Key` 不匹配 |
| `409` | CDK 不可用、剩余次数不足或业务状态冲突 |
| `413` | 请求体过大 |
| `429` | 请求过于频繁；读取 `Retry-After` 后重试 |
| `503` | 管理端已暂停接收新任务 |

常规错误格式：

```json
{
  "ok": false,
  "code": "ERROR_CODE",
  "message": "错误信息"
}
```

部分错误不包含 `code`。客户端应优先显示 `message`，并根据 HTTP 状态码决定是否重试。

创建和 CDK 校验接口按来源 IP 限制为 10 分钟最多 30 次请求。HTTP 429 不排队，客户端应按照 `Retry-After` 延迟后重试。

## 7. curl 完整示例

### 校验 CDK

```bash
curl -X POST "https://tiboai.top/api/cdks/validate" \
  -H "Content-Type: application/json" \
  -d '{"code":"LINK-XXXX-XXXX-XXXX-XXXX","purpose":"stripe_payment_link"}'
```

### 单条提交

```bash
curl -X POST "https://tiboai.top/api/link-submissions" \
  -H "Content-Type: application/json" \
  -d '{
    "paymentCdk":"LINK-XXXX-XXXX-XXXX-XXXX",
    "paymentLink":"https://payments.stripe.com/upi/instructions/PAYMENT_LINK_ID"
  }'
```

### 批量提交

```bash
curl -X POST "https://tiboai.top/api/link-submissions" \
  -H "Content-Type: application/json" \
  -d '{
    "paymentCdk":"LINK-XXXX-XXXX-XXXX-XXXX",
    "paymentLinks":[
      "https://payments.stripe.com/upi/instructions/PAYMENT_LINK_ID_1",
      "https://payments.stripe.com/upi/instructions/PAYMENT_LINK_ID_2"
    ]
  }'
```

### 查询任务

```bash
curl "https://tiboai.top/api/submissions/TASK_ID" \
  -H "X-Submission-Key: TASK_ACCESS_KEY"
```

### 取消任务

```bash
curl -X POST "https://tiboai.top/api/submissions/TASK_ID/cancel" \
  -H "X-Submission-Key: TASK_ACCESS_KEY"
```

## 8. JavaScript 轮询示例

```javascript
async function waitForPaymentTask({ id, accessKey }) {
  const terminalStatuses = new Set(['completed', 'failed', 'scan_failed', 'stopped']);

  while (true) {
    const response = await fetch(`https://tiboai.top/api/submissions/${id}`, {
      headers: { 'X-Submission-Key': accessKey },
    });
    const payload = await response.json();
    if (!response.ok || payload.ok === false) {
      throw new Error(payload.message || 'Task query failed');
    }
    if (terminalStatuses.has(payload.data.status)) return payload.data;
    await new Promise((resolve) => setTimeout(resolve, 3000));
  }
}
```

不要把 `accessKey` 放入 URL 查询参数、日志或公开页面。任务默认只保留有限时间，业务方应在创建后及时轮询并保存最终结果。
