企业 API 文档
我们按你采购的套餐数量,在后台给企业账号充好每个套餐的开通次数。之后可以用 API 随时下单:每单扣该套餐一次,提交员工的 ChatGPT 登录凭证后自动开通,开通失败或取消的订单退回一次。订单状态变化会推送到你的 Webhook 地址。
- 接口地址
https://pay2gpt.com/api/v1/business- 格式
- JSON,UTF-8
- 计费
- 按套餐的开通次数,一单一次
- 订单 id
order_number
快速开始
三步走通:在控制台拿 API Key,查剩余次数,用 POST /activations 一步完成下单和提交凭证。
1. 拿 API Key
登录企业控制台,在「API Key」页创建一个 Key。Key 只在创建时显示一次,请立即保存到你的服务端配置里,不要放进前端代码或提交到代码仓库。还没有企业账号的,先在企业批量开通页联系我们开通。
2. 查剩余次数
export P2G_KEY="你的 API Key"
curl https://pay2gpt.com/api/v1/business/credits \
-H "Authorization: Bearer $P2G_KEY"
{
"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 的值(见凭证格式)。每个请求带一个新的 Idempotency-Key。
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 的值>"}'
{
"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 等推送。
认证
每个请求在 Authorization 头里带 API Key:Authorization: Bearer <API Key>。
- Key 在企业控制台创建和吊销,创建时只显示一次。
- Key 属于企业账号,只能访问本企业账号的开通次数、订单和流水。
- 没带 Key 或 Key 无效:401
unauthorized。Key 不是企业 API 用的、或企业账号不存在:403forbidden。企业账号已暂停:403account_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返回 429throttled:POST /activations此时不建单、不扣次数,等已有订单出结果后用同一个Idempotency-Key重试即可。需要更高上限请联系我们。 - 批量开通时,建议先看
in_progress,按空位分批提交。
错误格式和错误码
出错时返回对应的 HTTP 状态码和统一的错误体,按 code 判断,message 是给人看的中文说明,可能调整。
{
"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。不建单、不扣次数,见凭证格式。 |
| 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 文件。
GET/credits查剩余次数
每个套餐的剩余开通次数和节流状态。
{
"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 下单不按金额扣,只扣该套餐的开通次数。
{
"products": [
{
"slug": "plus",
"name": "ChatGPT Plus",
"list_price_cents": 2190,
"currency": "usd"
}
]
}
POST/orders下单
扣该套餐的开通次数并建单,一次 1–100 单,每单扣一次、一个订单号。扣次数和建单在同一个事务里:剩余次数少于单数时一单都不建。之后对每个订单调 提交凭证。必须带 Idempotency-Key。
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}'
{
"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 对象也可以。见凭证格式。 |
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 的值>"}'
{
"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,可以换账号重新提交或取消退回一次。
GET/orders订单列表
| 查询参数 | 说明 |
|---|---|
status | 按状态过滤,取值见订单状态。 |
created_after | 只返回这个时间之后创建的订单,ISO 8601,如 2026-11-01T00:00:00Z。 |
limit | 每页条数,最多 100。 |
cursor | 翻页游标:上一页响应里的 next_cursor。 |
curl "https://pay2gpt.com/api/v1/business/orders?status=processing&limit=50" \
-H "Authorization: Bearer $P2G_KEY"
{
"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}查订单
{
"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 | 订单状态,见订单状态。 |
target_account_label | 开通的 ChatGPT 账号(邮箱打码)。账号检查前为 null。 |
rejection_reason | 账号检查不通过的原因,只在 account_rejected 时有值,取值见订单状态。 |
expires_at | 提交凭证的截止时间(下单后 30 天)。 |
fulfilled_at | 开通成功的时间,未成功为 null。 |
POST/orders/{order_number}/credential提交凭证
给 awaiting_account 或 account_rejected 的订单提交账号凭证(account_rejected 时就是换一个账号重新提交)。参数 session 同 POST /activations。返回 202 和订单当前状态。
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。
curl -X POST https://pay2gpt.com/api/v1/business/orders/P2G-3F9A1C7E5B2D4A6C8E0F1A2B/cancellation \
-H "Authorization: Bearer $P2G_KEY"
GET/ledger次数流水
开通次数的每一笔变动,按时间倒序,用 limit(最多 100)和 cursor 翻页,同订单列表。
{
"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 可正可负。 |
订单状态
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 处理,返回 422access_token_unsupported,不建单、不扣次数。 - session-token:chatgpt.com 的 Cookie
__Secure-next-auth.session-token的值(在浏览器开发者工具的 Application → Cookies 里能看到)。提交后订单进入checking,我们用它换取完整 Session 后再检查账号。
access_token_unsupported,不建单、不扣次数。原因是开通需要 session-token:我们要用它刷新登录状态,开通后还要用它关闭这个账号的自动续费。只有 accessToken 时这两件事都做不了。- 凭证只在开通这一单时使用,用完即删,不写日志。
- 员工在 ChatGPT 里「退出所有设备」即可让提交过的凭证失效(会影响还在进行中的开通,请在订单结束后再做)。
- 不需要、也请不要提供 ChatGPT 密码或验证码。
Webhook
在企业控制台的「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 结构相同。
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 分钟,防止重放。 - 轮换密钥后,新事件用新密钥签名,请同步更新服务端配置。
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"])
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"])
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。剩余次数不够时返回 402insufficient_credits,不建单。 - 退回次数:开通失败、取消、或客服处理的退款,退回一次(一笔
order_refund,每单最多一次),不退成现金。 - 剩余次数和每一笔变动都可以用
GET /credits、GET /ledger或在控制台查看。