# ChatGPT Subscription Activation API

Server-side API for contracted clients. Use one API key to submit an activation key plus a ChatGPT session, activate a ChatGPT subscription on the target account, and poll for the result.

| | |
|---|---|
| Base URL | `https://www.academicgate.org` |
| Version | `v1` (pinned in the path — see [13. Versioning](#13-versioning)) |
| Auth | `X-API-Key` header |
| Transport | HTTPS only, TLS 1.2+ |
| Encoding | `application/json; charset=utf-8` for both request and response |
| Self-serve page | `https://www.academicgate.org/store` (safe to hand to end users; no API key needed; supports Plus / Pro 5x / Pro 20x / Pro 500 — see 8.3) |

> **In one line**: `POST /api/v1/gptplus/redeem` with `card_code` + `session_data` returns a `task_id`; poll `GET /api/v1/gptplus/task/{task_id}` every 3 seconds until `finished: true`.

---

## 1. Quick start

```bash
export API_KEY="aisub_your_key"
export BASE="https://www.academicgate.org"

# 1) Verify the key works
curl -s "$BASE/api/v1/ping" -H "X-API-Key: $API_KEY"

# 2) Discover supported plans (don't hardcode key prefixes on your side)
curl -s "$BASE/api/v1/products" -H "X-API-Key: $API_KEY"

# 3) Submit an activation (session_data is the full JSON from https://chatgpt.com/api/auth/session)
curl -s -X POST "$BASE/api/v1/gptplus/redeem" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json; charset=utf-8" \
  -d '{"card_code":"PLS-XXXXXXXXXXXXXXXX","session_data":{"accessToken":"eyJ...","user":{"email":"user@example.com"}}}'

# 4) Poll for the result (every 3 seconds)
curl -s "$BASE/api/v1/gptplus/task/REPLACE_WITH_TASK_ID" -H "X-API-Key: $API_KEY"
```

---

## 2. Plans and activation keys

**The plan tier is derived automatically from the key prefix. Requests must not contain any tier parameter.** Send the key as-is; the system identifies which plan it belongs to and processes it accordingly.

This design eliminates one specific class of incident: if the caller specified the tier and got it wrong, the request would still count as a valid submission, consuming a use off the key with no refund. Deriving it from the prefix makes that outcome impossible.

| Plan | `product` | Key prefix | Example | Credential |
|---|---|---|---|---|
| ChatGPT Plus | `gpt_plus_self` | `PLS-` | `PLS-XXXXXXXXXXXXXXXX` | Session JSON |
| ChatGPT Pro 5x | `gpt_pro_5x_self` | `P5X-` | `P5X-XXXXXXXXXXXXXXXX` | Session JSON |
| ChatGPT Pro 20x | `gpt_pro_20x_self` | `P20X-` | `P20X-XXXXXXXXXXXXXXXX` | Session JSON |
| ChatGPT Pro 20x · Renewal | `gpt_pro_20x_renew` | `PH20-` | `PH20-XXXX-XXXX-XXXX` | Session JSON |
| ChatGPT Pro 500 | `gpt_pro_500_self` | `P500-` | `P500-XXXXXXXXXXXXXXXX` | Session JSON |

`GET /api/v1/products` is the source of truth for this table. Plans and prefixes may change, and the API ships before the docs, so **route and hint using the API response — never hardcode the prefix list in your code.** Keys issued earlier under other prefixes still redeem fine but are not listed in the catalog — **treat local checks as hints only and never reject a key on them**; the server has the final say (a malformed key returns `invalid_card_format` and consumes nothing).

### 2.1 Choosing a plan

- **Plus / Pro 5x / Pro 20x** — key prefixes `PLS-` / `P5X-` / `P20X-`. **Plus supports only accounts with no active subscription**; **Pro 20x (new activation supported since 2026-09-30): an account with no subscription can be activated directly, and an account already on an active Philippines (PHP) Plus / Pro 5x is upgraded (no need to cancel first; non-PH billing returns `plus_not_ph_region` and the key is not consumed)**; **Pro 5x additionally supports an account that already holds an active ChatGPT Plus — it is automatically upgraded to Pro 5x (no need to cancel Plus first; Philippines/PHP Plus only — a non-PH Plus returns `plus_not_ph_region` and the key is not consumed)**. Other ineligible accounts (e.g. already on Pro 5x or higher, or a non-Plus paid subscription) are rejected (`account_has_subscription`) and the key is not consumed.
- **Pro 20x eligibility**: three kinds of accounts are supported — (1) an account with no subscription (Free plan); (2) an account on an active Philippines (PHP) Plus; (3) an account on an active Philippines (PHP) Pro 5x ((2)/(3) are auto-upgraded, no need to cancel first). An account already on an active Pro 20x should renew after it expires; an account for which the official Pro 20x offer is not enabled returns `account_not_eligible_pro20` (rarely seen) and **the key is not consumed**.
- **Pro 500 eligibility**: key prefix `P500-`. **Free accounts with no subscription of any kind only**, and it is **not an upgrade product** — no upgrade from Plus / Pro 5x / Pro 20x / Go / Team and no price-difference top-up; an account whose renewal was cancelled but has not yet expired also counts as subscribed and is rejected (`account_has_subscription`, key not consumed). If the account already has a billing currency and it is not the Philippines (PHP), `promax_billing_not_php` is returned. `P500-` keys are delivered manually and cannot be bought online — see [2.4 Pro 500 activation notes](#2-4-pro-500-activation-notes).

### 2.2 Prerequisites common to all plans

- **The target account must not have an active paid subscription** (exception: **Pro 5x also accepts a Philippines-region account that holds an active ChatGPT Plus, and Pro 20x also accepts a Philippines-region account that holds an active Plus / Pro 5x — both are upgraded automatically**). The account's current subscription status is checked at submit time; other ineligible accounts are rejected with `account_has_subscription` (the key is not consumed). **A subscription whose renewal was cancelled but has not yet expired still counts as active** (resubmit a Plus key after it expires); an already-expired subscription counts as no subscription and can be activated normally. **Pro 500 is the opposite exception and stricter: no subscription of any kind is accepted and there is no upgrade path (see 2.4).**
- `session_data` must be the **complete** session JSON of the target ChatGPT account and must contain `accessToken`.
- The account must stay logged in in the browser from submission until the result arrives, otherwise the session expires and the task fails.

### 2.3 Duplicate top-up protection

Topping up the same ChatGPT account again does **not** extend the subscription — it overwrites the current one, and the activation key is still consumed with no refund. We therefore block duplicate submissions, in two tiers based on how long ago the account was last topped up:

| Since last successful top-up | Behaviour | `error` |
|---|---|---|
| Within 24 hours | **Rejected outright**; `confirm_duplicate` does not override this | `duplicate_email_blocked` |
| 24 hours – 30 days | Rejected pending confirmation; resend with `confirm_duplicate: true` to proceed | `duplicate_email_confirm` |
| More than 30 days | Allowed | — |

Both return HTTP `409` with the previous record attached (the 24-hour rejection also carries `blocked: true`, the confirmation case carries `need_confirm: true`; the example below is the confirmation case):

```json
{
  "success": false,
  "error": "duplicate_email_confirm",
  "detail": "This account was already topped up on 2026-09-14 08:51:21 …",
  "detail_zh": "该账号最近已充值过…",
  "duplicate": true,
  "last_product": "ChatGPT Plus",
  "last_product_zh": "ChatGPT Plus",
  "last_at": "2026-09-14 08:51:21",
  "hours_ago": 77.83,
  "need_confirm": true
}
```

Matching is by **email across any GPT plan**: an account topped up with Plus will also match when you submit Plus again, because any tier will fail or overwrite while a subscription is still active. Upgrades (Plus→Pro 5x, Plus / Pro 5x→Pro 20x) do not trigger this check. Pro 500 is not an upgrade, so the check applies to it as usual.

> **Integration guidance**: on `need_confirm`, show `last_product` and `last_at` to your end user and only resend with `confirm_duplicate: true` after they confirm. **Do not auto-resend unconditionally in code** — that disables the protection entirely. On `blocked`, tell the user and stop; do not retry.

The check covers all of our entry points (this API, the self-serve page, and our own activation page), so a top-up made elsewhere is still detected.

### 2.4 Pro 500 activation notes

`P500-` (Pro 500) follows rules that **differ** from the other plans — please review them before integrating:

- **Account eligibility**: **free accounts with no subscription of any kind only.** Plus / Pro 5x / Pro 20x / Go / Team accounts, accounts with a cancelled-but-unexpired subscription, and accounts with unpaid invoices are all rejected (`account_has_subscription`); there is no upgrade and no price-difference top-up. A non-Philippines (non-PHP) billing region returns `promax_billing_not_php`. If the account's subscription status cannot be read at submit time, the request is rejected as well (`precheck_failed`) — it is **never let through**. All of these rejections happen before the task is accepted, so the key is not consumed.
- **How it is activated**: we first set the account up on a base plan (Plus), then upgrade it to Pro 500, entirely automatically; after success we turn off auto-renewal. Because it is two steps, a failed upgrade can leave the account on the base plan (see "Failure handling" below).
- **How to obtain keys**: `P500-` keys are delivered manually and **cannot be ordered online**; the catalog and the API return no prices. Contact us to buy at the agent price; once you hold a key, both this API and the self-serve page can redeem it.
- **Queueing**: Pro 500 is processed **one order at a time across the whole system**; our own activation page, the self-serve page and all resellers share one queue, and a Pro 500 in progress may also make Pro 5x / Pro 20x tasks wait briefly during its engine step. After acceptance a task may stay `queued` for a long time (several minutes or more) — this is normal queueing, not a fault: keep polling and do not resubmit. A single task also takes longer than the other plans, and the duration is not fixed. Poll every 3–5 seconds and allow at least 20 minutes; after that switch to low-frequency checks or contact us, and do not mark it failed.
- **Concurrency per account**: when you submit Pro 500 and the account already has a task for a `PLS-` / `P5X-` / `P20X-` / `P500-` key in accepted / queued / running state (regardless of entry point or reseller), the new submission returns `409 account_busy` and the key is not consumed; submit again after the earlier task finishes.
- **Failure handling (important)**: **never auto-resubmit** after a Pro 500 failure. `promax_upgrade_incomplete` (base plan activated, upgrade not completed) and `payment_unverified` (payment result unconfirmed) both mean **do not resubmit — contact us for manual handling**: the account is already on the base plan, so a resubmission would be rejected because the account now has a subscription. This is the **opposite** of Pro 20x `upgrade_incomplete` (resubmit with the same key) — handle them separately, see 7.3. For all other failure codes follow the "Action" column in 7.3.
- **Failure freeze**: submissions for an account are paused after 2 consecutive billing-stage failures or 3 consecutive failures of any cause; same rule as the other plans, see 7.3.

---

## 3. Authentication and security

Every request must carry the key. These two forms are equivalent:

```http
X-API-Key: aisub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

```http
Authorization: Bearer aisub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Keys are issued by us and look like `aisub_` + a 32-character random string. One key identifies one client, and all records are scoped to the key — you can only see tasks you submitted.

### 3.1 Integration security requirements

These are minimum requirements, not suggestions:

1. **The key lives only on your server.** Putting it in a browser, app, or mini-program is equivalent to publishing it.
2. **Never log the key, full activation keys, session JSON, or access tokens.** Log the `task_id` instead — it carries no sensitive data.
3. If your egress IP is static, tell us and we will enable an IP allowlist for your key.
4. If you suspect a leak, contact us immediately to revoke and reissue. A revoked key returns `401` on every request from that moment.
5. Keys do not expire, but rotate them on whatever schedule your own key-management policy requires.

---

## 4. Request and response conventions

### 4.1 Success responses

`success: true` and `client_name` are always present at the top level; business fields sit alongside them:

```json
{
  "success": true,
  "client_name": "Example Client",
  "task_id": "a1b2c3d4e5f64789abcd1234",
  "status": "queued"
}
```

### 4.2 Error responses

**Check both the HTTP status code and the `success` field** — never just one of them.

```json
{
  "success": false,
  "error": "invalid_card_format",
  "detail": "Activation key format is invalid. Please check that the key is complete (including dashes).",
  "detail_zh": "卡密格式不正确，请核对卡密是否完整（含横线）"
}
```

| Field | Description |
|---|---|
| `error` | Machine-readable error code. **Branch on this field only** — never match on message text |
| `detail` | English message, safe to show to end users |
| `detail_zh` | Chinese message, safe to show to end users |

### 4.3 Time and characters

- All timestamps are **UTC**, formatted `YYYY-MM-DD HH:MM:SS`.
- Activation keys are case-insensitive; the server uppercases them before processing.
- The request body must be a JSON object. Malformed JSON returns `400 invalid_json_body`.

### 4.4 Rate limits

Per key, sliding window:

| Class | Endpoint | Limit |
|---|---|---|
| Write | `POST /api/v1/gptplus/redeem` | 60 / minute |
| Read | All other `GET` endpoints | 600 / minute |

Exceeding the limit returns `429 rate_limited`. Back off exponentially (1s → 2s → 4s, capped at 30s) and retry — **except for the write endpoint, see 4.5.**

### 4.5 Retry semantics (required reading)

> **`redeem` is not idempotent. A successful submission consumes one use of the activation key; submitting again consumes another, with no refund.**

This API has no idempotency key. Therefore:

| Situation | Correct action |
|---|---|
| `2xx` with a `task_id` | Store the `task_id` and poll. Do not resubmit |
| `4xx` (parameter / key / format errors) | The key was not consumed — fix and resubmit |
| `429` | Not consumed — back off and resubmit |
| `5xx` / `502` / network timeout / no response | **Do not resubmit blindly.** First call `GET /api/v1/gptplus/cdk-info?code=<key>` or `GET /api/v1/gptplus/jobs?card_code=<key>` to check whether the task was actually created. If it was, poll it; resubmit only if it was not |

All read endpoints are safe to retry freely.

**Submission latency and timeouts**: before accepting a task, `redeem` verifies the account's subscription status. It usually returns within seconds but can take up to about **80 seconds** in the worst case; set the client timeout for submissions to **>= 90 seconds** (60 seconds would time out before the server does). After a timeout or `502 upstream_error` the task **may already have been created on our side** — verify first as per the table above (`jobs` / `cdk-info`). If the task does exist, resubmitting with the **same key** returns `409 card_in_progress` (with a `job_id`, equal to the original `task_id`) and does not create a second task; but **never submit the same account with a different key**.

---

## 5. API reference

### 5.1 Verify the API key

```http
GET /api/v1/ping
```

No parameters. Use it for integration self-checks and health probes.

```json
{ "success": true, "message": "pong", "client_name": "Example Client" }
```

---

### 5.2 Plan catalogue

```http
GET /api/v1/products
```

Returns every plan and key prefix currently on sale. Fetch it once at startup and cache it; use it for UI routing and input hints. Leave prefixes outside the catalog to the server — do not reject them locally. The catalog includes Pro 500 (`P500-`), whose `note` / `note_zh` describe the account conditions and queueing; the catalog returns no prices.

```json
{
  "success": true,
  "client_name": "Example Client",
  "total": 5,
  "items": [
    {
      "product": "gpt_plus_self",
      "name": "ChatGPT Plus",
      "name_zh": "ChatGPT Plus",
      "key_prefix": "PLS-",
      "key_sample": "PLS-XXXXXXXXXXXXXXXX",
      "credential": "session_json",
      "note": "Only accounts without an active subscription can be activated.",
      "note_zh": "仅支持当前无订阅的账号开通。"
    }
  ]
}
```

---

### 5.3 Service status

```http
GET /api/v1/gptplus/status
```

Live availability of the activation service. **Call it before showing your order entry point** — when `available` is `false`, stop the user there rather than letting a key be submitted into a maintenance window.

```json
{
  "success": true,
  "client_name": "Example Client",
  "available": true,
  "tier": "ready",
  "label": "Operational",
  "label_zh": "服务正常",
  "tone": "ok",
  "pending": 0,
  "wait_seconds": 0,
  "checked_at": "2026-09-17 10:10:50"
}
```

| Field | Values | Meaning |
|---|---|---|
| `available` | `true` / `false` | `false` means maintenance — do not submit |
| `tier` | `ready` / `busy` / `down` | `busy` means a long queue; submissions still work but take longer |
| `pending` | integer | Kept for compatibility; currently always 0, does not reflect queueing |
| `wait_seconds` | integer | Kept for compatibility; currently always 0 |

The result is cached server-side for 20 seconds, so there is no benefit to polling it frequently. It only reflects whether the service as a whole is available, **not the queue position of an individual task** (notably the single-lane Pro 500 queue) — rely on the task's own `status`.

---

### 5.4 Check activation key status

```http
GET /api/v1/gptplus/cdk-info?code=PLS-XXXXXXXXXXXXXXXX
```

| Parameter | Required | Description |
|---|---|---|
| `code` | yes | The activation key. `card_code` and `cdk` are also accepted |

**Call this before submitting** — it prevents the large majority of "this key was already used" support tickets.

```json
{
  "success": true,
  "client_name": "Example Client",
  "product": "gpt_plus_self",
  "product_name": "ChatGPT Plus",
  "product_name_zh": "ChatGPT Plus",
  "card_key": "PLS-XXXXXXXXXXXXXXXX",
  "status": "unused",
  "redeemable": true,
  "total": 0,
  "success_count": 0,
  "running_count": 0,
  "records": []
}
```

| Field | Description |
|---|---|
| `status` | `unused` / `running` (activation in progress) / `used` |
| `redeemable` | Whether it can be submitted now. **Do not submit when this is `false`** |
| `records` | Task history for this key, up to 20 entries; account emails are masked for privacy |

> Usage through any entry point is reflected here, not just your own key. To see the full details of tasks you submitted (including unmasked emails), use 5.6 and 5.7.

---

### 5.5 Submit an activation task

```http
POST /api/v1/gptplus/redeem
```

| Field | Type | Required | Description |
|---|---|---|---|
| `card_code` | string | yes | Activation key. `cdk` and `cdk_code` are also accepted |
| `session_data` | object \| string | yes | Complete session JSON of the target account, must contain `accessToken`. `session` and `session_json` are also accepted; a string is parsed server-side |
| `confirm_duplicate` | bool | no | Only after receiving `duplicate_email_confirm`, to indicate the user accepted the overwrite. Defaults to `false` — see [2.3 Duplicate top-up protection](#2-3-duplicate-top-up-protection) |

**Do not include any tier parameters** — they are ignored. The tier is always derived from the key prefix.

**Submission latency**: the account is verified before the task is accepted; this usually takes seconds but can take up to about 80 seconds, so set your client timeout to **>= 90 seconds** (see 4.5). A successful response only means the task was **accepted** (`queued`), not that the plan is active — poll for the result. A Pro 500 task may queue for a long time after acceptance, see 2.4.

```json
{
  "card_code": "P20X-XXXXXXXXXXXXXXXX",
  "session_data": {
    "accessToken": "eyJhbGciOi...",
    "user": { "email": "user@example.com" },
    "expires": "2026-12-16T00:00:00.000Z"
  }
}
```

Returns `200` on success:

```json
{
  "success": true,
  "client_name": "Example Client",
  "product": "gpt_pro_20x_self",
  "product_name": "ChatGPT Pro 20x",
  "product_name_zh": "ChatGPT Pro 20x",
  "task_id": "a1b2c3d4e5f64789abcd1234",
  "status": "queued",
  "message": "Accepted",
  "message_zh": "已受理",
  "card_key": "P20X-XXXXXXXXXXXXXXXX",
  "email": "user@example.com",
  "submitted_at": "2026-09-17 10:12:00"
}
```

The success response carries no `duplicate` field; repeat submissions are blocked at submit time (returns `duplicate_email_blocked` / `duplicate_email_confirm`, see 2.3).

---

### 5.6 Poll a task

```http
GET /api/v1/gptplus/task/{task_id}
```

Only tasks submitted with your key are visible; anything else returns `404 task_not_found`.

```json
{
  "success": true,
  "client_name": "Example Client",
  "product": "gpt_plus_self",
  "product_name": "ChatGPT Plus",
  "task_id": "a1b2c3d4e5f64789abcd1234",
  "status": "success",
  "finished": true,
  "message": "Activation completed",
  "message_zh": "充值成功",
  "error_code": "",
  "duration_seconds": 142.6,
  "card_key": "PLS-XXXXXXXXXXXXXXXX",
  "email": "user@example.com",
  "submitted_at": "2026-09-17 10:12:00"
}
```

| Field | Description |
|---|---|
| `status` | `queued` / `running` / `success` / `failed` |
| `finished` | `true` when `status` is `success` or `failed`. **Use this as your polling stop condition** |
| `message` / `message_zh` | Status text; on failure it is the reason, safe to show to end users |
| `error_code` | Non-empty only on failure — see [7. Error codes](#7-error-codes); whether the key was consumed and whether you may resubmit is defined in 7.3 |
| `duration_seconds` | Appears only after the task finishes: processing time in seconds, excluding time spent queued |
| `activation_email` | Reserved field, always an empty string; use `email` for the account email |
| `action_required` | Present only when the end user must complete a one-time security check. Value: `manual_captcha` (see "Security check" below) |
| `verification_url` | Present together with `action_required` — our hosted verification page |
| `verification_expires_at` | Deadline of the verification (Unix seconds); after it passes the field disappears and the task fails |

**Polling guidance**: every 3 seconds. A single task usually completes in 1–3 minutes. Cap polling at about 15 minutes (~300 attempts); **Pro 500 queues in a single lane and takes longer — allow at least 20 minutes** (see 2.4). On timeout, do not mark it failed — switch to low-frequency checks or human follow-up. Time spent waiting for the user's verification should not count against the timeout.

**Security check**: a small number of orders need the end user to complete a one-time security check while processing. The response then looks like:

```json
{
  "success": true,
  "status": "running",
  "finished": false,
  "action_required": "manual_captcha",
  "verification_url": "https://www.academicgate.org/verify/a1b2c3d4e5f64789abcd1234",
  "verification_expires_at": 1790000000
}
```

- `status` stays `running` (**not success**). Hand `verification_url` to that order's user: embed it in an iframe on your HTTPS page (we suggest `referrerpolicy="no-referrer"`) or open it in a new window.
- After the user finishes, **keep polling the same `task_id`** until `success` / `failed` — closing the window does not mean success. If the link expires and a new one appears, show it again.
- **Never resubmit because of this**; the link is for that order's user only — never log it or forward it to unrelated third parties.

---

### 5.7 Task history

```http
GET /api/v1/gptplus/jobs?limit=50&offset=0&card_code=&status=&product=
```

Your own task records, for reconciliation and post-timeout recovery.

| Parameter | Default | Description |
|---|---|---|
| `limit` | 50 | 1–200 |
| `offset` | 0 | Pagination offset |
| `card_code` | — | Exact-match filter by activation key |
| `status` | — | Exact-match filter by status — values in [6. Task state machine](#6-task-state-machine) |
| `product` | — | Filter by plan — values come from the `product` field of `GET /api/v1/products` |

```json
{
  "success": true,
  "client_name": "Example Client",
  "total": 128,
  "limit": 50,
  "offset": 0,
  "items": [
    {
      "task_id": "a1b2c3d4e5f64789abcd1234",
      "card_key": "PLS-XXXXXXXXXXXXXXXX",
      "product": "gpt_plus_self",
      "product_name": "ChatGPT Plus",
      "product_name_zh": "ChatGPT Plus",
      "status": "success",
      "message": "Activation completed",
      "email": "user@example.com",
      "created_at": "2026-09-17 10:12:00",
      "updated_at": "2026-09-17 10:14:31",
      "finished_at": "2026-09-17 10:14:31"
    }
  ]
}
```

---

## 6. Task state machine

```
                 ┌──────────┐
   redeem  ─────▶│  queued  │  accepted, waiting to be processed
                 └────┬─────┘
                      ▼
                 ┌──────────┐
                 │ running  │  activation in progress
                 └────┬─────┘
            ┌─────────┴─────────┐
            ▼                   ▼
      ┌──────────┐        ┌──────────┐
      │ success  │        │  failed  │   ← both terminal, finished: true
      └──────────┘        └──────────┘
```

- States **only move forward**; a terminal state never reverts.
- `rejected` appears only in the history endpoint (5.7). It means the request was rejected before entering the processing queue and **no key use was consumed**. It is never returned as `status` from the `task` endpoint.
- The history endpoint (5.7) can also show two further states that exist only there: `submitting` (a short placeholder while the task is being created, at most about 3 minutes, after which it moves to another state) and `reset` (a record we reset manually — check `cdk-info` for whether the key can be used again, and contact us if in doubt). The `status` of the `task` endpoint is always one of `queued` / `running` / `success` / `failed`.
- A Pro 500 task can stay `queued` for a long time (single queue across the whole system); this is not a fault, see 2.4.
- Polling after a terminal state keeps returning the same result and has no side effects.
- While `running`, a task may ask the end user for a security check (the response carries `action_required`). This is not a separate state; the task proceeds to a terminal state afterwards — see 5.6.

## 7. Error codes

Branch on the `error` field only.

### 7.1 Authentication and quota

| HTTP | `error` | Meaning | Action |
|---|---|---|---|
| 401 | `missing_api_key` | No key supplied | Check the header name |
| 401 | `invalid_api_key` | Key invalid or disabled | Contact us |
| 403 | `key_not_allowed` | This key is not authorised for this endpoint | Contact us for a full key |
| 429 | `rate_limited` | Rate limit exceeded | Exponential backoff |

### 7.2 Parameters and activation keys

| HTTP | `error` | Meaning | Action |
|---|---|---|---|
| 400 | `invalid_json_body` | Body is not a JSON object | Check `Content-Type` and serialisation |
| 400 | `missing_card_code` | No activation key supplied | — |
| 400 | `invalid_card_format` | Prefix not in the supported list | Compare against `GET /api/v1/products` |
| 400 | `missing_session` | No `session_data` supplied | — |
| 400 | `invalid_session_json` | Session is not a valid JSON object | Ask the user to copy the whole session page again |
| 400 | `session_missing_access_token` | Session has no `accessToken` | User is not logged in, or copied only a fragment |
| 400 | `missing_task_id` | No `task_id` supplied | — |
| 404 | `task_not_found` | Task does not exist or belongs to another key | Verify the `task_id` |
| 409 | `duplicate_email_blocked` | Account was topped up within the last 24 hours | Stop and inform the user; do not retry |
| 409 | `duplicate_email_confirm` | Account was topped up within 30 days; confirmation required | Have the user confirm, then resend with `confirm_duplicate: true` |
| 400 | `product_not_supported_here` | This key type cannot be redeemed here at the moment (for example, the entry for that plan is temporarily closed) | **Key not consumed**; contact us |

### 7.3 Activation business errors

These appear in `redeem` error responses (with an HTTP status code) or in the `error_code` of a `task` response (a failed task: HTTP `200`, `status: failed`); the same code can show up in both places. **Whether a key was consumed and whether you may resubmit with it is defined by the "Action" column: where it says "Key not consumed" you may resubmit after fixing the cause; where it says "do not resubmit" never retry automatically.**

| HTTP | `error` / `error_code` | Meaning | Action |
|---|---|---|---|
| 400 | `invalid_card_key` | Key invalid, unknown, not sold, or out of uses | Verify with `cdk-info`, then give the end user a definitive answer |
| 409 | `card_already_used` | Key was already redeemed successfully | Keys are single-use — use another |
| 409 | `card_in_progress` | Key already has a task in progress, or the same account just (within about 3 hours) completed a top-up with it; the response carries a `job_id` (equal to the original `task_id`; pollable directly if it was submitted with the same key; it may be absent for a few seconds right after acceptance) | Don't resubmit; poll the existing task with `task` (a task that already succeeded returns its success result) |
| 409 | `account_has_subscription` | Target account has an active subscription (**exceptions: a Pro 5x key accepts a Philippines-region account on an active Plus, and a Pro 20x key accepts a Philippines-region account on an active Plus / Pro 5x — both upgrade**; an account already on an active Pro 20x should renew after it expires; **Pro 500 accepts no subscription of any kind and has no upgrade path**) | **Key not consumed**; use an account without a subscription (or an upgradable account per the exceptions), or wait for expiry |
| 409 | `account_busy` | (Pro 500) The account already has a `PLS-` / `P5X-` / `P20X-` / `P500-` key task accepted / queued / running, regardless of entry point or reseller | **Key not consumed**; wait for the earlier task to finish, then submit |
| 409 | `email_frozen` | Submissions for this account (email) are paused after 2 consecutive billing-stage failures (failures rejected at the payment step); the counter resets after one success or when we unfreeze it | **Key not consumed**; do not keep retrying this account — contact us to unfreeze |
| 409 | `session_failed_too_many` | Submissions for this account (session) are paused after 3 consecutive failed activation attempts of any cause (`detail_zh`: 当前session失败次数过多，请联系苏哲); the counter resets after one success or when we unfreeze it | **Key not consumed**; do not keep retrying this account — contact us to unfreeze |
| 409 | `plus_not_ph_region` | The account's Plus is a non-Philippines (non-PHP) subscription; the Pro 5x / Pro 20x upgrade only supports a Philippines (PHP) subscription or a free account | **Key not consumed**; use a free account or a Philippines (PHP) Plus account |
| 409 | `promax_billing_not_php` | (Pro 500) The account's billing region is not the Philippines (PHP) | **Key not consumed**; use another account or contact us |
| 409 | `account_not_eligible_pro20` | The official Pro 20x offer is explicitly not enabled for this account (rarely seen) | **Key not consumed**; retry with another account (Free, or a PHP Plus / Pro 5x) |
| 409 | `account_not_eligible_promax` | (Pro 500) The official offer is not enabled for this account (rarely seen) | **Key not consumed**; contact us |
| 409 | `pro20_already_active` | The account is already on an active Pro 20x that has not expired | **Key not consumed**; renew after it expires |
| 400 | `session_expired` | The login session (Session/accessToken) has expired or is invalid | **Key not consumed**; have the user log in to ChatGPT again, copy the latest full session, and retry |
| 502 | `precheck_failed` | (Pro 20x / Pro 500) Temporarily unable to verify the account's subscription status or eligibility (precheck service unavailable); a task is **not accepted** without verification | **Key not consumed**; retry shortly |
| — | `session_missing_access_token` | Session is missing `accessToken` | Have the user re-copy the full session page |
| — | `invalid_session_data` | The session was judged invalid | Have the user re-export the session and retry with the new one |
| — | `invalid_product_line` | Key type not currently supported | Contact us to register it — usually a new key type |
| — | `invalid_recharge_method` | This key is temporarily unavailable | Contact us |
| — | `card_key_busy_other_product` | Key is in use by another task | Re-check with `cdk-info` shortly |
| 200 | `payment_declined` | The payment did not go through during activation/upgrade | **Key not consumed**; retry later or contact us. For Pro 500, if it failed after the base plan had been activated, a resubmission is rejected with `account_has_subscription` — that means the account is already on the base plan; contact us for manual handling |
| 200 | `upgrade_incomplete` | Activation incomplete: the account was set up on the base plan and the upgrade still has to be completed (can occur when activating Pro 20x on a Free account; Pro 500 does not use this code — see the next row) | **Key not consumed**; resubmit with the same key to finish, and contact support if it fails again |
| 200 | `promax_upgrade_incomplete` | (Pro 500) The base plan was activated but the upgrade to Pro 500 did not succeed; the account is now on the base plan | **Do not resubmit** (a resubmission is rejected because the account now has a subscription); contact us for manual handling. **The opposite of `upgrade_incomplete`** |
| 200 | `payment_unverified` | The payment request was sent but its result could not be confirmed (can occur on any plan) | **Do not resubmit** and do not retry with another key; contact us to reconcile |
| 200 | `verification_timeout` / `verification_failed` | The security verification timed out / did not pass | **Key not consumed**; you may resubmit |
| 200 | `account_verification_required` | This ChatGPT account needs additional verification and cannot be activated automatically right now | **Key not consumed**; contact us |
| 200 | `invoice_renew_required` | The account has an unpaid outstanding invoice and cannot be activated automatically | **Key not consumed**; settle the invoice or use another account, then retry |
| 200 | `promax_trial_unsupported` | (Pro 500) The account currently has an official trial offer and cannot be activated automatically; no charge was made | **Key not consumed**; contact us |
| 200 | `activation_failed` | Catch-all: the activation did not complete (the specific cause is not broken out externally) | **Key not consumed**; Plus / Pro 5x / Pro 20x may be retried later with the same key, contact us if it keeps failing; **for Pro 500 do not retry automatically** — contact us |

Codes with "—" in the HTTP column do not have a fixed place (they may be synchronous errors or task failure codes); take the status code from the actual response, and always branch on `error` / `error_code`.

> For keys currently on sale (`PLS-` / `P5X-` / `P20X-` / `P500-`), `invalid_card_key` / `account_has_subscription` / `session_missing_access_token` are all returned **before** the task is accepted, so **the key is not consumed** and you may resend after fixing it; `card_already_used` / `card_in_progress` mean the key is already in play — do not open a new order.

### 7.4 Server errors

| HTTP | `error` | Meaning | Action |
|---|---|---|---|
| 500 | `server_error` | Server-side exception | Contact us with the `task_id` |
| 502 | `upstream_error` | Activation service temporarily unresponsive | **Verify before resubmitting — see 4.5** |
| 503 | `unavailable` | Service under maintenance | Confirm recovery via `status` before submitting |
| 503 | `tier_maintenance` | Pro 5x / Pro 20x self-serve temporarily under maintenance, or Pro 500 not open at the moment (Pro 500 has its own switch and is not affected by the 5x / 20x maintenance); key not consumed; if the switch closes while a Pro 500 task is queued, it also appears as a failure code (HTTP 200) | Retry later; Plus is unaffected |
| 200 | `system_maintenance` | **Not an HTTP error**: it only appears as the `error_code` of a failed task (`status: failed`) and means the activation system is being updated; key not consumed | Wait a moment and retry with the same key |

## 8. Recommended integration flow

```
① User places an order on your site
        │
        ▼
② GET /api/v1/gptplus/status      ← if available=false, stop here; don't take the order
        │
        ▼
③ GET /api/v1/gptplus/cdk-info    ← if redeemable=false, use a different key
        │
        ▼
④ Collect the session JSON from the user
        │
        ▼
⑤ POST /api/v1/gptplus/redeem     ← persist the task_id immediately
        │
        ▼
⑥ GET /task/{id} every 3s until finished=true
        │
        ├─ success → notify the user
        └─ failed  → map error_code to a user-facing message
```

### 8.1 Fields you must persist

`task_id`, `card_code`, `product`, submission time, and last known status. These five are sufficient to diagnose any incident — none of them is optional.

### 8.2 Common pitfalls

| Symptom | Cause | Fix |
|---|---|---|
| Fails immediately with an invalid-session error | The user logged out of ChatGPT after copying the session | Require them to stay logged in during processing |
| A brand-new key reports as already used | The same key was also submitted through another entry point | Check `cdk-info` before submitting |
| Activation succeeded but the user sees no subscription | Client-side cache | Have them refresh the ChatGPT page or sign in again |
| A timeout retry consumed the key twice | Violates the retry semantics in 4.5 | On 5xx/timeout, verify before resubmitting |
| Pro 20x reports an existing subscription | The account is already on an active Pro 20x, or on a paid plan other than Plus / Pro 5x | Renew after the subscription expires; a Philippines-region Plus / Pro 5x account can upgrade directly, while non-Philippines billing returns `plus_not_ph_region` |
| Worried about double-consuming a key after a submit timeout | Submission can take up to ~80 seconds and the client timeout was too short; the task was in fact created | Use a timeout >= 90 seconds; after a timeout check `jobs`, or resubmit with the same key (an existing or just-completed task returns `409 card_in_progress` + `job_id`; just poll it as usual) |
| A Pro 500 task stays `queued` for a long time | Pro 500 uses a single queue across the whole system | Keep polling with an upper bound >= 20 minutes; do not resubmit (see 2.4) |
| A resubmission after a Pro 500 failure is rejected with `account_has_subscription` | The failure happened after the base plan was activated, so the account is already on it | Do not resubmit; contact us for manual handling (see 2.4, 7.3) |

### 8.3 Self-serve activation page (for end users)

If you would rather not build your own submission page, you can hand `https://www.academicgate.org/store` straight to your end users:

- **No API key is needed by the user**: they paste the activation key you gave them and their own session JSON. The page identifies the plan from the key prefix and supports Plus / Pro 5x / Pro 20x / Pro 500 (Pro 500 has its own tab, and pasting a `P500-` key switches to it automatically), in Chinese and English.
- Account rules, duplicate protection, failure freezes and failure messages are the same as in this API; tasks submitted through the page appear in the key history of `cdk-info`, but **not** in your `jobs` records (`jobs` contains only tasks submitted with your key).
- The page's per-IP limits are tighter than this API's (8 submissions / minute, 60 reads / minute); many users behind one egress IP may hit them.
- The page waits for a result for about 6.5 minutes (about 15 minutes for Pro 500), then stops refreshing and asks the user to look the key up again later; the task itself is not affected. The page has no account-eligibility precheck — eligibility is decided at submit time, and a rejected key is not consumed.
- The page is meant for end users in a browser only: it calls anonymous web-facing endpoints, not key-based ones, and **cannot** be called cross-origin from a third-party site. To embed the activation flow in your own site, call this API from your server (keep the key server-side, see 3.1); if you want an equivalent page under your own domain, contact us to discuss deployment.

## 9. Code examples

### 9.1 Python

```python
import time
import requests

BASE = "https://www.academicgate.org"
API_KEY = "aisub_your_key"
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json; charset=utf-8"}

class ActivationError(Exception):
    def __init__(self, code, detail):
        super().__init__(f"{code}: {detail}")
        self.code = code

def _check(resp):
    data = resp.json()
    if resp.status_code >= 400 or not data.get("success"):
        raise ActivationError(data.get("error", "http_%d" % resp.status_code),
                              data.get("detail") or "")
    return data

def activate(card_code, session_data, timeout_seconds=900):  # Pro 500 queues longer: use >= 1200
    # 1) Service availability
    if not _check(requests.get(f"{BASE}/api/v1/gptplus/status", headers=HEADERS, timeout=30))["available"]:
        raise ActivationError("unavailable", "Activation service is under maintenance")

    # 2) Key availability
    if not _check(requests.get(f"{BASE}/api/v1/gptplus/cdk-info",
                               params={"code": card_code}, headers=HEADERS, timeout=30))["redeemable"]:
        raise ActivationError("invalid_card_key", "Key already used or currently processing")

    # 3) Submit. A success consumes a use — never retry here. It can take up to ~80s, so keep timeout >= 90; after a timeout/502 check jobs first
    job = _check(requests.post(f"{BASE}/api/v1/gptplus/redeem", headers=HEADERS, timeout=90,
                               json={"card_code": card_code, "session_data": session_data}))
    task_id = job["task_id"]
    print("submitted", task_id, job["product_name"])

    # 4) Poll
    deadline = time.time() + timeout_seconds
    while time.time() < deadline:
        time.sleep(3)
        try:
            st = _check(requests.get(f"{BASE}/api/v1/gptplus/task/{task_id}", headers=HEADERS, timeout=30))
        except (requests.RequestException, ActivationError):
            continue  # reads are safe to retry; keep polling through transient errors
        if st["finished"]:
            return st
    raise ActivationError("poll_timeout", f"No result after {timeout_seconds}s, task_id={task_id}")
```

### 9.2 Node.js

```js
const BASE = 'https://www.academicgate.org';
const API_KEY = process.env.AISUB_API_KEY;
const HEADERS = { 'X-API-Key': API_KEY, 'Content-Type': 'application/json; charset=utf-8' };

class ActivationError extends Error {
  constructor(code, detail) { super(`${code}: ${detail}`); this.code = code; }
}

async function call(path, options = {}) {
  const res = await fetch(BASE + path, { headers: HEADERS, ...options });
  const data = await res.json();
  if (!res.ok || !data.success) {
    throw new ActivationError(data.error || `http_${res.status}`, data.detail || '');
  }
  return data;
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function activate(cardCode, sessionData, timeoutMs = 900_000) { // Pro 500 queues longer: use >= 1_200_000
  const status = await call('/api/v1/gptplus/status');
  if (!status.available) throw new ActivationError('unavailable', 'Service under maintenance');

  const info = await call(`/api/v1/gptplus/cdk-info?code=${encodeURIComponent(cardCode)}`);
  if (!info.redeemable) throw new ActivationError('invalid_card_key', 'Key already used or processing');

  // A success consumes a use — never retry here. It can take up to ~80s, so any request timeout must be >= 90s
  const job = await call('/api/v1/gptplus/redeem', {
    method: 'POST',
    body: JSON.stringify({ card_code: cardCode, session_data: sessionData }),
  });

  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    await sleep(3000);
    try {
      const st = await call(`/api/v1/gptplus/task/${job.task_id}`);
      if (st.finished) return st;
    } catch (_) { /* reads are safe; keep polling */ }
  }
  throw new ActivationError('poll_timeout', `Timed out, task_id=${job.task_id}`);
}
```

---

## 10. Data handling and privacy

### 10.1 Session credentials

`session_data` is **used solely to complete that one activation and is never stored in any form.** It is not written to a database, not persisted to disk, and not written to logs; it is released from memory once processing finishes.

### 10.2 What we retain

For reconciliation, support, and abuse prevention we retain:

| Data | Purpose | Retention |
|---|---|---|
| Activation key, task ID, plan, status and result message | Reconciliation and support evidence | Long-term |
| The account email extracted from the session | Identifying which account was topped up | Long-term |
| Caller key identifier and request IP | Attribution and incident investigation | Long-term |

Emails are masked in the `cdk-info` history for all callers. Tasks you submitted yourself return the full value through the `task` and `jobs` endpoints.

### 10.3 Your obligations

An end user's ChatGPT session is their personal credential. Please disclose clearly in your product how that credential is used, and comply with the data-protection requirements of your jurisdiction. We process the data you send strictly as described in this section.

---

## 11. Service levels and support

| Item | Description |
|---|---|
| Per-task duration | Typically 1–3 minutes; longer at peak times. Cap polling at 15 minutes. Pro 500 queues in a single lane across the whole system, takes longer and varies — allow at least 20 minutes |
| Planned maintenance | `status` returns `available: false` during the window — use it to gate ordering |
| Incident handling | Reflected live through `status` and error codes; service resumes automatically with no action needed on your side |
| Technical support | Answered during business hours; mark urgent issues as "urgent" |

When raising a support request, provide the **`task_id`, the first 6 characters of the activation key, and the UTC timestamp**.

> For security reasons, **never send us a full activation key, session JSON, or access token through any channel.** We will never ask you for them.

Contact details are in the footer of `https://suzhe.ai`.

---

## 12. Go-live checklist

Confirm each item before sending real traffic:

- [ ] The API key lives only in server-side secret management — it is not findable in the repo or in front-end bundles
- [ ] Logs contain no keys, full activation keys, session JSON, or access tokens
- [ ] Both the HTTP status code and the `success` field are checked
- [ ] Business logic branches on `error`, not on `detail` text
- [ ] The 5xx/timeout path for `redeem` verifies before resubmitting, rather than retrying blindly
- [ ] `task_id`, `card_code`, `product`, submission time, and last status are all persisted
- [ ] Polling has an upper bound, and timeouts escalate to a human instead of being marked failed
- [ ] The client timeout for submissions is >= 90 seconds, and after a timeout you verify (`jobs` / `cdk-info`) before deciding whether to resend
- [ ] Pro 500: only free accounts are submitted; `promax_upgrade_incomplete` / `payment_unverified` are never auto-resubmitted; the polling cap is >= 20 minutes
- [ ] Key prefixes come from `GET /api/v1/products` and are not hardcoded; prefixes outside the catalog are left to the server, not rejected locally
- [ ] `status` is checked before the order entry point; orders are not taken during maintenance
- [ ] `429` triggers exponential backoff
- [ ] `duplicate_email_confirm` follows "show the user → user confirms → resend", not an unconditional auto-confirm in code
- [ ] One full success path and one failure path have been exercised end to end

---

## 13. Versioning

- The version is pinned in the path (`/api/v1/...`).
- **Backward-compatible changes do not bump the version** and can happen at any time. Your parser must tolerate: new response fields, new error codes, new plans and key prefixes, new optional request parameters.
- **Breaking changes ship under a new path prefix** (e.g. `/api/v2/`). The old version stays available for at least 90 days with advance notice.
- Parse responses in a way that ignores unknown fields — do not use strict deserialisation.

---

## 14. Changelog

| Date | Change |
|---|---|
| 2026-10-10 | **Documentation addendum (no API change)**: clarified `409 card_in_progress` — besides "the key already has a task in progress", it is also returned when the same account resubmits the same key within about 3 hours after a successful top-up (with the original `job_id`; polling returns the success result). This makes retrying after a network interruption or a lost response safe; after the window `card_already_used` is still returned. |
| 2026-10-06 | **ChatGPT Pro 500 added**: key prefix `P500-`, `product` is `gpt_pro_500_self`, and the `GET /api/v1/products` catalog grows from 4 to 5 items; same set of endpoints, and the self-serve page is opened to it as well. Free accounts with no subscription only (no upgrade, no price-difference top-up); delivered manually, not sold online; a single queue across the whole system, so a task may wait several minutes; **the failure codes `promax_upgrade_incomplete` / `payment_unverified` mean do not resubmit** (the opposite of `upgrade_incomplete`). New error codes `account_busy`, `promax_billing_not_php`, `account_not_eligible_promax`, `promax_trial_unsupported`, `promax_upgrade_incomplete` — see 2.4 and 7.3. **Endpoint paths and existing fields unchanged — existing integrations need no changes.** |
| 2026-10-06 | **Documentation aligned with live behaviour (no API change)**: 7.3 now lists HTTP status codes and the error codes that were already live but undocumented (`pro20_already_active`, `payment_unverified`, `verification_timeout`, `verification_failed`, `account_verification_required`, `invoice_renew_required`, `activation_failed`), and states the thresholds of `email_frozen` (2 consecutive billing-stage failures) and `session_failed_too_many` (3 consecutive failures of any cause); `card_in_progress` carries a `job_id`; `system_maintenance` is corrected to be the `error_code` of a failed task (not an HTTP 503); submission can take up to ~80 seconds, so set client timeouts to >= 90 seconds; the `task` response gains `duration_seconds` and `jobs` records gain the `submitting` / `reset` states; `pending` / `wait_seconds` of `status` are marked as compatibility fields; the 2.3 duplicate example now shows `last_product_zh` / `blocked`; `account_not_eligible_pro20` is marked as rarely seen; new section 8.3 on the self-serve page. |
| 2026-09-30 | **Pro 20x now supports new activation and upgrades**: accounts with no subscription can be activated directly; accounts already on an active Philippines (PHP) Plus / Pro 5x are auto-upgraded to Pro 20x (no need to cancel first). An account already on an active 20x still renews after expiry. New error `upgrade_incomplete` (account set up on the base plan, upgrade difference still pending; key not consumed, resubmit with the same key); `account_not_eligible_pro20` now means "the official offer is not enabled for this account". **Endpoint paths and fields unchanged.** |
| 2026-09-25 | **Pro 5x now additionally supports an account with an active ChatGPT Plus** (auto-upgraded to Pro 5x, no need to cancel Plus first — no longer returns `account_has_subscription`). (The Pro 20x eligibility rules were updated on 2026-09-30 — see the row above.) **Endpoint paths and fields unchanged.** |
| 2026-09-24 | Key prefixes updated to `PLS-` (Plus) / `P5X-` (Pro 5x) / `P20X-` (Pro 20x); `product` is `gpt_plus_self` / `gpt_pro_5x_self` / `gpt_pro_20x_self` (see 2, 5.2). Only accounts without an active subscription are supported; otherwise the new error `account_has_subscription` is returned. Also new: `card_already_used` / `card_in_progress` (see 7.3). Keys issued earlier still redeem but no longer appear in the `/products` catalog. **Trust the live `/products` response — do not hardcode prefixes, and do not reject prefixes outside the catalog locally.** |
| 2026-09-24 | The security check is unified as `action_required: "manual_captcha"` + `verification_url` (our address), opened in an iframe or new window; `status` stays `running` — keep polling the same `task_id` after completion (see 5.6). No other verification type is issued. |
| 2026-09-23 | API base URL changed to `https://www.academicgate.org` (`aisubscription.vip` is retired). Paths, parameters and API keys are unchanged — only the domain changes. |
| 2026-09-17 | Added duplicate top-up protection: a second submission for the same account within 24 hours is rejected outright; within 30 days it requires `confirm_duplicate: true`. See 2.3. New error codes `duplicate_email_blocked` / `duplicate_email_confirm`. |
| 2026-09-17 | Coverage extended to all GPT plans (Plus / Pro 5x / Pro 20x new and renewal). Added `GET /api/v1/products`; added `product` and related fields to `cdk-info` / `redeem` / `task` / `jobs`; added a `product` filter to `jobs`. Outbound fields tightened at the same time: internal identifiers removed, and account emails in `cdk-info` history are now masked (tasks you submitted still return the full value through `task` / `jobs`). **Endpoint paths and existing business fields are unchanged — existing integrations need no changes.** |
| 2026-09-07 | Docs moved to `suzhe.ai/gptapi`; the API base URL is fixed at `aisubscription.vip`. |
| 2026-08 | ChatGPT Plus key format updated. |
