企业 API 文档

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

接口地址
https://ru.pay2gpt.com/api/v1/business
格式
JSON,UTF-8
计费
按套餐的开通次数,一单一次
订单 id
order_number

快速开始

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

1. 拿 API Key

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

2. 查剩余次数

sh
export P2G_KEY="你的 API Key"

curl https://ru.pay2gpt.com/api/v1/business/credits \
  -H "Authorization: Bearer $P2G_KEY"
响应 200
{
  "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。

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

接口

所有接口的路径都相对于 https://ru.pay2gpt.com/api/v1/business。完整的字段定义见 OpenAPI 文件。

GET/credits查剩余次数

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

响应 200
{
  "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
{
  "products": [
    {
      "slug": "plus",
      "name": "ChatGPT Plus",
      "list_price_cents": 2190,
      "currency": "usd"
    }
  ]
}

POST/orders下单

扣该套餐的开通次数并建单,一次 1–100 单,每单扣一次、一个订单号。扣次数和建单在同一个事务里:剩余次数少于单数时一单都不建。之后对每个订单调 提交凭证。必须带 Idempotency-Key。

sh
curl -X POST https://ru.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
{
  "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 对象也可以。见凭证格式。
sh
curl -X POST https://ru.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
{
  "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。
sh
curl "https://ru.pay2gpt.com/api/v1/business/orders?status=processing&limit=50" \
  -H "Authorization: Bearer $P2G_KEY"
响应 200
{
  "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
{
  "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 和订单当前状态。

sh
curl -X POST https://ru.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://ru.pay2gpt.com/api/v1/business/orders/P2G-3F9A1C7E5B2D4A6C8E0F1A2B/cancellation \
  -H "Authorization: Bearer $P2G_KEY"

GET/ledger次数流水

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

响应 200
{
  "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_numberorder_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

在企业控制台的「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
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
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
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 或在控制台查看。