---
title: 企业 API 文档：按次数下单、自动开通 ChatGPT 会员 | Pay2GPT
description: Pay2GPT 企业 API：按产品充开通次数，用 API 下单并提交账号凭证自动开通 ChatGPT 会员，每单扣一次，失败或取消退回一次，状态变化 Webhook 推送。含认证、幂等、错误码、每个接口的请求和响应示例。
url: https://pay2gpt.com/business/api.md
canonical: https://pay2gpt.com/business/api
locale: zh-CN
site: Pay2GPT
image: https://pay2gpt.com/assets/opengraph-8c4ca537.png
---

> HTML version: https://pay2gpt.com/business/api
> Site index for agents: https://pay2gpt.com/llms.txt

# 企业 API 文档

我们按你采购的套餐数量，在后台给企业账号充好每个套餐的开通次数。之后可以用 API 随时下单：每单扣该套餐一次，提交员工的 ChatGPT 登录凭证后自动开通，开通失败或取消的订单退回一次。订单状态变化会推送到你的 Webhook 地址。

- 接口地址：`https://pay2gpt.com/api/v1/business`
- 格式：JSON，UTF-8
- 计费：按套餐的开通次数，一单一次
- 订单 id：`order_number`
- [OpenAPI 3.1 文件](https://pay2gpt.com/business/openapi.yaml)
- [企业控制台](https://pay2gpt.com/business/console)

## 快速开始

三步走通：在控制台拿 API Key，查剩余次数，用 `POST /activations` 一步完成下单和提交凭证。

### 1. 拿 API Key

登录[企业控制台](https://pay2gpt.com/business/console)，在「API Key」页创建一个 Key。Key 只在创建时显示一次，请立即保存到你的服务端配置里，不要放进前端代码或提交到代码仓库。还没有企业账号的，先在[企业批量开通](https://pay2gpt.com/business)页联系我们开通。

### 2. 查剩余次数

```sh
export P2G_KEY="你的 API Key"

curl https://pay2gpt.com/api/v1/business/credits \
  -H "Authorization: Bearer $P2G_KEY"
```

响应 200：

```json
{
  "credits": {
    "products": [
      {"product": "plus", "product_name": "ChatGPT Plus", "balance": 48},
      {"product": "pro", "product_name": "ChatGPT Pro", "balance": 5}
    ],
    "max_in_progress": 10,
    "in_progress": 2
  }
}
```

### 3. 下单并提交凭证

`session` 填员工账号的完整 Session JSON 或 `__Secure-next-auth.session-token` 的值（见[凭证格式](https://pay2gpt.com/business/api#credentials)）。每个请求带一个新的 `Idempotency-Key`。

```sh
curl -X POST https://pay2gpt.com/api/v1/business/activations \
  -H "Authorization: Bearer $P2G_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7c1e9a52-4d3b-4f0e-8a61-2b9d5c0e3f14" \
  -d '{"product": "plus", "session": "<完整 Session JSON 文本，或 session-token 的值>"}'
```

响应 202：

```json
{
  "order": {
    "order_number": "P2G-3F9A1C7E5B2D4A6C8E0F1A2B",
    "product": "plus",
    "product_name": "Plus",
    "status": "checking",
    "target_account_label": null,
    "rejection_reason": null,
    "expires_at": "2026-12-03T08:00:00Z",
    "fulfilled_at": null,
    "refunded_at": null,
    "created_at": "2026-11-03T08:00:00Z"
  }
}
```

之后用 `GET /orders/{order_number}` 查状态，或者配置 [Webhook](https://pay2gpt.com/business/api#webhooks) 等推送。

## 认证

每个请求在 `Authorization` 头里带 API Key：`Authorization: Bearer <API Key>`。

- Key 在[企业控制台](https://pay2gpt.com/business/console)创建和吊销，创建时只显示一次。
- Key 属于企业账号，只能访问本企业账号的开通次数、订单和流水。
- 没带 Key 或 Key 无效：401 `unauthorized`。Key 不是企业 API 用的、或企业账号不存在：403 `forbidden`。企业账号已暂停：403 `account_suspended`。
- Key 泄露后请立即在控制台吊销并新建一个。

## 幂等

写请求可以带 `Idempotency-Key` 头，`POST /orders` 和 `POST /activations` 必须带，否则返回 400 `idempotency_key_missing`。

- 每次新的下单用一个新的 Key，建议用 UUID。网络超时、拿不到响应时，用**同一个 Key、同样的参数**重试：不会重复扣次数或重复建单，返回第一次的结果。
- 同一个 Key 换了参数（比如换了套餐或数量）：409 `idempotency_conflict`，不做任何处理。
- Key 在企业账号内唯一，不同企业之间互不影响。

## 限流和节流

- **限流**：每个 API Key 每分钟最多 120 个请求，超出返回 429 `rate_limited`，等一会儿再试。
- **节流**：同时处于开通中（`checking`、`processing`）的订单数有上限，即 `GET /credits` 里的 `max_in_progress`（默认 10），当前数量是 `in_progress`；`awaiting_account`、`account_rejected` 的订单不占名额。达到上限后，提交凭证和 `POST /activations` 返回 429 `throttled`：`POST /activations` 此时不建单、不扣次数，等已有订单出结果后用同一个 `Idempotency-Key` 重试即可。需要更高上限请联系我们。
- 批量开通时，建议先看 `in_progress`，按空位分批提交。

## 错误格式和错误码

出错时返回对应的 HTTP 状态码和统一的错误体，按 `code` 判断，`message` 是给人看的中文说明，可能调整。

```json
{
  "error": {
    "code": "insufficient_credits",
    "message": "该套餐的开通次数不足"
  }
}
```

| HTTP | code | 含义和处理 |
| --- | --- | --- |
| 400 | `idempotency_key_missing` | `POST /orders`、`POST /activations` 没带 `Idempotency-Key`。 |
| 400 | `invalid_request` | 请求体不是合法 JSON，或 `cursor`、`limit`、`status`、`created_after`、`Idempotency-Key`（最长 255 个可见字符）格式不对。 |
| 401 | `unauthorized` | 没带 API Key，或 Key 无效、已吊销。 |
| 402 | `insufficient_credits` | 该套餐的开通次数不足（一次下多单时按总单数算）。没有建单，也没有扣次数。联系我们加次数后重试。 |
| 403 | `forbidden` | Key 没有企业 API 权限，或不属于任何企业账号。 |
| 403 | `account_suspended` | 企业账号已暂停，请联系我们。 |
| 404 | `not_found` | 订单不存在，或不属于你的企业账号。 |
| 409 | `idempotency_conflict` | 同一个 `Idempotency-Key` 用在了参数不同的请求上。 |
| 422 | `product_unavailable` | 套餐不存在或暂停销售。以 `GET /products` 为准。 |
| 422 | `invalid_quantity` | `quantity` 不是 1–100 的整数。 |
| 422 | `access_token_unsupported` | 提交的凭证只有 accessToken。不建单、不扣次数，见[凭证格式](https://pay2gpt.com/business/api#credentials)。 |
| 422 | `invalid_session` | 凭证无法识别（既不是 Session JSON，也不像 session-token）。不建单、不扣次数。 |
| 422 | `not_cancellable` | 订单已经连上账号或已结束，不能取消。 |
| 413 | `payload_too_large` | 请求体超过 64 KB。 |
| 422 | `already_confirmed` | 这个订单的账号已经确认，不能再换凭证。 |
| 422 | `credential_not_accepted` | 订单已开通、已退款或已取消，不再接受凭证。 |
| 429 | `rate_limited` | 超过每分钟 120 次的限流。 |
| 429 | `throttled` | 开通中的订单数已达 `max_in_progress`。 |
| 500 | `internal_error` | 服务端出错，已自动上报。可以用同一个 `Idempotency-Key` 重试。 |

## 接口

所有接口的路径都相对于 `https://pay2gpt.com/api/v1/business`。完整的字段定义见 [OpenAPI 文件](https://pay2gpt.com/business/openapi.yaml)。

### `GET /credits` 查剩余次数

每个套餐的剩余开通次数和节流状态。

响应 200：

```json
{
  "credits": {
    "products": [
      {"product": "plus", "product_name": "ChatGPT Plus", "balance": 48},
      {"product": "pro", "product_name": "ChatGPT Pro", "balance": 5}
    ],
    "max_in_progress": 10,
    "in_progress": 2
  }
}
```

| 字段 | 说明 |
| --- | --- |
| `products` | 充过次数的套餐，按零售价从低到高。没充过的套餐不出现，剩余次数视为 0。 |
| `products[].product` | 套餐 slug，同 `GET /products` 的 `slug`。 |
| `products[].balance` | 这个套餐的剩余开通次数，每下一单扣 1。 |
| `max_in_progress` | 同时开通中的订单上限。 |
| `in_progress` | 当前开通中的订单数。 |

### `GET /products` 查在售套餐

可以下单的套餐。`list_price_cents` 是零售参考价（美分），仅供对照；API 下单不按金额扣，只扣该套餐的开通次数。

响应 200：

```json
{
  "products": [
    {
      "slug": "plus",
      "name": "ChatGPT Plus",
      "list_price_cents": 2190,
      "currency": "usd"
    }
  ]
}
```

### `POST /orders` 下单

扣该套餐的开通次数并建单，一次 1–100 单，每单扣一次、一个订单号。扣次数和建单在同一个事务里：剩余次数少于单数时一单都不建。之后对每个订单调 [提交凭证](https://pay2gpt.com/business/api#post-credential)。必须带 `Idempotency-Key`。

```sh
curl -X POST https://pay2gpt.com/api/v1/business/orders \
  -H "Authorization: Bearer $P2G_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0b6f3d2e-91a4-4c57-b8e2-5f7a1c9d4e60" \
  -d '{"product": "plus", "quantity": 2}'
```

响应 201：

```json
{
  "orders": [
    {
      "order_number": "P2G-3F9A1C7E5B2D4A6C8E0F1A2B",
      "product": "plus",
      "product_name": "Plus",
      "status": "awaiting_account",
      "target_account_label": null,
      "rejection_reason": null,
      "expires_at": "2026-12-03T08:00:00Z",
      "fulfilled_at": null,
      "refunded_at": null,
      "created_at": "2026-11-03T08:00:00Z"
    },
    {
      "order_number": "P2G-8D2E4F6A0B1C3D5E7F9A1B2C",
      "product": "plus",
      "product_name": "Plus",
      "status": "awaiting_account",
      "target_account_label": null,
      "rejection_reason": null,
      "expires_at": "2026-12-03T08:00:00Z",
      "fulfilled_at": null,
      "refunded_at": null,
      "created_at": "2026-11-03T08:00:00Z"
    }
  ]
}
```

可能的错误：402 `insufficient_credits`、422 `product_unavailable`、422 `invalid_quantity`、400 `idempotency_key_missing`、409 `idempotency_conflict`、403 `account_suspended`。

### `POST /activations` 下单并提交凭证

最常用的接口：一个账号一单，扣该套餐一次，下单和提交凭证一步完成，返回 202 和订单。必须带 `Idempotency-Key`。

| 参数 | 说明 |
| --- | --- |
| `product` | 套餐 slug，来自 `GET /products`。 |
| `session` | 完整 Session JSON 或 session-token 的值。推荐把原文作为字符串传；直接传 JSON 对象也可以。见[凭证格式](https://pay2gpt.com/business/api#credentials)。 |

```sh
curl -X POST https://pay2gpt.com/api/v1/business/activations \
  -H "Authorization: Bearer $P2G_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7c1e9a52-4d3b-4f0e-8a61-2b9d5c0e3f14" \
  -d '{"product": "plus", "session": "<完整 Session JSON 文本，或 session-token 的值>"}'
```

响应 202：

```json
{
  "order": {
    "order_number": "P2G-3F9A1C7E5B2D4A6C8E0F1A2B",
    "product": "plus",
    "product_name": "Plus",
    "status": "checking",
    "target_account_label": null,
    "rejection_reason": null,
    "expires_at": "2026-12-03T08:00:00Z",
    "fulfilled_at": null,
    "refunded_at": null,
    "created_at": "2026-11-03T08:00:00Z"
  }
}
```

凭证只有 accessToken（422 `access_token_unsupported`）或无法识别（422 `invalid_session`）时，不建单、不扣次数；开通次数不足时返回 402 `insufficient_credits`。凭证格式通过检查后就会建单、扣一次；之后账号检查不通过的，订单变为 `account_rejected`，可以[换账号重新提交](https://pay2gpt.com/business/api#post-credential)或[取消](https://pay2gpt.com/business/api#post-cancellation)退回一次。

### `GET /orders` 订单列表

| 查询参数 | 说明 |
| --- | --- |
| `status` | 按状态过滤，取值见[订单状态](https://pay2gpt.com/business/api#order-status)。 |
| `created_after` | 只返回这个时间之后创建的订单，ISO 8601，如 `2026-11-01T00:00:00Z`。 |
| `limit` | 每页条数，最多 100。 |
| `cursor` | 翻页游标：上一页响应里的 `next_cursor`。 |

```sh
curl "https://pay2gpt.com/api/v1/business/orders?status=processing&limit=50" \
  -H "Authorization: Bearer $P2G_KEY"
```

响应 200：

```json
{
  "orders": [
    {
      "order_number": "P2G-3F9A1C7E5B2D4A6C8E0F1A2B",
      "product": "plus",
      "product_name": "Plus",
      "status": "processing",
      "target_account_label": "al***@example.com",
      "rejection_reason": null,
      "expires_at": "2026-12-03T08:00:00Z",
      "fulfilled_at": null,
      "refunded_at": null,
      "created_at": "2026-11-03T08:00:00Z"
    }
  ],
  "next_cursor": "eyJpZCI6MTIzNH0"
}
```

`next_cursor` 为 `null` 表示没有下一页。

### `GET /orders/{order_number}` 查订单

响应 200：

```json
{
  "order": {
    "order_number": "P2G-3F9A1C7E5B2D4A6C8E0F1A2B",
    "product": "plus",
    "product_name": "Plus",
    "status": "succeeded",
    "target_account_label": "al***@example.com",
    "rejection_reason": null,
    "expires_at": "2026-12-03T08:00:00Z",
    "fulfilled_at": "2026-11-03T08:04:12Z",
    "refunded_at": null,
    "created_at": "2026-11-03T08:00:00Z"
  }
}
```

| 字段 | 说明 |
| --- | --- |
| `order_number` | 订单号，所有接口和 Webhook 都用它指代订单。 |
| `product` | 套餐 slug。 |
| `status` | 订单状态，见[订单状态](https://pay2gpt.com/business/api#order-status)。 |
| `target_account_label` | 开通的 ChatGPT 账号（邮箱打码）。账号检查前为 `null`。 |
| `rejection_reason` | 账号检查不通过的原因，只在 `account_rejected` 时有值，取值见[订单状态](https://pay2gpt.com/business/api#order-status)。 |
| `expires_at` | 提交凭证的截止时间（下单后 30 天）。 |
| `fulfilled_at` | 开通成功的时间，未成功为 `null`。 |

### `POST /orders/{order_number}/credential` 提交凭证

给 `awaiting_account` 或 `account_rejected` 的订单提交账号凭证（`account_rejected` 时就是换一个账号重新提交）。参数 `session` 同 `POST /activations`。返回 202 和订单当前状态。

```sh
curl -X POST https://pay2gpt.com/api/v1/business/orders/P2G-3F9A1C7E5B2D4A6C8E0F1A2B/credential \
  -H "Authorization: Bearer $P2G_KEY" \
  -H "Content-Type: application/json" \
  -d '{"session": "<完整 Session JSON 文本，或 session-token 的值>"}'
```

可能的错误：422 `access_token_unsupported`、422 `invalid_session`、429 `throttled`、404 `not_found`。

### `POST /orders/{order_number}/cancellation` 取消订单

还没连上账号的订单（`awaiting_account`、`account_rejected`）可以取消，扣的那次退回，订单变为 `refunded`。已经在开通或已结束的订单返回 422 `not_cancellable`。

```sh
curl -X POST https://pay2gpt.com/api/v1/business/orders/P2G-3F9A1C7E5B2D4A6C8E0F1A2B/cancellation \
  -H "Authorization: Bearer $P2G_KEY"
```

### `GET /ledger` 次数流水

开通次数的每一笔变动，按时间倒序，用 `limit`（最多 100）和 `cursor` 翻页，同订单列表。

响应 200：

```json
{
  "entries": [
    {
      "id": "bcre_8Kf3Qx2LmN7p",
      "kind": "order_refund",
      "product": "plus",
      "count": 1,
      "balance_after": 50,
      "amount_cents": null,
      "currency": null,
      "reference": null,
      "order_number": "P2G-8D2E4F6A0B1C3D5E7F9A1B2C",
      "created_at": "2026-11-03T09:10:00Z"
    },
    {
      "id": "bcre_2Vh9Ry4TwZ1c",
      "kind": "order_debit",
      "product": "plus",
      "count": -1,
      "balance_after": 49,
      "amount_cents": null,
      "currency": null,
      "reference": null,
      "order_number": "P2G-8D2E4F6A0B1C3D5E7F9A1B2C",
      "created_at": "2026-11-03T08:00:00Z"
    },
    {
      "id": "bcre_5Tn1Wc8PqA3d",
      "kind": "topup",
      "product": "plus",
      "count": 50,
      "balance_after": 50,
      "amount_cents": 100750,
      "currency": "usd",
      "reference": "0x9f2c4e8b1d7a3c5e6f0b2d4a8c1e3f5a7b9d0c2e4f6a8b1c3d5e7f9a0b2c4e6d",
      "order_number": null,
      "created_at": "2026-11-02T03:00:00Z"
    }
  ],
  "next_cursor": null
}
```

| 字段 | 说明 |
| --- | --- |
| `product` | 这笔变动属于哪个套餐，每个套餐的次数分开算。 |
| `count` | 有符号的次数变动：加次数为正，扣次数为负。 |
| `balance_after` | 这笔变动后该套餐的剩余次数。 |
| `amount_cents`、`currency` | 只有 `topup` 有值，记录这次采购付的金额（美分）；其他类型为 `null`。 |
| `order_number` | `order_debit`、`order_refund` 对应的订单，其他类型为 `null`。 |

| kind | 说明 |
| --- | --- |
| `topup` | 按你采购的数量加次数，`count` 为正。`amount_cents` 和 `currency` 是这次采购付的金额，`reference` 是银行流水号或 USDT 交易哈希。 |
| `order_debit` | 下单扣一次，`count` 为 -1。每单只扣一次。 |
| `order_refund` | 订单退回一次，`count` 为 1。每单最多退一次。 |
| `adjustment` | 人工调整，`count` 可正可负。 |

## 订单状态

```text
awaiting_account  → checking → processing → succeeded
checking          → account_rejected → checking        （换账号重新提交）
processing        → failed → refunded                  （自动退回一次）
awaiting_account  → refunded                           （取消）
account_rejected  → refunded                           （取消）
```

| status | 含义 | 你可以做什么 |
| --- | --- | --- |
| `awaiting_account` | 已扣次数建单，等你提交凭证。 | 提交凭证，或取消。 |
| `checking` | 凭证已收到，正在检查账号。 | 等结果。 |
| `account_rejected` | 账号检查不通过，原因见 `rejection_reason`。 | 换账号重新提交，或取消。 |
| `processing` | 账号已确认，正在开通。 | 等结果，不能取消。 |
| `succeeded` | 开通成功，`fulfilled_at` 是开通时间。 | — |
| `failed` | 开通最终失败。 | 无需操作，随后自动退回一次，变为 `refunded`。 |
| `refunded` | 扣的那次已退回（开通失败、取消，或客服处理的退款）。 | — |

API 下的订单不需要人工确认账号：账号检查通过后自动开始开通。

`rejection_reason` 的取值：

| rejection_reason | 含义 |
| --- | --- |
| `access_token_unsupported` | 提交的 session-token 实际是 accessToken。 |
| `session_expired` | 登录已过期或已失效，请让员工重新登录 ChatGPT 后再取凭证。 |
| `invalid_session` | 凭证格式无效。 |
| `upstream_unavailable` | 检查账号的服务暂时不可用，多次重试仍失败。可以稍后重新提交同一个凭证。 |
| `plan_not_eligible` | 账号当前的套餐类型不能开通这个产品。 |
| `plan_already_active` | 账号已经有生效中的同类订阅。 |
| `workspace_account` | 这是 Team/Enterprise 工作区账号，不能开通个人套餐。 |
| `half_price_ineligible` | 账号没有半价资格。 |
| `deactivated` | 账号已被停用。 |
| `delinquent` | 账号有未付的欠款。 |
| `session_unusable` | 这个凭证不能用来开通，请重新登录后再取。 |
| `account_busy` | 这个账号还有别的订单在处理。 |

账号检查还可能给出其他原因，一律按 `account_rejected` 处理：换账号重新提交，或取消。

## 凭证格式

`session` 接受两种格式，都只用于这一单的开通：

- **完整 Session JSON**：员工在浏览器里登录 chatgpt.com 后，打开 `https://chatgpt.com/api/auth/session` 看到的整段 JSON 文本，原样提交，不要删字段。其中必须有 `sessionToken`：只有 `accessToken`、没有 `sessionToken` 的 JSON 按只有 accessToken 处理，返回 422 `access_token_unsupported`，不建单、不扣次数。
- **session-token**：chatgpt.com 的 Cookie `__Secure-next-auth.session-token` 的值（在浏览器开发者工具的 Application → Cookies 里能看到）。提交后订单进入 `checking`，我们用它换取完整 Session 后再检查账号。

> **只有 accessToken 不行**：只提交 accessToken（一段 JWT，没有 session-token）会直接返回 422 `access_token_unsupported`，不建单、不扣次数。原因是开通需要 session-token：我们要用它刷新登录状态，开通后还要用它关闭这个账号的自动续费。只有 accessToken 时这两件事都做不了。

- 凭证只在开通这一单时使用，用完即删，不写日志。
- 员工在 ChatGPT 里「退出所有设备」即可让提交过的凭证失效（会影响还在进行中的开通，请在订单结束后再做）。
- 不需要、也请不要提供 ChatGPT 密码或验证码。

## Webhook

在[企业控制台](https://pay2gpt.com/business/console)的「Webhook」页填写接收地址（必须是 https，不能解析到内网或本机地址）。地址为空时不推送。控制台可以轮换签名密钥（只显示一次）和发送测试事件（`ping`）。

### 事件

| type | 什么时候发 |
| --- | --- |
| `order.created` | 下单成功（已扣一次）。 |
| `order.account_rejected` | 账号检查不通过，可以换账号重新提交。 |
| `order.processing` | 账号已确认，开始开通。 |
| `order.succeeded` | 开通成功。 |
| `order.failed` | 开通最终失败，随后会有 `order.refunded`。 |
| `order.refunded` | 扣的那次已退回。 |
| `ping` | 控制台里点「发送测试」。 |

### 请求格式

`POST` 到你的地址，JSON 请求体。`data.order` 和 `GET /orders/{order_number}` 返回的 `order` 结构相同。

```http
POST /your/webhook/path HTTP/1.1
Content-Type: application/json
Pay2GPT-Event-Id: 5d0c8f5e-3a7b-4c1e-9f2a-6b8d0e4c1a37
Pay2GPT-Signature: t=1762156800,v1=3b1f0c...

{
  "id": "5d0c8f5e-3a7b-4c1e-9f2a-6b8d0e4c1a37",
  "type": "order.succeeded",
  "created_at": "2026-11-03T08:04:12Z",
  "data": {
    "order": {
      "order_number": "P2G-3F9A1C7E5B2D4A6C8E0F1A2B",
      "product": "plus",
      "product_name": "Plus",
      "status": "succeeded",
      "target_account_label": "al***@example.com",
      "rejection_reason": null,
      "expires_at": "2026-12-03T08:00:00Z",
      "fulfilled_at": "2026-11-03T08:04:12Z",
      "refunded_at": null,
      "created_at": "2026-11-03T08:00:00Z"
    }
  }
}
```

### 重试

- 你的地址在 10 秒内返回 2xx 即算成功。超时或其他状态码会按指数退避重试，最多 8 次，大约覆盖 24 小时。
- 重试意味着同一事件可能到达多次，也可能晚于后面的事件到达：按 `id`（与 `Pay2GPT-Event-Id` 头相同）去重；需要最新状态时调 `GET /orders/{order_number}`。
- 先返回 2xx，再异步处理，避免超时引起重复投递。

### 验证签名

`Pay2GPT-Signature` 的格式是 `t=<unix 时间戳>,v1=<签名>`，签名是用你的 Webhook 密钥对 `<t>.<原始请求体>` 做 HMAC-SHA256 得到的十六进制字符串。验证时：

- 用**原始请求体**（收到的字节）计算，不要先解析 JSON 再序列化。
- 用常数时间比较签名。
- 检查 `t` 与当前时间相差不超过 5 分钟，防止重放。
- 轮换密钥后，新事件用新密钥签名，请同步更新服务端配置。

Ruby：

```ruby
require "openssl"

# raw_body: 原始请求体字符串；header: Pay2GPT-Signature 头；secret: Webhook 密钥
def valid_pay2gpt_signature?(raw_body, header, secret, tolerance: 300)
  parts = header.to_s.split(",").to_h { |part| part.split("=", 2) }
  timestamp, signature = parts["t"], parts["v1"]
  return false unless timestamp&.match?(/\A\d+\z/) && signature
  return false if (Time.now.to_i - timestamp.to_i).abs > tolerance

  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{raw_body}")
  OpenSSL.secure_compare(expected, signature)
end

# Rails: valid_pay2gpt_signature?(request.raw_post, request.headers["Pay2GPT-Signature"], ENV["P2G_WEBHOOK_SECRET"])
```

Python：

```python
import hashlib
import hmac
import time

def valid_pay2gpt_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(part.split("=", 1) for part in (header or "").split(",") if "=" in part)
    timestamp, signature = parts.get("t", ""), parts.get("v1")
    if not timestamp.isdigit() or not signature:
        return False
    if abs(time.time() - int(timestamp)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

# Flask: valid_pay2gpt_signature(request.get_data(), request.headers.get("Pay2GPT-Signature"), os.environ["P2G_WEBHOOK_SECRET"])
```

Node.js：

```javascript
const crypto = require("node:crypto");

// rawBody: 原始请求体 Buffer；header: Pay2GPT-Signature 头；secret: Webhook 密钥
function validPay2gptSignature(rawBody, header, secret, tolerance = 300) {
  const parts = Object.fromEntries(String(header || "").split(",").map((part) => {
    const index = part.indexOf("=");
    return [part.slice(0, index), part.slice(index + 1)];
  }));
  const timestamp = parts.t;
  const signature = parts.v1;
  if (!/^\d+$/.test(timestamp || "") || !signature) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > tolerance) return false;

  const expected = crypto.createHmac("sha256", secret)
    .update(Buffer.concat([Buffer.from(`${timestamp}.`), rawBody]))
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express: app.post("/hooks/pay2gpt", express.raw({ type: "application/json" }), (req, res) => {
//   if (!validPay2gptSignature(req.body, req.get("Pay2GPT-Signature"), process.env.P2G_WEBHOOK_SECRET)) return res.sendStatus(400);
//   res.sendStatus(200);
// });
```

## 计费

- **按套餐充开通次数**：次数由我们按你采购的套餐数量在后台充值，例如采购 50 个 Plus 就给 Plus 加 50 次。采购通过银行转账或 USDT 付款，到账后入账，流水里是一笔 `topup`，记下次数和付的金额。采购和加次数请联系我们。
- **每个套餐分开算**：Plus 的次数只能开 Plus，不能挪给别的套餐。剩余次数以 `GET /credits` 为准。
- **扣次数**：开通一个扣一次，下单时每单一笔 `order_debit`。剩余次数不够时返回 402 `insufficient_credits`，不建单。
- **退回次数**：开通失败、取消、或客服处理的退款，退回一次（一笔 `order_refund`，每单最多一次），不退成现金。
- 剩余次数和每一笔变动都可以用 `GET /credits`、`GET /ledger` 或在控制台查看。

---

Pay2GPT 为第三方服务，与 OpenAI 无任何关联。

- [Pay2GPT](https://pay2gpt.com/.md)
- [教程](https://pay2gpt.com/guide.md)
- [帮助](https://pay2gpt.com/help.md)
- [服务状态](https://pay2gpt.com/status.md)
- [订单](https://pay2gpt.com/orders)
- [企业批量开通](https://pay2gpt.com/business)
- [马上开通](https://pay2gpt.com/checkout?plan=plus)
- [服务条款](https://pay2gpt.com/terms.md)
- [隐私政策](https://pay2gpt.com/privacy.md)
- [Trustpilot](https://www.trustpilot.com/review/pay2gpt.com)
- [Site index for agents](https://pay2gpt.com/llms.txt)

Contact: [support@pay2gpt.com](mailto:support@pay2gpt.com)

## Structured data

```json
{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Organization",
      "@id": "https://pay2gpt.com/#organization",
      "name": "Pay2GPT",
      "url": "https://pay2gpt.com/",
      "logo": {
        "@type": "ImageObject",
        "url": "https://pay2gpt.com/icon-512.png",
        "width": 512,
        "height": 512
      },
      "description": "ChatGPT Plus、Pro、Go 代充和 Codex 点数，直接开在你自己的账号上。支付宝、银行卡、USDT 都能付，不用海外卡，不要密码；付款后连接账号，大多几分钟开好。开不了全额退，30 天内掉了免费补。",
      "contactPoint": {
        "@type": "ContactPoint",
        "contactType": "customer support",
        "url": "https://pay2gpt.com/help",
        "email": "support@pay2gpt.com",
        "availableLanguage": [
          "en",
          "zh-CN",
          "es",
          "ja",
          "de",
          "fr",
          "pt-BR",
          "ko",
          "it",
          "nl",
          "pl",
          "ru",
          "tr",
          "zh-TW",
          "zh-HK",
          "ar"
        ]
      }
    },
    {
      "@type": "BreadcrumbList",
      "itemListElement": [
        {
          "@type": "ListItem",
          "position": 1,
          "name": "Pay2GPT",
          "item": "https://pay2gpt.com/"
        },
        {
          "@type": "ListItem",
          "position": 2,
          "name": "企业 API 文档",
          "item": "https://pay2gpt.com/business/api"
        }
      ]
    },
    {
      "@type": "TechArticle",
      "@id": "https://pay2gpt.com/business/api#webpage",
      "url": "https://pay2gpt.com/business/api",
      "name": "企业 API 文档",
      "inLanguage": "zh-CN",
      "isPartOf": {
        "@id": "https://pay2gpt.com/#website"
      },
      "publisher": {
        "@id": "https://pay2gpt.com/#organization"
      }
    }
  ]
}
```
