# gocdn API reference 2.1.1792967583

## Authentication

Send the X-API-Key header (create a key on the API Keys page; the key is shown only once at creation). An Authorization: Bearer token obtained by logging in also works.

1. X-API-Key is checked first (not expired or deleted)
2. otherwise the Authorization: Bearer access token
3. then the key scopes and the operation scope (tenants only reach their own resources)

Authentication failures return 401; forbidden returns 403 (code such as insufficient_scope).

## Responses and errors

Success returns the resource JSON directly (HTTP 2xx); failures return application/problem+json (code, detail, plus errors on validation failures); lists carry items + total.

## Conventions

- Paging uses page (from 1) and page_size; sort with sort=field or sort=-field.
- Times are RFC 3339 (UTC); amounts are in cents.
- Writes return 200/201/204 on success; duplicates of a unique key return 409.

## Authentication (`auth`)

Login, token refresh, logout, password change, TOTP, password reset, captcha and admin SSO. Login endpoints need no authentication.

Authentication: No auth · Docs: https://www-demo.kuxitong.com/en/docs/api/auth

### GET `/api/v2/auth/captcha`

Get captcha · Authentication: No auth

```sh
curl "https://panel.example.com/api/v2/auth/captcha"
```

### POST `/api/v2/auth/login`

Log in · Authentication: No auth

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `captcha_code` | string |  | Characters shown in the captcha image |
| `captcha_id` | string |  | Captcha id from GET /auth/captcha; required after repeated failed logins |
| `password` | string | Yes | Login password |
| `totp_code` | string |  | Current 6-digit TOTP code, or an unused recovery code (XXXXX-XXXXX, consumed on use); required when the account has TOTP enabled |
| `username` | string | Yes | Username |
| `verify_code` | string |  | Second-factor login code. Required when the account has email/SMS code login (login_verify) enabled; obtain it first via POST /verify-codes (purpose=login) |

```sh
curl -X POST "https://panel.example.com/api/v2/auth/login"
```

### POST `/api/v2/auth/logout`

Log out · Authentication: User

```sh
curl -X POST "https://panel.example.com/api/v2/auth/logout" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/auth/password`

Change password · Authentication: User

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `new_password` | string | Yes | New password, 8 to 128 characters |
| `old_password` | string | Yes | Current password |

```sh
curl -X PUT "https://panel.example.com/api/v2/auth/password" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/auth/password-reset`

Reset password · Authentication: No auth

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `channel` | string | Yes | Recover by email or by phone number Values: ["email","sms"] |
| `code` | string | Yes | Verification code sent to target (POST /verify-codes, purpose=reset_password) |
| `new_password` | string | Yes | New password, 8 to 128 characters |
| `target` | string | Yes | Email or phone number bound to the account |

```sh
curl -X POST "https://panel.example.com/api/v2/auth/password-reset" \
  -H "Content-Type: application/json" \
  -d '{"channel":"email","code":"123456","new_password":"${NEW_PASSWORD}","target":"user@example.com"}'
```

### POST `/api/v2/auth/refresh`

Refresh token · Authentication: No auth

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `refresh_token` | string | Yes | Refresh token, exchanged for a new token pair |

```sh
curl -X POST "https://panel.example.com/api/v2/auth/refresh"
```

### POST `/api/v2/auth/sso`

Admin SSO login · Authentication: No auth

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `sso_ticket` | string | Yes | Login ticket issued by the super admin: base64url (no padding) bytes of a licwire envelope (kind sso_ticket) |

```sh
curl -X POST "https://panel.example.com/api/v2/auth/sso" \
  -H "Content-Type: application/json" \
  -d '{"sso_ticket":"eyJib2R5Ijp7fX0"}'
```

### POST `/api/v2/auth/switch`

Switch admin to user · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `user_id` | integer | Yes | User ID |

```sh
curl -X POST "https://panel.example.com/api/v2/auth/switch" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"user_id":2}'
```

### POST `/api/v2/auth/switch-login`

Log in with a switch ticket · Authentication: No auth

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `switch_ticket` | string | Yes | One-time switch ticket obtained by an administrator. |

```sh
curl -X POST "https://panel.example.com/api/v2/auth/switch-login"
```

### POST `/api/v2/auth/switch-ticket`

Create a one-time switch ticket · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `user_id` | integer | Yes | User ID |

```sh
curl -X POST "https://panel.example.com/api/v2/auth/switch-ticket" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"user_id":2}'
```

### POST `/api/v2/auth/totp`

Generate TOTP secret · Authentication: User

```sh
curl -X POST "https://panel.example.com/api/v2/auth/totp" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/auth/totp`

Enable TOTP · Authentication: User

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | string | Yes | Verification code sent to target (POST /verify-codes, purpose=reset_password) |

```sh
curl -X PUT "https://panel.example.com/api/v2/auth/totp" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/auth/totp`

Disable TOTP · Authentication: User

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | string | Yes | Verification code sent to target (POST /verify-codes, purpose=reset_password) |

```sh
curl -X DELETE "https://panel.example.com/api/v2/auth/totp" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Authentication object:

| Field | Type | Required | Description |
|---|---|---|---|
| `access_token` | string |  | Access token, sent as Authorization: Bearer |
| `captcha_code` | string |  | Characters shown in the captcha image |
| `captcha_id` | string |  | Captcha id from GET /auth/captcha; required after repeated failed logins |
| `channel` | string | Yes | Recover by email or by phone number Values: ["email","sms"] |
| `code` | string | Yes | Verification code sent to target (POST /verify-codes, purpose=reset_password) |
| `expires_in` | integer |  | Access token lifetime in seconds |
| `id` | string |  | Pass back as captcha_id when logging in; single use, invalidated after one check whether right or wrong |
| `image` | string |  | PNG data URI |
| `key` | string |  | Base32 secret (returned only once, here) |
| `must_change_password` | boolean |  | Login response only; true means the account still uses the installer-generated initial password and must change it first. |
| `new_password` | string | Yes | New password, 8 to 128 characters |
| `old_password` | string | Yes | Current password |
| `otpauth_uri` | string |  | otpauth:// URI to add to an authenticator app by scanning |
| `password` | string | Yes | Login password |
| `recovery_codes` | string[] |  | Eight one-time recovery codes (XXXXX-XXXXX), returned only once, when TOTP is enabled |
| `refresh_token` | string |  | Refresh token, exchanged for a new token pair |
| `sso_ticket` | string | Yes | Login ticket issued by the super admin: base64url (no padding) bytes of a licwire envelope (kind sso_ticket) |
| `switch_ticket` | string | Yes | One-time switch ticket obtained by an administrator. |
| `target` | string | Yes | Email or phone number bound to the account |
| `token_type` | string |  | Token type, always Bearer |
| `totp_code` | string |  | Current 6-digit TOTP code, or an unused recovery code (XXXXX-XXXXX, consumed on use); required when the account has TOTP enabled |
| `user_id` | integer | Yes | User ID |
| `username` | string | Yes | Username |
| `verify_code` | string |  | Second-factor login code. Required when the account has email/SMS code login (login_verify) enabled; obtain it first via POST /verify-codes (purpose=login) |

## Users (`users`)

Admins manage platform accounts: create, view, update, enable/disable, reset passwords, turn off TOTP. An account must be disabled before it can be deleted.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/users

### GET `/api/v2/users`

List users · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `ids` | string |  | Only list these IDs: comma-separated positive integers, at most 200 after deduplication (same as the page_size limit; more returns 400); an empty value is the same as omitting it. Combined with other filters as an intersection; IDs you cannot see are silently left out. Pagination is unchanged: set page_size to at least the number of IDs to get them in one page |
| `email` | string |  | Email |
| `phone` | string |  | Phone number |
| `username` | string |  | Username |

```sh
curl "https://panel.example.com/api/v2/users" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/users`

Create user · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | string |  | Email |
| `enabled` | boolean |  | Whether enabled |
| `login_ip_allow` | string[] |  | Login IP allowlist (IPs or CIDRs); empty means no restriction |
| `login_verify` | LoginVerify |  | Second-factor login: none means not required; email / sms also require an email/SMS code at login (the platform must allow it and the address must be bound) Values: ["none","email","sms"] |
| `password` | string |  | Required on create; on update, providing it resets the password and revokes all sessions of the user |
| `phone` | string |  | Phone number |
| `role` | string |  | Account role: admin or tenant Values: ["admin","tenant"] |
| `totp_enabled` | boolean |  | Whether TOTP is enabled |
| `username` | string | Yes | Username |

```sh
curl -X POST "https://panel.example.com/api/v2/users" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/users/batch`

Batch enable, disable or delete users · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | string | Yes | enable, disable or delete. Values: ["enable","disable","delete"] |
| `ids` | integer[] | Yes | User IDs to operate on, 1 to 100 per call. |

```sh
curl -X POST "https://panel.example.com/api/v2/users/batch" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/users/{id}`

Get user · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/users/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/users/{id}`

Update user · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | string |  | Email |
| `enabled` | boolean |  | Whether enabled |
| `login_ip_allow` | string[] |  | Login IP allowlist (IPs or CIDRs); empty means no restriction |
| `login_verify` | LoginVerify |  | Second-factor login: none means not required; email / sms also require an email/SMS code at login (the platform must allow it and the address must be bound) Values: ["none","email","sms"] |
| `password` | string |  | Required on create; on update, providing it resets the password and revokes all sessions of the user |
| `phone` | string |  | Phone number |
| `role` | string |  | Account role: admin or tenant Values: ["admin","tenant"] |
| `totp_enabled` | boolean |  | Whether TOTP is enabled |
| `username` | string | Yes | Username |

```sh
curl -X PUT "https://panel.example.com/api/v2/users/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/users/{id}`

Delete user · Authentication: Admin only

```sh
curl -X DELETE "https://panel.example.com/api/v2/users/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/users/{user_id}/account`

Get user account · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/users/{user_id}/account" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/users/{user_id}/account`

Freeze or unfreeze user account · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `freeze_reason` | string |  | Freeze reason; empty if not frozen. |
| `frozen` | boolean | Yes | Whether the account is frozen; deductions are rejected while frozen. |

```sh
curl -X PUT "https://panel.example.com/api/v2/users/{user_id}/account" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"freeze_reason":"风控核查","frozen":true}'
```

### GET `/api/v2/users/{user_id}/api-keys`

List a user's API keys · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |

```sh
curl "https://panel.example.com/api/v2/users/{user_id}/api-keys" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/users/{user_id}/api-keys`

Create an API key for a user · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `allow_ips` | ApiKeyAllowIps |  | Caller IP allowlist (single IPs only, at most 200); empty means no restriction. Calls from other IPs get 401 as invalid credentials |
| `expires_at` | string |  | Expires at |
| `name` | string | Yes | Name |
| `scopes` | string[] | Yes | Granted scopes, same values as scopes at creation |

```sh
curl -X POST "https://panel.example.com/api/v2/users/{user_id}/api-keys" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/users/{user_id}/api-keys/{id}`

Update a user's API key IP allowlist or reset it · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `allow_ips` | ApiKeyAllowIps |  | Caller IP allowlist (single IPs only, at most 200); empty means no restriction. Calls from other IPs get 401 as invalid credentials |
| `reset` | boolean |  | When true, issue a new key; the old one stops working at once |

```sh
curl -X PUT "https://panel.example.com/api/v2/users/{user_id}/api-keys/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reset":true}'
```

### DELETE `/api/v2/users/{user_id}/api-keys/{id}`

Delete a user's API key · Authentication: Admin only

```sh
curl -X DELETE "https://panel.example.com/api/v2/users/{user_id}/api-keys/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/users/{user_id}/recharges`

Recharge or deduct user balance · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `amount_cents` | integer | Yes | Amount in cents; must be a positive integer. |
| `direction` | string |  | credit = recharge (default), debit = deduct. Values: ["credit","debit"] Default: "credit" |
| `remark` | string |  | Remark |

```sh
curl -X POST "https://panel.example.com/api/v2/users/{user_id}/recharges" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount_cents":10000,"direction":"credit","remark":"线下转账"}'
```

Users object:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | string | Yes | enable, disable or delete. Values: ["enable","disable","delete"] |
| `affected` | integer |  | Number of users processed. |
| `allow_ips` | ApiKeyAllowIps |  | Caller IP allowlist (single IPs only, at most 200); empty means no restriction. Calls from other IPs get 401 as invalid credentials |
| `amount_cents` | integer | Yes | Amount in cents; must be a positive integer. |
| `archived` | boolean |  | Moved into the archive table by the archiving task (read-only). |
| `balance_cents` | integer |  | Account balance in cents, same as balance_cents of GET /users/{user_id}/account (0 when there is no account). Only present in the GET /users list, not in single-user responses (read-only) |
| `certified` | boolean |  | Whether real-name verification has passed |
| `client_ip` | string |  | Source IP of this request, derived the same way as the login IP allowlist check (what the allowlist compares is what is shown). Only present in GET /me and PUT /me (read-only) |
| `created_at` | string |  | Created at (read-only) |
| `data` | object |  | Extra data (such as package purchase details). |
| `delegated_session` | boolean |  | Whether the current session was issued by superadmin sign-in or switch-to-user (if so, password, email, phone, login allowlist, login verification and real-name are read-only). Only present in GET /me and PUT /me (read-only) |
| `des` | string |  | Description |
| `direction` | string |  | credit = recharge (default), debit = deduct. Values: ["credit","debit"] Default: "credit" |
| `email` | string |  | Email |
| `enabled` | boolean |  | Whether enabled |
| `expires_at` | string |  | Expires at |
| `freeze_reason` | string |  | Freeze reason; empty if not frozen. |
| `frozen` | boolean |  | Whether the account is frozen; deductions are rejected while frozen. |
| `id` | integer |  | Record ID (read-only) |
| `ids` | integer[] | Yes | User IDs to operate on, 1 to 100 per call. |
| `last_used_at` | string |  | Time of last use; null if never used |
| `ledger_id` | integer |  | ID of the linked ledger entry; null if none. |
| `login_ip_allow` | string[] |  | Login IP allowlist (IPs or CIDRs); empty means no restriction |
| `login_verify` | LoginVerify |  | Second-factor login: none means not required; email / sms also require an email/SMS code at login (the platform must allow it and the address must be bound) Values: ["none","email","sms"] |
| `mch_order_no` | string |  | Merchant order number (the gateway-side order number); null for non-online payments. |
| `name` | string |  | Name |
| `paid_at` | string |  | Payment time; null if unpaid. |
| `password` | string |  | Required on create; on update, providing it resets the password and revokes all sessions of the user |
| `pay_type` | string |  | admin/balance/alipay/wxpay, etc.; empty = unpaid. |
| `phone` | string |  | Phone number |
| `prefix` | string |  | First few characters of the plaintext key, for recognition |
| `remark` | string |  | Remark |
| `reset` | boolean |  | When true, issue a new key; the old one stops working at once |
| `role` | string |  | Account role: admin or tenant Values: ["admin","tenant"] |
| `scopes` | string[] |  | Granted scopes, same values as scopes at creation |
| `secret` | string |  | Plaintext key (the X-API-Key value), returned only once at creation; only a hash is stored |
| `status` | string |  | Order status: unpaid or paid. Values: ["unpaid","paid"] |
| `tenant_id` | integer |  | Owner tenant (user) ID (read-only) |
| `totp_enabled` | boolean |  | Whether TOTP is enabled |
| `totp_recovery_left` | integer |  | Number of unused TOTP recovery codes. Only present in GET /me and PUT /me when TOTP is enabled (read-only) |
| `transaction_id` | string |  | Gateway-side transaction ID; empty if unpaid. |
| `type` | string |  | recharge (admin recharge) / deduct (admin deduction) / payment (online payment) / package (package purchase), etc. |
| `updated_at` | string |  | Updated at (read-only) |
| `user_id` | integer |  | User ID |
| `username` | string |  | Username |

## Current user (`me`)

The signed-in user views or edits their own profile, real-name verification and UI preferences.

Authentication: User · Docs: https://www-demo.kuxitong.com/en/docs/api/me

### GET `/api/v2/me`

Get current user · Authentication: User

```sh
curl "https://panel.example.com/api/v2/me" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/me`

Update profile · Authentication: User

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | string |  | Email |
| `email_code` | string |  | Code sent to the new email (POST /verify-codes, purpose=bind); required when changing the email |
| `login_ip_allow` | string[] |  | Login IP allowlist (IPs or CIDRs); empty means no restriction |
| `login_verify` | LoginVerify |  | Second-factor login: none means not required; email / sms also require an email/SMS code at login (the platform must allow it and the address must be bound) Values: ["none","email","sms"] |
| `phone` | string |  | Phone number |
| `phone_code` | string |  | Code sent to the new phone number (POST /verify-codes, purpose=bind); required when changing the phone number |

```sh
curl -X PUT "https://panel.example.com/api/v2/me" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/me/certification`

Get real-name verification status · Authentication: User

```sh
curl "https://panel.example.com/api/v2/me/certification" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/me/certification`

Submit real-name verification · Authentication: User

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `id_number` | string | Yes | Masked ID number |
| `real_name` | string | Yes | Masked real name |

```sh
curl -X POST "https://panel.example.com/api/v2/me/certification" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"id_number":"11010519491231002X","real_name":"张三"}'
```

### GET `/api/v2/me/preferences`

Get preferences · Authentication: User

```sh
curl "https://panel.example.com/api/v2/me/preferences" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/me/preferences`

Update preferences · Authentication: User

```sh
curl -X PUT "https://panel.example.com/api/v2/me/preferences" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Current user object:

| Field | Type | Required | Description |
|---|---|---|---|
| `balance_cents` | integer |  | Account balance in cents, same as balance_cents of GET /users/{user_id}/account (0 when there is no account). Only present in the GET /users list, not in single-user responses (read-only) |
| `certified` | boolean |  | Whether real-name verification has passed |
| `client_ip` | string |  | Source IP of this request, derived the same way as the login IP allowlist check (what the allowlist compares is what is shown). Only present in GET /me and PUT /me (read-only) |
| `created_at` | string |  | Created at (read-only) |
| `delegated_session` | boolean |  | Whether the current session was issued by superadmin sign-in or switch-to-user (if so, password, email, phone, login allowlist, login verification and real-name are read-only). Only present in GET /me and PUT /me (read-only) |
| `email` | string |  | Email |
| `email_code` | string |  | Code sent to the new email (POST /verify-codes, purpose=bind); required when changing the email |
| `enabled` | boolean |  | Whether enabled |
| `id` | integer |  | Record ID (read-only) |
| `id_number` | string |  | Masked ID number |
| `login_ip_allow` | string[] |  | Login IP allowlist (IPs or CIDRs); empty means no restriction |
| `login_verify` | LoginVerify |  | Second-factor login: none means not required; email / sms also require an email/SMS code at login (the platform must allow it and the address must be bound) Values: ["none","email","sms"] |
| `phone` | string |  | Phone number |
| `phone_code` | string |  | Code sent to the new phone number (POST /verify-codes, purpose=bind); required when changing the phone number |
| `provider` | string |  | Real-name verification provider |
| `real_name` | string |  | Masked real name |
| `reason` | string |  | Reason for failure |
| `role` | string |  | Account role: admin or tenant Values: ["admin","tenant"] |
| `status` | string |  | Verification status: none, pending, verified or failed Values: ["none","pending","verified","failed"] |
| `totp_enabled` | boolean |  | Whether TOTP is enabled |
| `totp_recovery_left` | integer |  | Number of unused TOTP recovery codes. Only present in GET /me and PUT /me when TOTP is enabled (read-only) |
| `updated_at` | string |  | Updated at (read-only) |
| `url` | string |  | Verification URL the user must visit while pending (returned only on submission) |
| `username` | string |  | Username |

## API keys (`api-keys`)

API keys of the current user. The plaintext key is returned only once at creation, and scopes can be limited per domain.

Authentication: User · Docs: https://www-demo.kuxitong.com/en/docs/api/api-keys

### GET `/api/v2/api-keys`

List API keys · Authentication: User

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |

```sh
curl "https://panel.example.com/api/v2/api-keys" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/api-keys`

Create API key · Authentication: User

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `allow_ips` | ApiKeyAllowIps |  | Caller IP allowlist (single IPs only, at most 200); empty means no restriction. Calls from other IPs get 401 as invalid credentials |
| `expires_at` | string |  | Expires at |
| `name` | string | Yes | Name |
| `scopes` | string[] | Yes | Granted scopes, same values as scopes at creation |

```sh
curl -X POST "https://panel.example.com/api/v2/api-keys" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/api-keys/{id}`

Update API key IP allowlist or reset it · Authentication: User

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `allow_ips` | ApiKeyAllowIps |  | Caller IP allowlist (single IPs only, at most 200); empty means no restriction. Calls from other IPs get 401 as invalid credentials |
| `reset` | boolean |  | When true, issue a new key; the old one stops working at once |

```sh
curl -X PUT "https://panel.example.com/api/v2/api-keys/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reset":true}'
```

### DELETE `/api/v2/api-keys/{id}`

Delete API key · Authentication: User

```sh
curl -X DELETE "https://panel.example.com/api/v2/api-keys/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

API keys object:

| Field | Type | Required | Description |
|---|---|---|---|
| `allow_ips` | ApiKeyAllowIps |  | Caller IP allowlist (single IPs only, at most 200); empty means no restriction. Calls from other IPs get 401 as invalid credentials |
| `created_at` | string |  | Created at (read-only) |
| `expires_at` | string |  | Expires at |
| `id` | integer |  | Record ID (read-only) |
| `last_used_at` | string |  | Time of last use; null if never used |
| `name` | string |  | Name |
| `prefix` | string |  | First few characters of the plaintext key, for recognition |
| `reset` | boolean |  | When true, issue a new key; the old one stops working at once |
| `scopes` | string[] |  | Granted scopes, same values as scopes at creation |
| `secret` | string |  | Plaintext key (the X-API-Key value), returned only once at creation; only a hash is stored |
| `updated_at` | string |  | Updated at (read-only) |

## Registration info (`registration-info`)

No authentication needed. Returns whether registration is open, which fields are required or need verification, and the user agreement.

Authentication: No auth · Docs: https://www-demo.kuxitong.com/en/docs/api/registration-info

### GET `/api/v2/registration-info`

Get registration info · Authentication: No auth

```sh
curl "https://panel.example.com/api/v2/registration-info"
```

Registration info object:

| Field | Type | Required | Description |
|---|---|---|---|
| `agreement_body` | string |  | User agreement body |
| `agreement_title` | string |  | User agreement title |
| `allow_register` | boolean |  | Whether registration is open |
| `fields` | object |  | Requirements for each registration form field (required or not, whether a code is needed) |
| `login_verify_allowed` | LoginVerify[] |  | Second-factor login methods the platform lets users turn on |

## Registrations (`registrations`)

No authentication needed. Submits registration details to create a tenant account; the agreement must be accepted and verified email or phone needs a code.

Authentication: No auth · Docs: https://www-demo.kuxitong.com/en/docs/api/registrations

### POST `/api/v2/registrations`

Register · Authentication: No auth

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `accept_agreement` | boolean | Yes | Must be true |
| `email` | string |  | Email |
| `email_code` | string |  | Code sent to the email (POST /verify-codes, purpose=bind); required when the email must be verified |
| `password` | string | Yes | Login password, 8 to 128 characters |
| `phone` | string |  | Phone number |
| `phone_code` | string |  | Code sent to the phone number (POST /verify-codes, purpose=bind); required when the phone number must be verified |
| `username` | string |  | Required if registration info demands it, otherwise generated automatically. Must not look like a phone number (all digits) |

```sh
curl -X POST "https://panel.example.com/api/v2/registrations" \
  -H "Content-Type: application/json" \
  -d '{"accept_agreement":true,"email":"alice@example.com","email_code":"123456","password":"${PASSWORD}","username":"alice"}'
```

Registrations object:

| Field | Type | Required | Description |
|---|---|---|---|
| `accept_agreement` | boolean | Yes | Must be true |
| `balance_cents` | integer |  | Account balance in cents, same as balance_cents of GET /users/{user_id}/account (0 when there is no account). Only present in the GET /users list, not in single-user responses (read-only) |
| `certified` | boolean |  | Whether real-name verification has passed |
| `client_ip` | string |  | Source IP of this request, derived the same way as the login IP allowlist check (what the allowlist compares is what is shown). Only present in GET /me and PUT /me (read-only) |
| `created_at` | string |  | Created at (read-only) |
| `delegated_session` | boolean |  | Whether the current session was issued by superadmin sign-in or switch-to-user (if so, password, email, phone, login allowlist, login verification and real-name are read-only). Only present in GET /me and PUT /me (read-only) |
| `email` | string |  | Email |
| `email_code` | string |  | Code sent to the email (POST /verify-codes, purpose=bind); required when the email must be verified |
| `enabled` | boolean |  | Whether enabled |
| `id` | integer |  | Record ID (read-only) |
| `login_ip_allow` | string[] |  | Login IP allowlist (IPs or CIDRs); empty means no restriction |
| `login_verify` | LoginVerify |  | Second-factor login: none means not required; email / sms also require an email/SMS code at login (the platform must allow it and the address must be bound) Values: ["none","email","sms"] |
| `password` | string | Yes | Login password, 8 to 128 characters |
| `phone` | string |  | Phone number |
| `phone_code` | string |  | Code sent to the phone number (POST /verify-codes, purpose=bind); required when the phone number must be verified |
| `role` | string |  | Account role: admin or tenant Values: ["admin","tenant"] |
| `totp_enabled` | boolean |  | Whether TOTP is enabled |
| `totp_recovery_left` | integer |  | Number of unused TOTP recovery codes. Only present in GET /me and PUT /me when TOTP is enabled (read-only) |
| `updated_at` | string |  | Updated at (read-only) |
| `username` | string |  | Required if registration info demands it, otherwise generated automatically. Must not look like a phone number (all digits) |

## Verification codes (`verify-codes`)

No authentication needed. Sends email or SMS codes for binding at registration, password reset and second-factor login; SMS needs a captcha.

Authentication: No auth · Docs: https://www-demo.kuxitong.com/en/docs/api/verify-codes

### POST `/api/v2/verify-codes`

Send verification code · Authentication: No auth

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `account` | string |  | Login name (required for login) |
| `captcha_code` | string |  | Characters shown in the captcha image (required when sending SMS) |
| `captcha_id` | string |  | Captcha id (required when sending SMS) |
| `channel` | string | Yes | Delivery channel: email or sms Values: ["email","sms"] |
| `purpose` | string | Yes | Purpose: bind (registration or rebinding), reset_password, or login (second factor) Values: ["bind","reset_password","login"] |
| `target` | string |  | Email or phone number (required for bind and reset_password) |

```sh
curl -X POST "https://panel.example.com/api/v2/verify-codes" \
  -H "Content-Type: application/json" \
  -d '{"channel":"email","purpose":"bind","target":"alice@example.com"}'
```

Verification codes object:

| Field | Type | Required | Description |
|---|---|---|---|
| `account` | string |  | Login name (required for login) |
| `captcha_code` | string |  | Characters shown in the captcha image (required when sending SMS) |
| `captcha_id` | string |  | Captcha id (required when sending SMS) |
| `channel` | string | Yes | Delivery channel: email or sms Values: ["email","sms"] |
| `expires_in` | integer |  | Validity in seconds |
| `purpose` | string | Yes | Purpose: bind (registration or rebinding), reset_password, or login (second factor) Values: ["bind","reset_password","login"] |
| `resend_in` | integer |  | Seconds before the code can be resent |
| `target` | string |  | Email or phone number (required for bind and reset_password) |
| `to` | string |  | Masked recipient address |

## Login policy (`login-policy`)

No authentication needed. Tells whether an account needs an email or SMS code to log in, with the masked destination.

Authentication: No auth · Docs: https://www-demo.kuxitong.com/en/docs/api/login-policy

### GET `/api/v2/login-policy`

Get login policy · Authentication: No auth

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `account` | string | Yes | Login name (username, email or phone number) |

```sh
curl "https://panel.example.com/api/v2/login-policy"
```

Login policy object:

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | string |  | Masked email |
| `login_verify` | LoginVerify |  | Second-factor login: none means not required; email / sms also require an email/SMS code at login (the platform must allow it and the address must be bound) Values: ["none","email","sms"] |
| `phone` | string |  | Masked phone number |

## Identity verification channels (`certify-channels`)

Admin only: merchant credentials of Alipay identity verification and Tencent Cloud FaceID (WeChat face verification); the channel in use is chosen by certify_provider in account settings.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/certify-channels

### GET `/api/v2/certify-channels`

Get identity verification channels · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/certify-channels" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/certify-channels`

Update identity verification channels · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `alipay` | CertifyAlipay |  | Merchant parameters of Alipay identity verification (alipay.user.certify.open.*). |
| `wechat` | CertifyWechat |  | Parameters of Tencent Cloud FaceID (WeChat face verification, faceid DetectAuth). |

```sh
curl -X PUT "https://panel.example.com/api/v2/certify-channels" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Identity verification channels object:

| Field | Type | Required | Description |
|---|---|---|---|
| `alipay` | CertifyAlipay |  | Merchant parameters of Alipay identity verification (alipay.user.certify.open.*). |
| `wechat` | CertifyWechat |  | Parameters of Tencent Cloud FaceID (WeChat face verification, faceid DetectAuth). |

## Account (`account`)

The current users own balance and freeze status; admins manage other users accounts through the user account endpoints.

Authentication: User · Docs: https://www-demo.kuxitong.com/en/docs/api/account

### GET `/api/v2/account`

Get my account · Authentication: User

```sh
curl "https://panel.example.com/api/v2/account" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Account object:

| Field | Type | Required | Description |
|---|---|---|---|
| `balance_cents` | integer |  | Account balance in cents. |
| `freeze_reason` | string |  | Freeze reason; empty if not frozen. |
| `frozen` | boolean |  | Whether the account is frozen; deductions are rejected while frozen. |
| `updated_at` | string |  | Updated at |
| `user_id` | integer |  | User ID |

## Orders (`orders`)

Orders created by recharges, deductions, online payments and package purchases; tenants see only their own, admins can see all tenants, and only the remark can be edited.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/orders

### GET `/api/v2/orders`

List orders · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `type` | string |  | Filter by order type (recharge/deduct/payment/package, etc.). |
| `status` | string |  | Filter by status. Values: ["unpaid","paid"] |
| `start` | string |  | Order time from (inclusive), RFC 3339. |
| `end` | string |  | Order time until (exclusive), RFC 3339. |

```sh
curl "https://panel.example.com/api/v2/orders" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/orders/stats`

Order amount statistics · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `type` | string |  | Filter by order type (recharge/deduct/payment/package, etc.). |
| `status` | string |  | Filter by status. Values: ["unpaid","paid"] |
| `start` | string |  | Order time from (inclusive), RFC 3339. |
| `end` | string |  | Order time until (exclusive), RFC 3339. |
| `group_by` | string |  | Aggregation granularity, default day. Values: ["day","month","year"] |
| `utc_offset_minutes` | integer |  | Deprecated and kept only for compatibility with old callers (the range is still validated). Days, months and years are always split in the platform time zone (Asia/Shanghai), the same day boundary as today's traffic; this parameter no longer shifts them. |

```sh
curl "https://panel.example.com/api/v2/orders/stats" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/orders/{id}`

Get order · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/orders/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/orders/{id}`

Update order remark · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `des` | string | Yes | Description |

```sh
curl -X PUT "https://panel.example.com/api/v2/orders/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"des":"线下转账已核对"}'
```

Orders object:

| Field | Type | Required | Description |
|---|---|---|---|
| `amount_cents` | integer |  | Amount in cents; negative for deductions. |
| `archived` | boolean |  | Moved into the archive table by the archiving task (read-only). |
| `created_at` | string |  | Created at (read-only) |
| `data` | object |  | Extra data (such as package purchase details). |
| `des` | string |  | Description |
| `group_by` | string |  | Aggregation granularity: day, month or year. Values: ["day","month","year"] |
| `id` | integer |  | Record ID (read-only) |
| `items` | OrderStatsItem[] |  | Items on this page |
| `ledger_id` | integer |  | ID of the linked ledger entry; null if none. |
| `mch_order_no` | string |  | Merchant order number (the gateway-side order number); null for non-online payments. |
| `paid_at` | string |  | Payment time; null if unpaid. |
| `pay_type` | string |  | admin/balance/alipay/wxpay, etc.; empty = unpaid. |
| `status` | string |  | Order status: unpaid or paid. Values: ["unpaid","paid"] |
| `tenant_id` | integer |  | Owner tenant (user) ID (read-only) |
| `transaction_id` | string |  | Gateway-side transaction ID; empty if unpaid. |
| `type` | string |  | recharge (admin recharge) / deduct (admin deduction) / payment (online payment) / package (package purchase), etc. |
| `updated_at` | string |  | Updated at (read-only) |

## Ledger entries (`ledger-entries`)

Every change to an account balance, each with a balance snapshot taken after posting.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/ledger-entries

### GET `/api/v2/ledger-entries`

List ledger entries · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `kind` | string |  | Filter by ledger entry kind. |
| `start` | string |  | Entry time from (inclusive), RFC 3339. |
| `end` | string |  | Entry time until (exclusive), RFC 3339. |

```sh
curl "https://panel.example.com/api/v2/ledger-entries" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Ledger entries object:

| Field | Type | Required | Description |
|---|---|---|---|
| `amount_cents` | integer |  | Signed: positive for credit, negative for debit. |
| `balance_after_cents` | integer |  | Balance snapshot after this entry. |
| `created_at` | string |  | Created at |
| `des` | string |  | Description |
| `id` | integer |  | Record ID |
| `kind` | string |  | Entry kind, such as recharge or deduction. |
| `order_id` | integer |  | Related order ID; empty if none. |
| `tenant_id` | integer |  | Owner tenant (user) ID |

## Online payments (`payments`)

Start an online recharge: creates an unpaid order, pre-orders it at the payment gateway and returns the payment URL.

Authentication: User · Docs: https://www-demo.kuxitong.com/en/docs/api/payments

### POST `/api/v2/payments/{provider}/orders`

Create online payment · Authentication: User

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `amount_cents` | integer | Yes | Recharge amount in cents. |

```sh
curl -X POST "https://panel.example.com/api/v2/payments/{provider}/orders" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Online payments object:

| Field | Type | Required | Description |
|---|---|---|---|
| `amount_cents` | integer | Yes | Recharge amount in cents. |
| `gateway` | string |  | The payment gateway the order was placed with. Values: ["alipay","wxpay","codepay","epay"] |
| `mch_order_no` | string |  | Merchant order number (the gateway-side order number). |
| `order_id` | integer |  | ID of the unpaid order. |
| `pay_url` | string |  | Payment address: a redirect URL, or a QR code string for weixin:// and Alipay face-to-face payment. |

## Recharge methods (`payment-methods`)

The currently enabled online payment methods, the default method and transfer instructions, used to render the recharge page.

Authentication: User · Docs: https://www-demo.kuxitong.com/en/docs/api/payment-methods

### GET `/api/v2/payment-methods`

Get available recharge methods · Authentication: User

```sh
curl "https://panel.example.com/api/v2/payment-methods" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Recharge methods object:

| Field | Type | Required | Description |
|---|---|---|---|
| `default_method` | string |  | Default selected payment method; empty means none. |
| `methods` | string[] |  | Enabled online payment methods. |
| `transfer` | PaymentTransfer |  | Offline transfer settings. |

## Payment channels (`payment-channels`)

Admin settings for which gateway each payment method uses and each gateways merchant parameters; secrets are write-only.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/payment-channels

### GET `/api/v2/payment-channels`

Get payment channel settings · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/payment-channels" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/payment-channels`

Update payment channels · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `alipay` | AlipayGateway |  | Merchant parameters of the Alipay gateway. |
| `codepay` | CodepayGateway |  | Merchant parameters of the Codepay gateway. |
| `default_method` | string |  | The method selected by default on the recharge page; transfer = bank transfer (transfer must be enabled). Values: ["","alipay","wxpay","transfer"] |
| `epay` | EpayGateway |  | Merchant parameters of the Epay gateway. |
| `methods` | PaymentMethodSet |  | Enable switch and gateway for each online payment method. |
| `notify_base` | string |  | Root URL of the panel reachable by the gateways (for example https://panel.example.com); callback URLs are built from it, and empty = orders cannot be placed. |
| `return_url` | string |  | Page to return to after payment (used by Alipay desktop website payment and Epay); empty = no redirect. |
| `transfer` | PaymentTransfer |  | Offline transfer settings. |
| `wxpay` | WxpayGateway |  | Merchant parameters of the WeChat Pay gateway. |

```sh
curl -X PUT "https://panel.example.com/api/v2/payment-channels" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Payment channels object:

| Field | Type | Required | Description |
|---|---|---|---|
| `alipay` | AlipayGateway |  | Merchant parameters of the Alipay gateway. |
| `codepay` | CodepayGateway |  | Merchant parameters of the Codepay gateway. |
| `default_method` | string |  | The method selected by default on the recharge page; transfer = bank transfer (transfer must be enabled). Values: ["","alipay","wxpay","transfer"] |
| `epay` | EpayGateway |  | Merchant parameters of the Epay gateway. |
| `methods` | PaymentMethodSet |  | Enable switch and gateway for each online payment method. |
| `notify_base` | string |  | Root URL of the panel reachable by the gateways (for example https://panel.example.com); callback URLs are built from it, and empty = orders cannot be placed. |
| `return_url` | string |  | Page to return to after payment (used by Alipay desktop website payment and Epay); empty = no redirect. |
| `transfer` | PaymentTransfer |  | Offline transfer settings. |
| `wxpay` | WxpayGateway |  | Merchant parameters of the WeChat Pay gateway. |

## Payment notifications (`payment-notifications`)

Result notification endpoint called by payment gateways; no authentication, signature verified per gateway rules, and repeated notifications are credited only once.

Authentication: No auth · Docs: https://www-demo.kuxitong.com/en/docs/api/payment-notifications

### GET `/api/v2/payment-notifications/{gateway}`

Payment notification callback (GET) · Authentication: No auth

```sh
curl "https://panel.example.com/api/v2/payment-notifications/{gateway}"
```

### POST `/api/v2/payment-notifications/{gateway}`

Payment notification callback · Authentication: No auth

```sh
curl -X POST "https://panel.example.com/api/v2/payment-notifications/{gateway}"
```

## DNS providers (`dns-providers`)

Admin only. Stores DNS provider accounts (such as Alibaba Cloud or Cloudflare); credentials are verified online before saving and only the last 4 characters are ever returned.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/dns-providers

### GET `/api/v2/dns-providers`

List DNS providers · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `type` | string |  | Only list providers of this type (such as aliyun). |

```sh
curl "https://panel.example.com/api/v2/dns-providers" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/dns-providers`

Create DNS provider · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `auth_id` | string |  | Credential ID (Alibaba Cloud AccessKey ID, DNSPod Token ID or Tencent Cloud SecretId, Cloudflare email; empty when a Cloudflare API Token is used). |
| `auth_key` | string |  | Credential (AccessKey Secret / Token / SecretKey / API Key); required on create, omitted or empty on update = keep the existing value. Stored encrypted. |
| `expected_updated_at` | string |  | Optimistic lock (used on update) - the updated_at of the resource as the caller read it. If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check; ignored on create. |
| `gateway` | string |  | Self-hosted API gateway address (used by dnsdun only). |
| `name` | string | Yes | Name |
| `remark` | string |  | Remark |
| `ttl` | integer |  | Default TTL (seconds) of records under this account. |
| `type` | string | Yes | Provider type: aliyun, cloudflare, dnspod_cn, dnspod_com, dns_la, dnsdotcom, dnsdun. |

```sh
curl -X POST "https://panel.example.com/api/v2/dns-providers" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"auth_id":"LTAI-example","auth_key":"${DNS_AUTH_KEY}","name":"main-aliyun","ttl":120,"type":"aliyun"}'
```

### GET `/api/v2/dns-providers/{id}`

Get DNS provider · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/dns-providers/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/dns-providers/{id}`

Update DNS provider · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `auth_id` | string |  | Credential ID (Alibaba Cloud AccessKey ID, DNSPod Token ID or Tencent Cloud SecretId, Cloudflare email; empty when a Cloudflare API Token is used). |
| `auth_key` | string |  | Credential (AccessKey Secret / Token / SecretKey / API Key); required on create, omitted or empty on update = keep the existing value. Stored encrypted. |
| `expected_updated_at` | string |  | Optimistic lock (used on update) - the updated_at of the resource as the caller read it. If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check; ignored on create. |
| `gateway` | string |  | Self-hosted API gateway address (used by dnsdun only). |
| `name` | string | Yes | Name |
| `remark` | string |  | Remark |
| `ttl` | integer |  | Default TTL (seconds) of records under this account. |
| `type` | string | Yes | Provider type: aliyun, cloudflare, dnspod_cn, dnspod_com, dns_la, dnsdotcom, dnsdun. |

```sh
curl -X PUT "https://panel.example.com/api/v2/dns-providers/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"auth_id":"LTAI-example","auth_key":"${DNS_AUTH_KEY}","name":"main-aliyun","ttl":120,"type":"aliyun"}'
```

### DELETE `/api/v2/dns-providers/{id}`

Delete DNS provider · Authentication: Admin only

```sh
curl -X DELETE "https://panel.example.com/api/v2/dns-providers/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/dns-providers/{id}/lines`

Fetch DNS provider lines · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `domain` | string | Yes | Root domain, such as example.com. |

```sh
curl "https://panel.example.com/api/v2/dns-providers/{id}/lines" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

DNS providers object:

| Field | Type | Required | Description |
|---|---|---|---|
| `auth_id` | string |  | Credential ID (Alibaba Cloud AccessKey ID, DNSPod Token ID or Tencent Cloud SecretId, Cloudflare email; empty when a Cloudflare API Token is used). |
| `auth_key` | string |  | Credential (AccessKey Secret / Token / SecretKey / API Key); required on create, omitted or empty on update = keep the existing value. Stored encrypted. |
| `auth_key_hint` | string |  | Last 4 characters of the credential (padded with * in front); the original is never returned. |
| `created_at` | string |  | Created at (read-only) |
| `default_line` | string |  | ID of the default line of this provider. |
| `expected_updated_at` | string |  | Optimistic lock (used on update) - the updated_at of the resource as the caller read it. If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check; ignored on create. |
| `gateway` | string |  | Self-hosted API gateway address (used by dnsdun only). |
| `id` | integer |  | Record ID (read-only) |
| `items` | DnsLine[] |  | Items on this page |
| `lines` | DnsLine[] |  | Cached line tree from the last fetch (refreshed by GET /dns-providers/{id}/lines); an empty array if never fetched. |
| `name` | string |  | Name |
| `remark` | string |  | Remark |
| `ttl` | integer |  | Default TTL (seconds) of records under this account. |
| `type` | string |  | Provider type: aliyun, cloudflare, dnspod_cn, dnspod_com, dns_la, dnsdotcom, dnsdun. |
| `updated_at` | string |  | Updated at (read-only) |

## CNAME access domains (`cname-domains`)

Admin only. Root domains used to build CNAME access names, each owned by a DNS provider account; supports migrating everything to a new root domain and checking whether customers have switched their records.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/cname-domains

### GET `/api/v2/cname-domains`

List CNAME access domains · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `domain` | string |  | Only list root domains containing this text (partial match, case-insensitive; surrounding spaces and a trailing dot are ignored). |
| `type` | string |  | Only list domains belonging to this provider type (such as aliyun). |
| `dns_provider_id` | integer |  | Only list domains belonging to this account. |
| `status` | CnameDomainStatus |  | Only list domains in this status. Values: ["active","retiring"] |

```sh
curl "https://panel.example.com/api/v2/cname-domains" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/cname-domains`

Create CNAME access domain · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `dns_provider_id` | integer | Yes | ID of the provider account hosting this root domain. |
| `domain` | string | Yes | Lowercase root domain, such as cdn.example.com. |
| `expected_updated_at` | string |  | Optimistic lock (used on update) - the updated_at of the resource as the caller read it. If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check; ignored on create. |
| `expire_at` | string |  | Expiry date entered by the admin, YYYY-MM-DD; the admin is reminded 30, 7 and 1 days before expiry and once after it. |
| `remark` | string |  | Remark |
| `status` | CnameDomainStatus |  | active: in use; retiring: being retired (migrated away, no longer offered when choosing a package domain). Values: ["active","retiring"] |

```sh
curl -X POST "https://panel.example.com/api/v2/cname-domains" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"dns_provider_id":1,"domain":"cdn.example.com","expire_at":"2027-01-31","remark":"主接入域名"}'
```

### PUT `/api/v2/cname-domains/status`

Batch set CNAME access domain status · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `ids` | integer[] | Yes | IDs of the access domains to update. |
| `status` | CnameDomainStatus | Yes | active: in use; retiring: being retired (migrated away, no longer offered when choosing a package domain). Values: ["active","retiring"] |

```sh
curl -X PUT "https://panel.example.com/api/v2/cname-domains/status" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ids":[1],"status":"retiring"}'
```

### GET `/api/v2/cname-domains/{id}`

Get CNAME access domain · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/cname-domains/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/cname-domains/{id}`

Update CNAME access domain · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `dns_provider_id` | integer | Yes | ID of the provider account hosting this root domain. |
| `domain` | string | Yes | Lowercase root domain, such as cdn.example.com. |
| `expected_updated_at` | string |  | Optimistic lock (used on update) - the updated_at of the resource as the caller read it. If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check; ignored on create. |
| `expire_at` | string |  | Expiry date entered by the admin, YYYY-MM-DD; the admin is reminded 30, 7 and 1 days before expiry and once after it. |
| `remark` | string |  | Remark |
| `status` | CnameDomainStatus |  | active: in use; retiring: being retired (migrated away, no longer offered when choosing a package domain). Values: ["active","retiring"] |

```sh
curl -X PUT "https://panel.example.com/api/v2/cname-domains/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"dns_provider_id":1,"domain":"cdn.example.com","expire_at":"2027-01-31","remark":"主接入域名"}'
```

### DELETE `/api/v2/cname-domains/{id}`

Delete CNAME access domain · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `local_only` | boolean |  | When true, delete only the local record and do not clean up provider-side records under this root domain. |

```sh
curl -X DELETE "https://panel.example.com/api/v2/cname-domains/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/cname-domains/{id}/migrate`

Migrate CNAME access domain · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `notify` | boolean |  | Whether to send in-app messages to the affected customers. Default: true |
| `retire` | boolean |  | Whether to set the old domain to retiring. Default: true |
| `target_id` | integer | Yes | ID of the target root domain. |

```sh
curl -X POST "https://panel.example.com/api/v2/cname-domains/{id}/migrate" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/cname-domains/{id}/refs`

Get CNAME access domain references · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/cname-domains/{id}/refs" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/cname-domains/{id}/switch-check`

Check switch status after migration · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `state` | CnameSwitchState |  | Only list rows in this check state. Values: ["changed","unchanged","not_cname","failed","pending"] |
| `q` | string |  | Fuzzy match on customer name, site domain or actual resolution. |

```sh
curl "https://panel.example.com/api/v2/cname-domains/{id}/switch-check" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/cname-domains/{id}/switch-check/export`

Export switch check table · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `state` | CnameSwitchState |  | Only list rows in this check state. Values: ["changed","unchanged","not_cname","failed","pending"] |
| `q` | string |  | Fuzzy match on customer name, site domain or actual resolution. |

```sh
curl "https://panel.example.com/api/v2/cname-domains/{id}/switch-check/export" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/cname-domains/{id}/switch-check/remind`

Remind customers who have not switched · Authentication: Admin only

```sh
curl -X POST "https://panel.example.com/api/v2/cname-domains/{id}/switch-check/remind" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/cname-domains/{id}/switch-check/run`

Run switch check now · Authentication: Admin only

```sh
curl -X POST "https://panel.example.com/api/v2/cname-domains/{id}/switch-check/run" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

CNAME access domains object:

| Field | Type | Required | Description |
|---|---|---|---|
| `actual` | string |  | What it actually resolves to (CNAME target or IP); on lookup failure, the reason (NXDOMAIN, Timeout, ...). |
| `changed` | integer |  | Number of rows now switched. |
| `check_rows` | integer |  | Number of check rows created (one per site domain). |
| `checked` | integer |  | Number of rows rechecked. |
| `checked_at` | string |  | Time of the last resolution check. |
| `created_at` | string |  | Created at (read-only) |
| `dns_provider_id` | integer |  | ID of the provider account hosting this root domain. |
| `domain` | string |  | Lowercase root domain, such as cdn.example.com. |
| `example_hostname` | string |  | An existing access name (without the root domain), used by the wizard to build old and new access address examples. |
| `expected_updated_at` | string |  | Optimistic lock (used on update) - the updated_at of the resource as the caller read it. If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check; ignored on create. |
| `expire_at` | string |  | Expiry date entered by the admin, YYYY-MM-DD; the admin is reminded 30, 7 and 1 days before expiry and once after it. |
| `failed` | integer |  | Number of rows whose lookup failed. |
| `from` | string |  | Source root domain. |
| `id` | integer |  | Record ID (read-only) |
| `ids` | integer[] | Yes | IDs of the access domains to update. |
| `messaged` | integer |  | Number of customers who received an in-app message. |
| `new_target` | string |  | CNAME target after the switch. |
| `not_cname` | integer |  | Number of rows not using a CNAME. |
| `notified_at` | string |  | Time the customer was notified. |
| `notify` | boolean |  | Whether to send in-app messages to the affected customers. Default: true |
| `old_target` | string |  | CNAME target before the switch. |
| `package` | integer |  | Number of package access names switched. |
| `package_list` | string[] |  | First 20 purchased packages referencing this root domain. |
| `provider_name` | string |  | Name of the owning account. |
| `provider_type` | string |  | Provider type of the owning account. |
| `remark` | string |  | Remark |
| `retire` | boolean |  | Whether to set the old domain to retiring. Default: true |
| `site` | integer |  | Number of site access names switched. |
| `site_id` | integer |  | Site ID |
| `site_list` | string[] |  | List of site access names switched. |
| `skipped` | object[] |  | Skipped access domains with reasons. |
| `state` | CnameSwitchState |  | changed: already switched; unchanged: not switched yet; not_cname: not a CNAME access (points elsewhere or only has an A record); failed: lookup failed; pending: waiting to be checked. Values: ["changed","unchanged","not_cname","failed","pending"] |
| `status` | CnameDomainStatus |  | active: in use; retiring: being retired (migrated away, no longer offered when choosing a package domain). Values: ["active","retiring"] |
| `stream` | integer |  | Number of TCP/UDP forwarding access names switched. |
| `stream_list` | string[] |  | First 20 TCP/UDP forwardings (id:<id> <access name>). |
| `switched_at` | string |  | Time of the migration; empty if not migrated. |
| `switched_to` | integer |  | ID of the root domain it was migrated to. |
| `switched_to_domain` | string |  | Name of the root domain it was migrated to; empty if not migrated. |
| `target_id` | integer | Yes | ID of the target root domain. |
| `tenant_id` | integer |  | Owner tenant (user) ID |
| `to` | string |  | Target root domain. |
| `unchanged` | integer |  | Number of rows still not switched. |
| `updated` | integer |  | Number of domains actually updated. |
| `updated_at` | string |  | Updated at (read-only) |
| `user_list` | string[] |  | List of affected customers. |
| `user_name` | string |  | Customer (tenant) name; empty if the tenant was deleted. |
| `users` | integer |  | Number of affected customers (distinct tenants owning the sites). |

## DNS sync (`dns-sync`)

Admin only. Makes the scheduler reconcile all DNS records against the desired state right away (the Record repair button in the panel).

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/dns-sync

### POST `/api/v2/dns-sync`

Sync DNS records now · Authentication: Admin only

```sh
curl -X POST "https://panel.example.com/api/v2/dns-sync" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

## Regions (`regions`)

Geographic regions used to categorize nodes and node groups.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/regions

### GET `/api/v2/regions`

List regions · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |

```sh
curl "https://panel.example.com/api/v2/regions" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/regions`

Create region · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `des` | string |  | Description |
| `name` | string | Yes | Name |

```sh
curl -X POST "https://panel.example.com/api/v2/regions" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/regions/{id}`

Get region · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/regions/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/regions/{id}`

Update region · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `des` | string |  | Description |
| `name` | string |  | Name |

```sh
curl -X PUT "https://panel.example.com/api/v2/regions/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"des":"east"}'
```

### DELETE `/api/v2/regions/{id}`

Delete region · Authentication: Admin only

```sh
curl -X DELETE "https://panel.example.com/api/v2/regions/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Regions object:

| Field | Type | Required | Description |
|---|---|---|---|
| `created_at` | string |  | Created at (read-only) |
| `des` | string |  | Description |
| `id` | integer |  | Record ID (read-only) |
| `name` | string |  | Name |
| `updated_at` | string |  | Updated at (read-only) |

## Nodes (`nodes`)

Approved edge nodes and sub-IPs; primary nodes self-enroll with an enrollment token, while sub-IPs are added manually by an admin.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/nodes

### GET `/api/v2/nodes`

List nodes · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |

```sh
curl "https://panel.example.com/api/v2/nodes" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/nodes`

Create node · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `area` | string |  | Area (display only, never sent to nodes); empty = not set. Sub-IPs take the primary node's value. |
| `bandwidth_mbps` | integer |  | Node bandwidth in Mbps (display only, unrelated to monitor.bw_limit_mbps); 0 = not set. Sub-IPs take the primary node's value. |
| `des` | string |  | Description |
| `enabled` | boolean |  | Whether enabled |
| `ip` | string |  | Node IP address (IPv4 or IPv6). |
| `monitor` | NodeMonitor |  | Node monitor probe settings (the control plane probes this IP at the interval configured in monitor.node). |
| `name` | string | Yes | Name |
| `parent_id` | integer |  | Primary node that owns this sub-IP; null = primary node. |
| `region_id` | integer |  | Region ID |
| `service_expire` | string |  | Server service expiry date YYYY-MM-DD (used for the home-page expiry reminder; never auto-disables); null = not set, no reminder. Sub-IPs take the primary node's value. |
| `service_start` | string |  | Server service start date YYYY-MM-DD (display only); null = not set. Sub-IPs take the primary node's value. |

```sh
curl -X POST "https://panel.example.com/api/v2/nodes" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ip":"192.0.2.20","name":"edge-1-ip2","parent_id":1}'
```

### GET `/api/v2/nodes/{id}`

Get node · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `capacity` | boolean |  | When true, include capacity (primary nodes only; null when no system info is available). |

```sh
curl "https://panel.example.com/api/v2/nodes/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/nodes/{id}`

Update node · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `area` | string |  | Area (display only, never sent to nodes); empty = not set. Sub-IPs take the primary node's value. |
| `bandwidth_mbps` | integer |  | Node bandwidth in Mbps (display only, unrelated to monitor.bw_limit_mbps); 0 = not set. Sub-IPs take the primary node's value. |
| `des` | string |  | Description |
| `enabled` | boolean |  | Whether enabled |
| `ip` | string |  | Node IP address (IPv4 or IPv6). |
| `monitor` | NodeMonitor |  | Node monitor probe settings (the control plane probes this IP at the interval configured in monitor.node). |
| `name` | string |  | Name |
| `parent_id` | integer |  | Primary node that owns this sub-IP; null = primary node. |
| `region_id` | integer |  | Region ID |
| `service_expire` | string |  | Server service expiry date YYYY-MM-DD (used for the home-page expiry reminder; never auto-disables); null = not set, no reminder. Sub-IPs take the primary node's value. |
| `service_start` | string |  | Server service start date YYYY-MM-DD (display only); null = not set. Sub-IPs take the primary node's value. |

```sh
curl -X PUT "https://panel.example.com/api/v2/nodes/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"bandwidth_mbps":30}'
```

### DELETE `/api/v2/nodes/{id}`

Delete node · Authentication: Admin only

```sh
curl -X DELETE "https://panel.example.com/api/v2/nodes/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/nodes/{id}/config-readmit`

Re-push the config a node rolled back · Authentication: Admin only

```sh
curl -X POST "https://panel.example.com/api/v2/nodes/{id}/config-readmit" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/nodes/{id}/local-addrs`

Node NIC addresses · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/nodes/{id}/local-addrs" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/nodes/{id}/to-pending`

Move node back to pending · Authentication: Admin only

```sh
curl -X POST "https://panel.example.com/api/v2/nodes/{id}/to-pending" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Nodes object:

| Field | Type | Required | Description |
|---|---|---|---|
| `area` | string |  | Area (display only, never sent to nodes); empty = not set. Sub-IPs take the primary node's value. |
| `bandwidth_mbps` | integer |  | Node bandwidth in Mbps (display only, unrelated to monitor.bw_limit_mbps); 0 = not set. Sub-IPs take the primary node's value. |
| `capacity` | NodeCapacity |  | Node memory capacity (read-only, only returned by GET /nodes/{id}?capacity=true): per-site resident cost = locations × 28.1KB + HTTPS ratio × 25.5KB, budget = memory MiB × 0.25 / 2 − 28MiB, limit = budget / per-site cost. null = no system info for the node or memory total missing. (read-only) |
| `cert_expiry` | string |  | Expiry time of the most recently issued certificate; null = no certificate issued yet (or a sub-IP). |
| `created_at` | string |  | Created at (read-only) |
| `des` | string |  | Description |
| `disable_by` | string |  | Disable source (manual = admin; monitor = auto-disabled by node monitoring and re-enabled once probes recover; bandwidth = auto-disabled when outbound bandwidth exceeds monitor.bw_limit_mbps and re-enabled when it drops back); empty when enabled. |
| `enabled` | boolean |  | Whether enabled |
| `enrolled_at` | string |  | Time the node enrolled; null for a manually added sub-IP. |
| `hints` | string[] |  | Hints (e.g. NIC addresses could not be obtained). |
| `hostname` | string |  | Node hostname, reported at enrollment. |
| `id` | integer |  | Record ID (read-only) |
| `ip` | string |  | Node IP address (IPv4 or IPv6). |
| `monitor` | NodeMonitor |  | Node monitor probe settings (the control plane probes this IP at the interval configured in monitor.node). |
| `name` | string |  | Name |
| `ops` | NodeOps |  | Node operations status (read-only, from heartbeats): online, whether the config lags behind the latest published version, and clock sync. Sub-IPs follow their primary node. (read-only) |
| `parent_id` | integer |  | Primary node that owns this sub-IP; null = primary node. |
| `region_id` | integer |  | Region ID |
| `role` | string |  | Node role: edge for an edge node, l2 for a second-tier cache node. Values: ["edge","l2"] |
| `service_expire` | string |  | Server service expiry date YYYY-MM-DD (used for the home-page expiry reminder; never auto-disables); null = not set, no reminder. Sub-IPs take the primary node's value. |
| `service_start` | string |  | Server service start date YYYY-MM-DD (display only); null = not set. Sub-IPs take the primary node's value. |
| `status` | string |  | ok = result available; pending = command queued, fetch again later; unknown = node offline. Values: ["ok","pending","unknown"] |
| `updated_at` | string |  | Updated at (read-only) |
| `v4` | string[] |  | IPv4 addresses on the node's NICs. |
| `v6` | string[] |  | IPv6 addresses on the node's NICs. |

## Pending nodes (`pending-nodes`)

Nodes that have enrolled with a token and await admin approval; once approved they become regular nodes.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/pending-nodes

### GET `/api/v2/pending-nodes`

List pending nodes · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |

```sh
curl "https://panel.example.com/api/v2/pending-nodes" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/pending-nodes/{id}`

Delete pending node · Authentication: Admin only

```sh
curl -X DELETE "https://panel.example.com/api/v2/pending-nodes/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/pending-nodes/{id}/approve`

Approve pending node · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `des` | string |  | Description |
| `ip` | string |  | Source address of the registration connection. |
| `name` | string |  | Name |
| `region_id` | integer |  | Region ID |
| `replace_node_id` | integer |  | Replace an existing primary node (new machine takes over the old one): the new node inherits the old node's lines, IP pool IP, region (when region_id is omitted), monitor settings, service period and area/bandwidth; the old node is disabled and removed from DNS, all in the approval transaction. The old node's sub IPs move to the new node by default (lines, pool IPs and monitor settings kept); see replace_sub_ips for new addresses or opting out. The old node must be an approved primary node with the same role; 409 ip_in_use when its pool IP is bound to IP sites and the IP changes. |
| `replace_sub_ips` | ReplaceSubIp[] |  | Per-sub-IP handling of the old node when replacing (only with replace_node_id); unlisted sub IPs move to the new node keeping their address. ip switches to a new address (422 conflict if another node uses it); skip=true leaves the sub IP and its lines on the old node, disabled. |

```sh
curl -X POST "https://panel.example.com/api/v2/pending-nodes/{id}/approve" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Pending nodes object:

| Field | Type | Required | Description |
|---|---|---|---|
| `area` | string |  | Area (display only, never sent to nodes); empty = not set. Sub-IPs take the primary node's value. |
| `bandwidth_mbps` | integer |  | Node bandwidth in Mbps (display only, unrelated to monitor.bw_limit_mbps); 0 = not set. Sub-IPs take the primary node's value. |
| `capacity` | NodeCapacity |  | Node memory capacity (read-only, only returned by GET /nodes/{id}?capacity=true): per-site resident cost = locations × 28.1KB + HTTPS ratio × 25.5KB, budget = memory MiB × 0.25 / 2 − 28MiB, limit = budget / per-site cost. null = no system info for the node or memory total missing. (read-only) |
| `cert_expiry` | string |  | Expiry time of the most recently issued certificate; null = no certificate issued yet (or a sub-IP). |
| `created_at` | string |  | Created at (read-only) |
| `des` | string |  | Description |
| `disable_by` | string |  | Disable source (manual = admin; monitor = auto-disabled by node monitoring and re-enabled once probes recover; bandwidth = auto-disabled when outbound bandwidth exceeds monitor.bw_limit_mbps and re-enabled when it drops back); empty when enabled. |
| `enabled` | boolean |  | Whether enabled |
| `enrolled_at` | string |  | Time the node registered. |
| `hostname` | string |  | Host name reported by the node. |
| `id` | integer |  | Record ID (read-only) |
| `ip` | string |  | Source address of the registration connection. |
| `monitor` | NodeMonitor |  | Node monitor probe settings (the control plane probes this IP at the interval configured in monitor.node). |
| `name` | string |  | Name |
| `ops` | NodeOps |  | Node operations status (read-only, from heartbeats): online, whether the config lags behind the latest published version, and clock sync. Sub-IPs follow their primary node. (read-only) |
| `parent_id` | integer |  | Primary node that owns this sub-IP; null = primary node. |
| `region_id` | integer |  | Region ID |
| `replace_node_id` | integer |  | Replace an existing primary node (new machine takes over the old one): the new node inherits the old node's lines, IP pool IP, region (when region_id is omitted), monitor settings, service period and area/bandwidth; the old node is disabled and removed from DNS, all in the approval transaction. The old node's sub IPs move to the new node by default (lines, pool IPs and monitor settings kept); see replace_sub_ips for new addresses or opting out. The old node must be an approved primary node with the same role; 409 ip_in_use when its pool IP is bound to IP sites and the IP changes. |
| `replace_sub_ips` | ReplaceSubIp[] |  | Per-sub-IP handling of the old node when replacing (only with replace_node_id); unlisted sub IPs move to the new node keeping their address. ip switches to a new address (422 conflict if another node uses it); skip=true leaves the sub IP and its lines on the old node, disabled. |
| `role` | string |  | Node role: edge or l2. Values: ["edge","l2"] |
| `service_expire` | string |  | Server service expiry date YYYY-MM-DD (used for the home-page expiry reminder; never auto-disables); null = not set, no reminder. Sub-IPs take the primary node's value. |
| `service_start` | string |  | Server service start date YYYY-MM-DD (display only); null = not set. Sub-IPs take the primary node's value. |
| `updated_at` | string |  | Updated at (read-only) |

## Node enrollment tokens (`node-enroll-tokens`)

Generates a one-time enrollment token and a one-line node install command; the token is returned only once, when generated.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/node-enroll-tokens

### POST `/api/v2/node-enroll-tokens`

Create node enrollment token · Authentication: Admin only

```sh
curl -X POST "https://panel.example.com/api/v2/node-enroll-tokens" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Node enrollment tokens object:

| Field | Type | Required | Description |
|---|---|---|---|
| `command` | string |  | One-line node install command (includes the asset site URL, the control plane URL, the token and the trust root); a short notice if no asset site is configured or the control plane key is not initialized. |
| `expires_at` | string |  | Expires at |
| `token` | string |  | Plain text of the one-time enrollment token; shown only in this response. |

## Node groups (`node-groups`)

Groups lines under one access name; a backup threshold can switch to the backup group automatically when usable lines run low.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/node-groups

### GET `/api/v2/node-groups`

List node groups · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `ids` | string |  | Only list these IDs: comma-separated positive integers, at most 200 after deduplication (same as the page_size limit; more returns 400); an empty value is the same as omitting it. Combined with other filters as an intersection; IDs you cannot see are silently left out. Pagination is unchanged: set page_size to at least the number of IDs to get them in one page |

```sh
curl "https://panel.example.com/api/v2/node-groups" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/node-groups`

Create node group · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `backup_interval_s` | integer |  | Rotation interval in seconds in interval mode. |
| `backup_ip_num` | integer |  | Number of backup IPs in use per rotation in interval mode. |
| `backup_min_online` | integer |  | When usable lines in the group drop below this, access names referencing the group are switched to their own backup groups; 0 = never switch automatically, default 1 (switch when none is usable). |
| `backup_switch_order` | BackupSwitchOrder |  | Rotation order in interval mode: seq picks the least recently used first; rand picks at random. Values: ["seq","rand"] |
| `backup_switch_type` | BackupSwitchType |  | Backup IP switch mode. master_down: bring in as many backup IPs as there are unavailable primary IPs on the same DNS line; gt_online_ip: when online primary IPs are fewer than backup IPs, bring in backups to make up the difference (at most the number of unavailable primary IPs); interval: every IP in the group is a backup and they rotate on a timer (the group must have no primary IPs before switching to interval). Values: ["master_down","gt_online_ip","interval"] |
| `cname_hostname` | string |  | Host record part of the access name (for example www); lowercase letters, digits, hyphens, dot-separated labels; unique across groups (409 cname_hostname_taken); empty on create generates 8 random characters, empty on update keeps the current value. |
| `des` | string |  | Description |
| `name` | string | Yes | Name |
| `region_id` | integer |  | Region ID |

```sh
curl -X POST "https://panel.example.com/api/v2/node-groups" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/node-groups/{id}`

Get node group · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/node-groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/node-groups/{id}`

Update node group · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `backup_interval_s` | integer |  | Rotation interval in seconds in interval mode. |
| `backup_ip_num` | integer |  | Number of backup IPs in use per rotation in interval mode. |
| `backup_min_online` | integer |  | When usable lines in the group drop below this, access names referencing the group are switched to their own backup groups; 0 = never switch automatically, default 1 (switch when none is usable). |
| `backup_switch_order` | BackupSwitchOrder |  | Rotation order in interval mode: seq picks the least recently used first; rand picks at random. Values: ["seq","rand"] |
| `backup_switch_type` | BackupSwitchType |  | Backup IP switch mode. master_down: bring in as many backup IPs as there are unavailable primary IPs on the same DNS line; gt_online_ip: when online primary IPs are fewer than backup IPs, bring in backups to make up the difference (at most the number of unavailable primary IPs); interval: every IP in the group is a backup and they rotate on a timer (the group must have no primary IPs before switching to interval). Values: ["master_down","gt_online_ip","interval"] |
| `cname_hostname` | string |  | Host record part of the access name (for example www); lowercase letters, digits, hyphens, dot-separated labels; unique across groups (409 cname_hostname_taken); empty on create generates 8 random characters, empty on update keeps the current value. |
| `des` | string |  | Description |
| `name` | string |  | Name |
| `region_id` | integer |  | Region ID |

```sh
curl -X PUT "https://panel.example.com/api/v2/node-groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"backup_min_online":2}'
```

### DELETE `/api/v2/node-groups/{id}`

Delete node group · Authentication: Admin only

```sh
curl -X DELETE "https://panel.example.com/api/v2/node-groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Node groups object:

| Field | Type | Required | Description |
|---|---|---|---|
| `backup_active` | boolean |  | Automatically switched to the backup group by monitoring (switches back once usable lines recover to the threshold). (read-only) |
| `backup_interval_s` | integer |  | Rotation interval in seconds in interval mode. |
| `backup_ip_num` | integer |  | Number of backup IPs in use per rotation in interval mode. |
| `backup_min_online` | integer |  | When usable lines in the group drop below this, access names referencing the group are switched to their own backup groups; 0 = never switch automatically, default 1 (switch when none is usable). |
| `backup_switch_order` | BackupSwitchOrder |  | Rotation order in interval mode: seq picks the least recently used first; rand picks at random. Values: ["seq","rand"] |
| `backup_switch_type` | BackupSwitchType |  | Backup IP switch mode. master_down: bring in as many backup IPs as there are unavailable primary IPs on the same DNS line; gt_online_ip: when online primary IPs are fewer than backup IPs, bring in backups to make up the difference (at most the number of unavailable primary IPs); interval: every IP in the group is a backup and they rotate on a timer (the group must have no primary IPs before switching to interval). Values: ["master_down","gt_online_ip","interval"] |
| `backup_switched` | string |  | Time of the most recent automatic switch or switch-back. (read-only) |
| `cname_domains` | string[] |  | Access names (host record.root domain) pointing to this group, in alphabetical order; empty array if none reference it. (read-only) |
| `cname_hostname` | string |  | Host record part of the access name (for example www); lowercase letters, digits, hyphens, dot-separated labels; unique across groups (409 cname_hostname_taken); empty on create generates 8 random characters, empty on update keeps the current value. |
| `created_at` | string |  | Created at (read-only) |
| `des` | string |  | Description |
| `id` | integer |  | Record ID (read-only) |
| `name` | string |  | Name |
| `node_count` | integer |  | Number of distinct node IPs referenced by the lines in this group. (read-only) |
| `region_id` | integer |  | Region ID |
| `site_count` | integer |  | Number of sites whose access name points to this group. (read-only) |
| `stream_count` | integer |  | Number of TCP/UDP forwarding rules whose access name points to this group. (read-only) |
| `updated_at` | string |  | Updated at (read-only) |

## Lines (`lines`)

A resolution record in a node group, showing which DNS line and weight a node appears with; a line must be disabled before it can be deleted.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/lines

### GET `/api/v2/lines`

List lines · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |

```sh
curl "https://panel.example.com/api/v2/lines" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/lines`

Create line · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `dns_line` | string |  | DNS resolution line code (default = default line; carrier/region line codes are mapped by the DNS domain). |
| `enabled` | boolean |  | Whether enabled |
| `force` | boolean |  | Force the write and skip two safeguards (requires a second confirmation): when the line becomes an enabled primary line, its node must have applied its configuration and be at most one release behind (otherwise 422 node_config_behind); disabling the last enabled primary line of the node group or making it a backup (otherwise 422 last_primary_line). |
| `is_backup` | boolean |  | Backup IP; kept out of DNS normally and brought in or taken out automatically according to the node group's backup IP switch mode. |
| `is_backup_default` | boolean |  | Backup default line; when the node group has no default line in use, this line is automatically resolved on the default line instead, and switched back once one is in use again. |
| `node_group_id` | integer | Yes | ID of the node group this line belongs to. |
| `node_id` | integer | Yes | Node ID |
| `weight` | integer |  | Weight, 1 to 100; a larger weight receives a larger share of resolutions. |

```sh
curl -X POST "https://panel.example.com/api/v2/lines" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"dns_line":"default","node_group_id":1,"node_id":1,"weight":1}'
```

### GET `/api/v2/lines/{id}`

Get line · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/lines/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/lines/{id}`

Update line · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `dns_line` | string |  | DNS resolution line code (default = default line; carrier/region line codes are mapped by the DNS domain). |
| `enabled` | boolean |  | Whether enabled |
| `force` | boolean |  | Force the write and skip two safeguards (requires a second confirmation): when the line becomes an enabled primary line, its node must have applied its configuration and be at most one release behind (otherwise 422 node_config_behind); disabling the last enabled primary line of the node group or making it a backup (otherwise 422 last_primary_line). |
| `is_backup` | boolean |  | Backup IP; kept out of DNS normally and brought in or taken out automatically according to the node group's backup IP switch mode. |
| `is_backup_default` | boolean |  | Backup default line; when the node group has no default line in use, this line is automatically resolved on the default line instead, and switched back once one is in use again. |
| `node_group_id` | integer |  | ID of the node group this line belongs to. |
| `node_id` | integer |  | Node ID |
| `weight` | integer |  | Weight, 1 to 100; a larger weight receives a larger share of resolutions. |

```sh
curl -X PUT "https://panel.example.com/api/v2/lines/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false}'
```

### DELETE `/api/v2/lines/{id}`

Delete line · Authentication: Admin only

```sh
curl -X DELETE "https://panel.example.com/api/v2/lines/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Lines object:

| Field | Type | Required | Description |
|---|---|---|---|
| `backup_default_on` | boolean |  | Whether this backup default line is currently in effect and resolved on the default line (read-only, maintained by the scheduled job). (read-only) |
| `backup_on` | boolean |  | Whether this backup IP is currently in DNS (read-only, maintained by the scheduled job). (read-only) |
| `created_at` | string |  | Created at (read-only) |
| `disable_by` | string |  | Disable source (manual by admin, monitor by node monitoring, bandwidth for bandwidth limit, etc.); empty when enabled. |
| `dns_line` | string |  | DNS resolution line code (default = default line; carrier/region line codes are mapped by the DNS domain). |
| `enabled` | boolean |  | Whether enabled |
| `force` | boolean |  | Force the write and skip two safeguards (requires a second confirmation): when the line becomes an enabled primary line, its node must have applied its configuration and be at most one release behind (otherwise 422 node_config_behind); disabling the last enabled primary line of the node group or making it a backup (otherwise 422 last_primary_line). |
| `id` | integer |  | Record ID (read-only) |
| `is_backup` | boolean |  | Backup IP; kept out of DNS normally and brought in or taken out automatically according to the node group's backup IP switch mode. |
| `is_backup_default` | boolean |  | Backup default line; when the node group has no default line in use, this line is automatically resolved on the default line instead, and switched back once one is in use again. |
| `node_group_id` | integer |  | ID of the node group this line belongs to. |
| `node_id` | integer |  | Node ID |
| `updated_at` | string |  | Updated at (read-only) |
| `weight` | integer |  | Weight, 1 to 100; a larger weight receives a larger share of resolutions. |

## Alert rules (`alert-rules`)

Built-in alert rules of the control plane with their current settings and status; thresholds are changed via system settings.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/alert-rules

### GET `/api/v2/alert-rules`

List alert rules and their status · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/alert-rules" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Alert rules object:

| Field | Type | Required | Description |
|---|---|---|---|
| `items` | AlertRule[] |  | Items on this page |

## Alerts (`alerts`)

Recent alert and recovery events, kept for 30 days.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/alerts

### GET `/api/v2/alerts`

List recent alerts and recoveries · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `rule` | string |  | Filter by rule name. |
| `kind` | AlertKind |  | Filter by kind: fire for alerts, resolve for recoveries. Values: ["fire","resolve"] |

```sh
curl "https://panel.example.com/api/v2/alerts" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Alerts object:

| Field | Type | Required | Description |
|---|---|---|---|
| `created_at` | string |  | Created at |
| `detail` | string |  | Additional details. |
| `id` | integer |  | Record ID |
| `kind` | AlertKind |  | fire: alert fired; resolve: recovered. Values: ["fire","resolve"] |
| `msg_id` | integer |  | Notification sent (in-app message ID). |
| `rule` | string |  | Alert rule key. |
| `subject` | string |  | Subject the alert is about, such as a node or site. |
| `threshold` | number |  | Threshold of the rule. |
| `title` | string |  | Chinese name of the rule (same as rule when the rule no longer exists). |
| `unit` | string |  | Unit of the value and threshold. |
| `value` | number |  | Value that triggered the alert. |

## Node monitor logs (`monitor-logs`)

Results of each probe round the control plane runs against monitored node IPs.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/monitor-logs

### GET `/api/v2/monitor-logs`

List node monitor probe logs · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `node_id` | integer |  | Filter by node. |
| `ip` | string |  | Filter by IP (exact match). |
| `result` | integer |  | Filter by result: 1 reachable, 0 unreachable, -1 undetermined. Values: [-1,0,1] |
| `type` | string |  | Filter by type: aval availability probe, bw bandwidth sample. Omitted = all. Values: ["aval","bw"] |
| `probe_group` | string |  | Filter by monitor group (as returned by the external probe service, exact match); empty string = rows from the master's built-in probe. Omitted = all. |
| `since` | string |  | Start time (inclusive), RFC 3339. |
| `until` | string |  | End time (exclusive), RFC 3339. |

```sh
curl "https://panel.example.com/api/v2/monitor-logs" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Node monitor logs object:

| Field | Type | Required | Description |
|---|---|---|---|
| `created_at` | string |  | Created at |
| `detail` | string |  | Why it was unreachable or undeterminable. |
| `id` | integer |  | Record ID |
| `ip` | string |  | Probed IP. |
| `node_id` | integer |  | Node ID |
| `result` | integer |  | 1: reachable; 0: unreachable; -1: undeterminable (http 5xx, whole round unreachable, etc.; not counted). Values: [-1,0,1] |
| `round_id` | integer |  | Probe round (same value within one round). |
| `type` | string |  | aval: availability probe (by the control plane or the external probe service); bw: bandwidth sample (one per minute, 0 when outbound is at or above the limit). Values: ["aval","bw"] |

## IP pools (`ip-pools`)

IP pools and the IPs inside them, maintained by admins. A pool belongs to a region and its name is unique platform-wide.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/ip-pools

### GET `/api/v2/ip-pools`

List IP pools · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `region_id` | integer |  | Filter by region. |

```sh
curl "https://panel.example.com/api/v2/ip-pools" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/ip-pools`

Create IP pool · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `des` | string |  | Description |
| `name` | string | Yes | Name |
| `region_id` | integer | Yes | Region ID |

```sh
curl -X POST "https://panel.example.com/api/v2/ip-pools" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/ip-pools/candidates`

IP pool candidate node IPs · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `region_id` | integer | Yes | Region ID. |

```sh
curl "https://panel.example.com/api/v2/ip-pools/candidates" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/ip-pools/{id}`

Get IP pool · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/ip-pools/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/ip-pools/{id}`

Update IP pool · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `des` | string |  | Description |
| `name` | string | Yes | Name |
| `region_id` | integer | Yes | Region ID |

```sh
curl -X PUT "https://panel.example.com/api/v2/ip-pools/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/ip-pools/{id}`

Delete IP pool · Authentication: Admin only

```sh
curl -X DELETE "https://panel.example.com/api/v2/ip-pools/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/ip-pools/{id}/refs`

IP pool referencing sites · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `ip_id` | integer |  | Show only this pool IP. |
| `limit` | integer |  | Maximum number of sites listed per IP. Default: 20 |

```sh
curl "https://panel.example.com/api/v2/ip-pools/{id}/refs" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/ip-pools/{pool_id}/ips`

List IPs in pool · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |

```sh
curl "https://panel.example.com/api/v2/ip-pools/{pool_id}/ips" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/ip-pools/{pool_id}/ips`

Add IP to pool · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `bind_addr` | string |  | The local address the node actually listens on (NAT setups); null means use ip. |
| `node_id` | integer | Yes | Node ID |

```sh
curl -X POST "https://panel.example.com/api/v2/ip-pools/{pool_id}/ips" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/ip-pools/{pool_id}/ips/{id}`

Update pool IP · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `bind_addr` | string |  | The local address the node actually listens on (NAT setups); null means use ip. |
| `enabled` | boolean |  | Whether enabled |

```sh
curl -X PUT "https://panel.example.com/api/v2/ip-pools/{pool_id}/ips/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/ip-pools/{pool_id}/ips/{id}`

Remove IP from pool · Authentication: Admin only

```sh
curl -X DELETE "https://panel.example.com/api/v2/ip-pools/{pool_id}/ips/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/ip-pools/{pool_id}/ips/{id}/issue-cert`

Issue platform certificate for pool IP · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `confirm` | boolean |  | Confirm re-issuing when a valid certificate already exists. |

```sh
curl -X POST "https://panel.example.com/api/v2/ip-pools/{pool_id}/ips/{id}/issue-cert" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

IP pools object:

| Field | Type | Required | Description |
|---|---|---|---|
| `bind_addr` | string |  | The local address the node actually listens on (NAT setups); null means use ip. |
| `cert` | IpCertState |  | Platform IP certificate status. |
| `confirm` | boolean |  | Confirm re-issuing when a valid certificate already exists. |
| `created_at` | string |  | Created at (read-only) |
| `des` | string |  | Description |
| `enabled` | boolean |  | Whether enabled |
| `hints` | string[] |  | Hints (e.g. the node reported no NIC addresses, so the pool entry was not verified). (read-only) |
| `id` | integer |  | Record ID (read-only) |
| `ip` | string |  | IP address. (read-only) |
| `ip_count` | integer |  | Number of IPs in the pool. (read-only) |
| `ips` | object[] |  | Pool IPs that have references, with their referencing sites. |
| `items` | IpCandidate[] |  | Items on this page |
| `name` | string |  | Name |
| `node_id` | integer |  | Node ID |
| `node_name` | string |  | Name of the node that owns the IP. (read-only) |
| `node_state` | IpNodeState |  | State of the owning primary node: disabled if the node is disabled; otherwise online if a heartbeat arrived in the last 2 minutes, else offline. Values: ["online","offline","disabled"] |
| `pool_id` | integer |  | ID of the IP pool it belongs to. (read-only) |
| `region_id` | integer |  | Region ID |
| `region_name` | string |  | Region name. (read-only) |
| `site_count` | integer |  | Number of IP sites referencing IPs in the pool (deduplicated by site). (read-only) |
| `updated_at` | string |  | Updated at (read-only) |

## IP sites (`ip-sites`)

Sites served on IPs with custom ports; the IPs must first be selected in the user package.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/ip-sites

### GET `/api/v2/ip-sites`

List IP sites · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `upid` | integer |  | Filter by user package. |

```sh
curl "https://panel.example.com/api/v2/ip-sites" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/ip-sites`

Create IP site · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `config` | object |  | Node configuration fields (same as the site config; kind is always ip, and listen_addrs is derived from the bindings at publish time). |
| `enabled` | boolean |  | Whether enabled |
| `ip_ids` | integer[] |  | IDs of the bound pool IPs, in order. |
| `name` | string |  | Name |
| `origins` | IpSiteOrigin[] |  | Origin servers. |
| `stop_code` | integer |  | 0 = normal; non-zero = every request returns this status code (set by admins or billing). |
| `upid` | integer |  | User package ID. |

```sh
curl -X POST "https://panel.example.com/api/v2/ip-sites" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/ip-sites/available-ips`

IP site available IPs · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `upid` | integer | Yes | User package ID. |
| `scope` | string |  | pool = the whole pool; mine = only the IPs already selected. Values: ["pool","mine"] Default: "pool" |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

```sh
curl "https://panel.example.com/api/v2/ip-sites/available-ips" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/ip-sites/port-check`

IP site port pre-check · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `http_ports` | integer[] |  | HTTP ports to check; ports outside 1-65535 are not rejected but reported as range. |
| `https_ports` | integer[] |  | HTTPS ports to check; ports outside 1-65535 are not rejected but reported as range. |
| `ip_ids` | integer[] | Yes | IDs of the bound pool IPs, in order. |
| `site_id` | integer |  | Pass when editing a site: ports held by this site itself are not counted as occupied. |
| `upid` | integer | Yes | User package ID. |

```sh
curl -X POST "https://panel.example.com/api/v2/ip-sites/port-check" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/ip-sites/{id}`

Get IP site · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/ip-sites/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/ip-sites/{id}`

Update IP site · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `config` | object |  | Node configuration fields (same as the site config; kind is always ip, and listen_addrs is derived from the bindings at publish time). |
| `enabled` | boolean |  | Whether enabled |
| `ip_ids` | integer[] |  | IDs of the bound pool IPs, in order. |
| `name` | string |  | Name |
| `origins` | IpSiteOrigin[] |  | Origin servers. |
| `stop_code` | integer |  | 0 = normal; non-zero = every request returns this status code (set by admins or billing). |
| `upid` | integer |  | User package ID. |

```sh
curl -X PUT "https://panel.example.com/api/v2/ip-sites/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/ip-sites/{id}`

Delete IP site · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/ip-sites/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

IP sites object:

| Field | Type | Required | Description |
|---|---|---|---|
| `access_addrs` | string[] |  | Access address in ip:port form. (read-only) |
| `config` | object |  | Node configuration fields (same as the site config; kind is always ip, and listen_addrs is derived from the bindings at publish time). |
| `created_at` | string |  | Created at (read-only) |
| `enabled` | boolean |  | Whether enabled |
| `hints` | string[] |  | Hints (e.g. the node reported no listening ports, so that check was skipped). (read-only) |
| `http_ports` | integer[] |  | HTTP ports to check; ports outside 1-65535 are not rejected but reported as range. |
| `https_ports` | integer[] |  | HTTPS ports to check; ports outside 1-65535 are not rejected but reported as range. |
| `id` | integer |  | Record ID (read-only) |
| `ip_ids` | integer[] |  | IDs of the bound pool IPs, in order. |
| `ips` | IpSiteIp[] |  | Details of the bound pool IPs. (read-only) |
| `items` | AvailableIp[] |  | Items on this page |
| `name` | string |  | Name |
| `origins` | IpSiteOrigin[] |  | Origin servers. |
| `ports` | object[] |  | Per-port check results. |
| `site_id` | integer |  | Pass when editing a site: ports held by this site itself are not counted as occupied. |
| `stop_code` | integer |  | 0 = normal; non-zero = every request returns this status code (set by admins or billing). |
| `tenant_id` | integer |  | Owner tenant (user) ID (read-only) |
| `updated_at` | string |  | Updated at (read-only) |
| `upid` | integer |  | User package ID. |

## Certificates (`certs`)

Uploaded or automatically issued HTTPS certificates. They can be bound to sites; a certificate in use by a site cannot be deleted or disabled.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/certs

### GET `/api/v2/certs`

List certificates · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `name` | string |  | Name contains. |
| `domain` | string |  | SAN contains (part of a domain or IP). |
| `scope` | string |  | site = site certificate; ip = platform IP certificate; panel = panel certificate (used by the controller's HTTPS listener). Values: ["site","ip","panel"] |
| `enabled` | boolean |  | Whether enabled |
| `expire_days` | integer |  | Only list certificates expiring within N days (including expired ones). |
| `id` | integer |  | Exact certificate ID match. |
| `ids` | string |  | Comma-separated certificate IDs (at most 200); matches any of them. Combined with id as an intersection. |
| `des` | string |  | Remark contains. |
| `type` | string |  | Certificate type. Values: ["custom","lets","zerossl","zerossl_api"] |
| `dns_api_id` | integer |  | Certificates using this DNS API credential. |
| `domains` | string |  | Comma-separated domains or IPs; matches certificates whose SAN contains any of them (exact match). |
| `issue_state` | string |  | ACME issuance state (uploaded certificates have none). Values: ["pending","running","issued","failed"] |
| `kind` | string |  | domain = SAN contains a domain; ip = SAN is all IPs. Values: ["domain","ip"] |

```sh
curl "https://panel.example.com/api/v2/certs" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/certs`

Create certificate · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `acme` | CertAcmeSummary |  | Issuance state summary (carried by certificate GET and list). Present only for issued certificates (type=lets/zerossl/zerossl_api); uploaded certificates (custom) do not have this field. See GET /certs/{id}/acme for the full state (challenge type, ARI window, etc.). |
| `cert` | string |  | Certificate chain PEM (leaf first, intermediates completed, root excluded); no private key (export it only via POST /certs/{id}/private-key). |
| `des` | string |  | Description |
| `enabled` | boolean |  | Whether enabled |
| `key` | string |  | Private key PEM (PKCS#1/PKCS#8/SEC1, must not be encrypted); omit when replacing the certificate to verify against the existing key. |
| `name` | string | Yes | Name |
| `scope` | string |  | Certificate scope: site = site certificate; ip = platform IP certificate; panel = panel certificate. Values: ["site","ip","panel"] |
| `type` | string |  | custom = uploaded; lets/zerossl = issued via ACME; zerossl_api = issued via the paid ZeroSSL REST API. |

```sh
curl -X POST "https://panel.example.com/api/v2/certs" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/certs/acme-states`

Get ACME issuance states in batch · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `ids` | string | Yes | Certificate IDs, comma-separated, at most 200. |

```sh
curl "https://panel.example.com/api/v2/certs/acme-states" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/certs/batch`

Batch-create ACME certificates · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `acme_account_id` | integer |  | Account to issue with (same as acme.acme_account_id in POST /certs); if omitted, the tenant's enabled account for the type is used first, then the platform's. |
| `auto_renew` | boolean |  | Defaults to true if omitted. |
| `des` | string |  | Description |
| `dns_api_id` | integer |  | DNS API credential used for DNS-01 validation (for ACME certificates). |
| `items` | CertAcme[] | Yes | Issuance states. |
| `type` | string |  | custom = uploaded; lets/zerossl = issued via ACME; zerossl_api = issued via the paid ZeroSSL REST API. |

```sh
curl -X POST "https://panel.example.com/api/v2/certs/batch" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"domains":["a.example.com"]},{"domains":["b.example.com","www.b.example.com"]}],"type":"lets"}'
```

### POST `/api/v2/certs/batch-issue`

Batch reissue or renew ACME certificates · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `ids` | integer[] | Yes | Certificate IDs. |

```sh
curl -X POST "https://panel.example.com/api/v2/certs/batch-issue" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ids":[1]}'
```

### GET `/api/v2/certs/zerossl-ips`

List IPs eligible for ZeroSSL certificates · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

```sh
curl "https://panel.example.com/api/v2/certs/zerossl-ips" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/certs/{id}`

Get certificate · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/certs/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/certs/{id}`

Update certificate · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `acme` | CertAcmeSummary |  | Issuance state summary (carried by certificate GET and list). Present only for issued certificates (type=lets/zerossl/zerossl_api); uploaded certificates (custom) do not have this field. See GET /certs/{id}/acme for the full state (challenge type, ARI window, etc.). |
| `cert` | string |  | Certificate chain PEM (leaf first, intermediates completed, root excluded); no private key (export it only via POST /certs/{id}/private-key). |
| `des` | string |  | Description |
| `enabled` | boolean |  | Whether enabled |
| `key` | string |  | Private key PEM (PKCS#1/PKCS#8/SEC1, must not be encrypted); omit when replacing the certificate to verify against the existing key. |
| `name` | string | Yes | Name |
| `scope` | string |  | Certificate scope: site = site certificate; ip = platform IP certificate; panel = panel certificate. Values: ["site","ip","panel"] |
| `type` | string |  | custom = uploaded; lets/zerossl = issued via ACME; zerossl_api = issued via the paid ZeroSSL REST API. |

```sh
curl -X PUT "https://panel.example.com/api/v2/certs/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/certs/{id}`

Delete certificate · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/certs/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/certs/{id}/acme`

Get certificate ACME issuance status · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/certs/{id}/acme" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/certs/{id}/acme`

Update certificate auto-renewal switch · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `auto_renew` | boolean | Yes | Defaults to true if omitted. |

```sh
curl -X PUT "https://panel.example.com/api/v2/certs/{id}/acme" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"auto_renew":false}'
```

### POST `/api/v2/certs/{id}/bind`

Bind certificate to sites · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `site_ids` | integer[] | Yes | Sites using this certificate. |

```sh
curl -X POST "https://panel.example.com/api/v2/certs/{id}/bind" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_ids":[1]}'
```

### POST `/api/v2/certs/{id}/issue`

Reissue ACME certificate · Authentication: User (some parameters admin only)

```sh
curl -X POST "https://panel.example.com/api/v2/certs/{id}/issue" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/certs/{id}/private-key`

Export certificate private key · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `password` | string |  | Current login password. |
| `totp_code` | string |  | Current 6-digit TOTP code; the account must have TOTP enabled. |

```sh
curl -X POST "https://panel.example.com/api/v2/certs/{id}/private-key" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Certificates object:

| Field | Type | Required | Description |
|---|---|---|---|
| `acme` | CertAcmeSummary |  | Issuance state summary (carried by certificate GET and list). Present only for issued certificates (type=lets/zerossl/zerossl_api); uploaded certificates (custom) do not have this field. See GET /certs/{id}/acme for the full state (challenge type, ARI window, etc.). (read-only) |
| `acme_account_id` | integer |  | Account to issue with (same as acme.acme_account_id in POST /certs); if omitted, the tenant's enabled account for the type is used first, then the platform's. |
| `ari_end` | string |  | End of the renewal window suggested by the CA via ARI. |
| `ari_start` | string |  | Renewal window suggested by the CA via ARI; null = none given by the CA, renew cert.acme.renew_before_days days before expiry. |
| `auto_renew` | boolean |  | Defaults to true if omitted. |
| `cert` | string |  | Certificate chain PEM (leaf first, intermediates completed, root excluded); no private key (export it only via POST /certs/{id}/private-key). (read-only) |
| `cert_id` | integer |  | Certificate ID. |
| `challenge` | string |  | Validation method: http-01, dns-01, or http-file (ZeroSSL file validation for zerossl_api). Values: ["http-01","dns-01","http-file"] |
| `created_at` | string |  | Created at (read-only) |
| `des` | string |  | Description |
| `dns_api_id` | integer |  | DNS API credential used for DNS-01 validation (for ACME certificates). |
| `enabled` | boolean |  | Whether enabled |
| `err_times` | integer |  | Number of failures in the current round. |
| `failed` | integer |  | Number of failed items. |
| `fingerprint` | string |  | SHA-256 fingerprint of the leaf certificate (lowercase hex). (read-only) |
| `id` | integer |  | Record ID (read-only) |
| `ids` | integer[] | Yes | Certificate IDs. |
| `issued_at` | string |  | Time of the last successful issuance. |
| `issuer` | string |  | Certificate issuer. (read-only) |
| `items` | CertAcme[] |  | Issuance states. |
| `key` | string |  | Private key PEM (PKCS#1/PKCS#8/SEC1, must not be encrypted); omit when replacing the certificate to verify against the existing key. |
| `last_error` | string |  | Last error |
| `name` | string |  | Name |
| `names` | string[] |  | SANs (domains, wildcard domains or IPs); falls back to CN when there is no SAN. (read-only) |
| `next_try_at` | string |  | After a renewal ends in failure, the earliest time the renewal scan will retry. |
| `not_after` | string |  | Certificate expiry time. (read-only) |
| `not_before` | string |  | Time the certificate becomes valid. (read-only) |
| `ocsp` | boolean |  | The leaf certificate carries an OCSP URL (nodes can staple). (read-only) |
| `ok` | integer |  | Number of items queued for issuance. |
| `password` | string |  | Current login password. |
| `renewing` | boolean |  | The current round is a renewal (the old certificate stays in use on failure). |
| `scope` | string |  | Certificate scope: site = site certificate; ip = platform IP certificate; panel = panel certificate. Values: ["site","ip","panel"] |
| `site_ids` | integer[] |  | Sites using this certificate. (read-only) |
| `state` | string |  | pending = queued or waiting to retry; running = issuing; issued = issued; failed = retries exhausted (alert sent). Values: ["pending","running","issued","failed"] |
| `tenant_id` | integer |  | Owning tenant; null = platform IP certificate. (read-only) |
| `totp_code` | string |  | Current 6-digit TOTP code; the account must have TOTP enabled. |
| `type` | string |  | custom = uploaded; lets/zerossl = issued via ACME; zerossl_api = issued via the paid ZeroSSL REST API. |
| `updated_at` | string |  | Updated at (read-only) |
| `version` | integer |  | Certificate content version; incremented on each replacement. (read-only) |
| `zerossl_order_id` | string |  | Current ZeroSSL order ID of a zerossl_api certificate; empty = no order yet. |
| `zerossl_order_state` | string |  | ZeroSSL order status (draft, pending_validation, issued, cancelled, etc.); empty = no order yet. |

## ACME accounts (`acme-accounts`)

CA accounts used for automatic issuance (Let's Encrypt, ZeroSSL). Registered with the CA on first issuance.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/acme-accounts

### GET `/api/v2/acme-accounts`

List ACME accounts · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `ca` | string |  | Only list accounts of this CA. Values: ["lets","zerossl"] |

```sh
curl "https://panel.example.com/api/v2/acme-accounts" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/acme-accounts`

Create ACME account · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `api_key` | string |  | Mask of the ZeroSSL REST API key (`****abcd`); empty = not set. The key is stored encrypted and never returned in plaintext. |
| `ca` | string | Yes | CA that issues certificates: lets = Let's Encrypt; zerossl = ZeroSSL. Values: ["lets","zerossl"] |
| `directory` | string |  | ACME directory URL; empty = the CA's URL in the controller's acme.directories (the production URL by default). |
| `eab_hmac` | string |  | Masked (last 4 chars shown as `****abcd` when 16+ chars, otherwise `********`); plaintext is never returned. |
| `eab_kid` | string |  | EAB key ID (used by ZeroSSL). |
| `email` | string |  | Email |
| `enabled` | boolean |  | Whether enabled |
| `name` | string | Yes | Name |
| `recheck` | boolean |  | When true, re-check the stored API key; on success the account becomes usable again and certificates waiting on it are retried immediately. |

```sh
curl -X POST "https://panel.example.com/api/v2/acme-accounts" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ca":"zerossl","eab_hmac":"${EAB_HMAC}","eab_kid":"kid-example","name":"zerossl-main"}'
```

### GET `/api/v2/acme-accounts/{id}`

Get ACME account · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/acme-accounts/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/acme-accounts/{id}`

Update ACME account · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `api_key` | string |  | Mask of the ZeroSSL REST API key (`****abcd`); empty = not set. The key is stored encrypted and never returned in plaintext. |
| `ca` | string | Yes | CA that issues certificates: lets = Let's Encrypt; zerossl = ZeroSSL. Values: ["lets","zerossl"] |
| `directory` | string |  | ACME directory URL; empty = the CA's URL in the controller's acme.directories (the production URL by default). |
| `eab_hmac` | string |  | Masked (last 4 chars shown as `****abcd` when 16+ chars, otherwise `********`); plaintext is never returned. |
| `eab_kid` | string |  | EAB key ID (used by ZeroSSL). |
| `email` | string |  | Email |
| `enabled` | boolean |  | Whether enabled |
| `name` | string | Yes | Name |
| `recheck` | boolean |  | When true, re-check the stored API key; on success the account becomes usable again and certificates waiting on it are retried immediately. |

```sh
curl -X PUT "https://panel.example.com/api/v2/acme-accounts/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ca":"zerossl","eab_hmac":"${EAB_HMAC}","eab_kid":"kid-example","name":"zerossl-main"}'
```

### DELETE `/api/v2/acme-accounts/{id}`

Delete ACME account · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/acme-accounts/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

ACME accounts object:

| Field | Type | Required | Description |
|---|---|---|---|
| `api_checked_at` | string |  | Time the API key was last checked. (read-only) |
| `api_error` | string |  | Last account-level error from checking the API key or issuing with it. (read-only) |
| `api_key` | string |  | Mask of the ZeroSSL REST API key (`****abcd`); empty = not set. The key is stored encrypted and never returned in plaintext. (read-only) |
| `api_state` | string |  | API key state: none = not set; ok = usable; blocked = account-level error (quota reached, unpaid, key rejected; retried after 6 hours or after a successful recheck); error = the key cannot be decrypted and must be re-entered. Values: ["none","ok","blocked","error"] (read-only) |
| `ca` | string |  | CA that issues certificates: lets = Let's Encrypt; zerossl = ZeroSSL. Values: ["lets","zerossl"] |
| `created_at` | string |  | Created at (read-only) |
| `directory` | string |  | ACME directory URL; empty = the CA's URL in the controller's acme.directories (the production URL by default). |
| `eab_hmac` | string |  | Masked (last 4 chars shown as `****abcd` when 16+ chars, otherwise `********`); plaintext is never returned. |
| `eab_kid` | string |  | EAB key ID (used by ZeroSSL). |
| `email` | string |  | Email |
| `enabled` | boolean |  | Whether enabled |
| `id` | integer |  | Record ID (read-only) |
| `last_error` | string |  | Last error (read-only) |
| `name` | string |  | Name |
| `recheck` | boolean |  | When true, re-check the stored API key; on success the account becomes usable again and certificates waiting on it are retried immediately. |
| `status` | string |  | new = not registered yet; registered = registered with the CA; error = registration failed (see last_error). Values: ["new","registered","error"] (read-only) |
| `tenant_id` | integer |  | Owning tenant; null = platform account. (read-only) |
| `updated_at` | string |  | Updated at (read-only) |

## DNS API (cert validation) (`dns-apis`)

DNS provider API credentials used for DNS-01 validation and wildcard certificates. Credentials are write-only; responses return only masks.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/dns-apis

### GET `/api/v2/dns-apis`

List DNS API credentials · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

```sh
curl "https://panel.example.com/api/v2/dns-apis" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/dns-apis`

Create DNS API credential · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `auth` | object |  | Credential key name to mask (last 4 chars shown as `****abcd` when 16+ chars, otherwise `********`); plaintext is never returned. |
| `des` | string |  | Description |
| `name` | string | Yes | Name |
| `type` | string | Yes | DNS provider. |

```sh
curl -X POST "https://panel.example.com/api/v2/dns-apis" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"auth":{"DP_Id":"12345","DP_Key":"${DP_KEY}"},"name":"my-dns","type":"dnspod_cn"}'
```

### GET `/api/v2/dns-apis/{id}`

Get DNS API credential · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/dns-apis/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/dns-apis/{id}`

Update DNS API credential · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `auth` | object |  | Credential key name to mask (last 4 chars shown as `****abcd` when 16+ chars, otherwise `********`); plaintext is never returned. |
| `des` | string |  | Description |
| `name` | string | Yes | Name |
| `type` | string | Yes | DNS provider. |

```sh
curl -X PUT "https://panel.example.com/api/v2/dns-apis/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"auth":{"DP_Id":"12345","DP_Key":"${DP_KEY}"},"name":"my-dns","type":"dnspod_cn"}'
```

### DELETE `/api/v2/dns-apis/{id}`

Delete DNS API credential · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/dns-apis/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

DNS API (cert validation) object:

| Field | Type | Required | Description |
|---|---|---|---|
| `auth` | object |  | Credential key name to mask (last 4 chars shown as `****abcd` when 16+ chars, otherwise `********`); plaintext is never returned. |
| `created_at` | string |  | Created at (read-only) |
| `des` | string |  | Description |
| `id` | integer |  | Record ID (read-only) |
| `name` | string |  | Name |
| `tenant_id` | integer |  | Owner tenant (user) ID (read-only) |
| `type` | string |  | DNS provider. |
| `updated_at` | string |  | Updated at (read-only) |

## Certificate settings (`cert-settings`)

The current user's default certificate type for new certificates; leave empty to follow the platform.

Authentication: User · Docs: https://www-demo.kuxitong.com/en/docs/api/cert-settings

### GET `/api/v2/cert-settings`

Get certificate settings · Authentication: User

```sh
curl "https://panel.example.com/api/v2/cert-settings" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/cert-settings`

Update certificate settings · Authentication: User

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `default_type` | string | Yes | The user's own default type (custom/lets/zerossl); null = follow the platform. |

```sh
curl -X PUT "https://panel.example.com/api/v2/cert-settings" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"default_type":"custom"}'
```

Certificate settings object:

| Field | Type | Required | Description |
|---|---|---|---|
| `default_type` | string |  | The user's own default type (custom/lets/zerossl); null = follow the platform. |
| `effective_type` | string |  | The default type actually in effect. (read-only) |

## Panel certificate (`panel-cert`)

Admin only. Status of the panel's own HTTPS certificate; one click issues a Let's Encrypt certificate for the panel domain, renewed with certificate management.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/panel-cert

### GET `/api/v2/panel-cert`

Get panel certificate status · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/panel-cert" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/panel-cert/issue`

Issue panel certificate · Authentication: Admin only

```sh
curl -X POST "https://panel.example.com/api/v2/panel-cert/issue" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Panel certificate object:

| Field | Type | Required | Description |
|---|---|---|---|
| `acme` | CertAcme |  | Automatic issuance state (state, challenge type, renewal and the last failure reason). |
| `cert` | Cert |  | Certificate (responses carry the chain, names, expiry and status but never the private key; export the key via the export endpoint). |
| `cert_id` | integer |  | panel.tls.cert_id; 0 when no panel certificate is configured. |
| `domain` | string |  | Panel domain; empty when not set. |
| `https` | string |  | Current state of panel HTTPS. on = the panel certificate is usable and the controller listens for HTTPS; no_domain = no panel domain set (no certificate is requested); no_cert = a domain is set but there is no usable panel certificate; issuing = the certificate is being issued; failed = issuance failed (see acme.last_error; automatic requests retry at intervals); no_listen = the certificate exists but the controller config file has no HTTPS listener (listen_tls); expired = the panel certificate has expired (not_after has passed and renewal has not succeeded). Values: ["on","no_domain","no_cert","issuing","failed","no_listen","expired"] |

## File validation (`pki-validations`)

Admin only. Provides content for the CA file validation, answered by nodes via the controller; no longer served after expiry.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/pki-validations

### GET `/api/v2/pki-validations`

List validation file contents · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |

```sh
curl "https://panel.example.com/api/v2/pki-validations" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/pki-validations`

Create validation file content · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `content` | string | Yes | Validation file content. |
| `name` | string | Yes | Name |
| `ttl_s` | integer |  | Seconds after which the file is no longer served; 0 or omitted = never expires. |

```sh
curl -X POST "https://panel.example.com/api/v2/pki-validations" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content":"example-validation-content","name":"0123456789ABCDEF0123456789ABCDEF.txt","ttl_s":86400}'
```

### DELETE `/api/v2/pki-validations/{id}`

Delete validation file content · Authentication: Admin only

```sh
curl -X DELETE "https://panel.example.com/api/v2/pki-validations/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

File validation object:

| Field | Type | Required | Description |
|---|---|---|---|
| `content` | string |  | Validation file content. |
| `created_at` | string |  | Created at (read-only) |
| `expires_at` | string |  | Expires at |
| `id` | integer |  | Record ID (read-only) |
| `name` | string |  | Name |
| `ttl_s` | integer |  | Seconds after which the file is no longer served; 0 or omitted = never expires. |
| `updated_at` | string |  | Updated at (read-only) |

## Access control lists (`acls`)

Rule sets that allow or block requests by IP/CIDR and other conditions; the platform provides built-in rules and tenants can create their own.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/acls

### GET `/api/v2/acls`

List ACLs · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `enabled` | boolean |  | Filter by enabled state. |
| `name` | string |  | Filter by name substring. |
| `owner` | string |  | platform = platform objects only; tenant = tenant objects only. Values: ["platform","tenant"] |

```sh
curl "https://panel.example.com/api/v2/acls" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/acls`

Create ACL · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `default_action` | string | Yes | Action when no rule matches. Values: ["allow","deny"] |
| `des` | string |  | Remark; empty if omitted (omitting it on update also clears it, since the update replaces the whole object). |
| `enabled` | boolean |  | Whether enabled Default: true |
| `name` | string | Yes | Name |
| `rules` | AclRule[] | Yes | Matched in order; the count is limited by the setting protect.acl_rules_max. |
| `tenant_id` | integer |  | Owning tenant; null = platform object (built-in or created by an admin, read-only for tenants). |

```sh
curl -X POST "https://panel.example.com/api/v2/acls" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"default_action":"deny","name":"office","rules":[{"action":"allow","when":{"f":"ip","op":"cidr","v":["192.0.2.0/24"]}}]}'
```

### POST `/api/v2/acls/batch-enable`

Batch enable or disable ACLs · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `enabled` | boolean | Yes | Whether enabled Default: true |
| `ids` | integer[] | Yes | IDs of the objects to change, at most 100. |

```sh
curl -X POST "https://panel.example.com/api/v2/acls/batch-enable" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false,"ids":[1,2]}'
```

### GET `/api/v2/acls/{id}`

Get ACL · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/acls/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/acls/{id}`

Update ACL · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `default_action` | string | Yes | Action when no rule matches. Values: ["allow","deny"] |
| `des` | string |  | Remark; empty if omitted (omitting it on update also clears it, since the update replaces the whole object). |
| `enabled` | boolean |  | Whether enabled Default: true |
| `name` | string | Yes | Name |
| `rules` | AclRule[] | Yes | Matched in order; the count is limited by the setting protect.acl_rules_max. |
| `tenant_id` | integer |  | Owning tenant; null = platform object (built-in or created by an admin, read-only for tenants). |

```sh
curl -X PUT "https://panel.example.com/api/v2/acls/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"default_action":"deny","name":"office","rules":[{"action":"allow","when":{"f":"ip","op":"cidr","v":["192.0.2.0/24"]}}]}'
```

### DELETE `/api/v2/acls/{id}`

Delete ACL · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/acls/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Access control lists object:

| Field | Type | Required | Description |
|---|---|---|---|
| `builtin` | boolean |  | Built-in object; cannot be deleted. (read-only) |
| `created_at` | string |  | Created at (read-only) |
| `default_action` | string |  | Action when no rule matches. Values: ["allow","deny"] |
| `des` | string |  | Remark; empty if omitted (omitting it on update also clears it, since the update replaces the whole object). |
| `enabled` | boolean |  | Whether enabled Default: true |
| `errors` | FieldError[] |  | Why each remaining item was not changed; field is /ids/<index>, code as in the single-item update (not_found, readonly and so on). |
| `id` | integer |  | Record ID (read-only) |
| `ids` | integer[] | Yes | IDs of the objects to change, at most 100. |
| `name` | string |  | Name |
| `rules` | AclRule[] |  | Matched in order; the count is limited by the setting protect.acl_rules_max. |
| `tenant_id` | integer |  | Owning tenant; null = platform object (built-in or created by an admin, read-only for tenants). (read-only) |
| `updated` | integer[] |  | IDs that were set (including those already in the target state), in request order. |
| `updated_at` | string |  | Updated at (read-only) |

## CC matchers (`cc-matches`)

Conditions that identify requests in CC protection; reusable across rule groups.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/cc-matches

### GET `/api/v2/cc-matches`

List CC matchers · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `name` | string |  | Filter by name substring. |
| `owner` | string |  | platform = platform objects only; tenant = tenant objects only. Values: ["platform","tenant"] |

```sh
curl "https://panel.example.com/api/v2/cc-matches" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/cc-matches`

Create CC matcher · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `des` | string |  | Remark; empty if omitted (omitting it on update also clears it, since the update replaces the whole object). |
| `name` | string | Yes | Name |
| `when` | object |  | Rule DSL condition: a combination of all/any/not, or a leaf {f, op, v, set, neg}; validated by the server and errors are returned as 422 (field is a JSON Pointer, code is the lowercase DSL error code). null = match everything. |

```sh
curl -X POST "https://panel.example.com/api/v2/cc-matches" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"static","when":{"f":"req_uri","op":"contains","v":[".js",".css"]}}'
```

### POST `/api/v2/cc-matches/replay`

Replay a CC matcher draft · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `site_id` | integer |  | Count this site's traffic only; omitted = every site visible to the caller. |
| `when` | object |  | Rule DSL condition: a combination of all/any/not, or a leaf {f, op, v, set, neg}; validated by the server and errors are returned as 422 (field is a JSON Pointer, code is the lowercase DSL error code). null = match everything. |

```sh
curl -X POST "https://panel.example.com/api/v2/cc-matches/replay" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"when":{"f":"ja4","op":"eq","v":["t13d1516h2_8daaf6152771_e5627efa2ab1"]}}'
```

### GET `/api/v2/cc-matches/{id}`

Get CC matcher · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/cc-matches/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/cc-matches/{id}`

Update CC matcher · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `des` | string |  | Remark; empty if omitted (omitting it on update also clears it, since the update replaces the whole object). |
| `name` | string | Yes | Name |
| `when` | object |  | Rule DSL condition: a combination of all/any/not, or a leaf {f, op, v, set, neg}; validated by the server and errors are returned as 422 (field is a JSON Pointer, code is the lowercase DSL error code). null = match everything. |

```sh
curl -X PUT "https://panel.example.com/api/v2/cc-matches/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"static","when":{"f":"req_uri","op":"contains","v":[".js",".css"]}}'
```

### DELETE `/api/v2/cc-matches/{id}`

Delete CC matcher · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/cc-matches/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

CC matchers object:

| Field | Type | Required | Description |
|---|---|---|---|
| `builtin` | boolean |  | Built-in object; cannot be deleted. (read-only) |
| `created_at` | string |  | Created at (read-only) |
| `des` | string |  | Remark; empty if omitted (omitting it on update also clears it, since the update replaces the whole object). |
| `hits` | integer |  | Requests matched in the window (restored by sample_weight). |
| `id` | integer |  | Record ID (read-only) |
| `ips` | integer |  | Distinct client IPs among the matched requests (a distinct count, not restored by sample weight). |
| `name` | string |  | Name |
| `reason` | string |  | Explanation when supported=false. |
| `sampled` | boolean |  | Some rows in the window were sampled (sample_weight > 1); hits and total are estimates. |
| `site_id` | integer |  | Count this site's traffic only; omitted = every site visible to the caller. |
| `sites` | integer |  | Distinct sites among the matched requests (a distinct count, not restored by sample weight). |
| `supported` | boolean |  | false = the condition uses something access_log does not hold (request headers, accept_language, named sets), so nothing was replayed; see reason. |
| `tenant_id` | integer |  | Owning tenant; null = platform object (built-in or created by an admin, read-only for tenants). (read-only) |
| `top_ips` | CcMatchReplayIp[] |  | Client IPs with the most matched requests, at most 20, sorted by requests descending; empty when supported=false or nothing matched. |
| `top_urls` | CcMatchReplayUrl[] |  | URLs (without the query string) with the most matched requests, at most 5, sorted by requests descending; empty when supported=false or nothing matched. |
| `total` | integer |  | Total requests in the window (also restored). |
| `updated_at` | string |  | Updated at (read-only) |
| `when` | object |  | Rule DSL condition: a combination of all/any/not, or a leaf {f, op, v, set, neg}; validated by the server and errors are returned as 422 (field is a JSON Pointer, code is the lowercase DSL error code). null = match everything. |
| `window_hours` | integer |  | Replay window in hours (fixed at 24). |

## CC filters (`cc-filters`)

How matched requests are handled: rate limiting or a challenge (JS, slider, captcha and so on).

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/cc-filters

### GET `/api/v2/cc-filters`

List CC filters · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `name` | string |  | Filter by name substring. |
| `owner` | string |  | platform = platform objects only; tenant = tenant objects only. Values: ["platform","tenant"] |

```sh
curl "https://panel.example.com/api/v2/cc-filters" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/cc-filters`

Create CC filter · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `count_key` | string |  | Who to count by (rate/challenge): ip = client IP (default, existing behavior); ja4 = TLS fingerprint only, regardless of IP — real visitors sharing the fingerprint are counted together, so it is allowed with the log action only (a rule item with ban/deny is rejected with 422); to punish use ip_ja4 = same IP and same fingerprint combined. Plaintext requests have no fingerprint, so ja4/ip_ja4 fall back to IP. Not used by url_auth (must be ip). Values: ["ip","ja4","ip_ja4"] |
| `des` | string |  | Remark; empty if omitted (omitting it on update also clears it, since the update replaces the whole object). |
| `kind` | string |  | Challenge type (required for challenge): js/redirect/slide/captcha/click/delay_jump/rotate/cookie; not used for rate. |
| `max` | integer | Yes | Requests allowed within the window. |
| `mode` | string |  | Required for url_auth: signing scheme. a = ?<sign_arg>=md5(key+path+timestamp)&<time_arg>=timestamp; b = ?<sign_arg>=timestamp-random-uid-md5(path-timestamp-random-uid-key). Values: ["a","b"] |
| `name` | string | Yes | Name |
| `per_uri_max` | integer |  | rate: per-URI limit; 0 = unlimited. |
| `sign_arg` | string |  | Required for url_auth: name of the signature query parameter (letters, digits, underscore, hyphen). |
| `sign_use_max` | integer |  | url_auth: how many times the same signature may be used; 0 = unlimited. |
| `time_arg` | string |  | Required for url_auth with mode=a: name of the timestamp query parameter; not used with mode=b. |
| `ttl_s` | integer |  | Required for url_auth (1 to 2592000): maximum difference in seconds between the signed timestamp and the current time. |
| `type` | string | Yes | rate = request rate; challenge = challenge (the type is given by kind); url_auth = URL signature check (the signing key is the site's config.url_auth_salt). Values: ["rate","challenge","url_auth"] |
| `window_s` | integer | Yes | Counting window in seconds. |

```sh
curl -X POST "https://panel.example.com/api/v2/cc-filters" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"max":20,"name":"rate-5-20","per_uri_max":10,"type":"rate","window_s":5}'
```

### GET `/api/v2/cc-filters/{id}`

Get CC filter · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/cc-filters/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/cc-filters/{id}`

Update CC filter · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `count_key` | string |  | Who to count by (rate/challenge): ip = client IP (default, existing behavior); ja4 = TLS fingerprint only, regardless of IP — real visitors sharing the fingerprint are counted together, so it is allowed with the log action only (a rule item with ban/deny is rejected with 422); to punish use ip_ja4 = same IP and same fingerprint combined. Plaintext requests have no fingerprint, so ja4/ip_ja4 fall back to IP. Not used by url_auth (must be ip). Values: ["ip","ja4","ip_ja4"] |
| `des` | string |  | Remark; empty if omitted (omitting it on update also clears it, since the update replaces the whole object). |
| `kind` | string |  | Challenge type (required for challenge): js/redirect/slide/captcha/click/delay_jump/rotate/cookie; not used for rate. |
| `max` | integer | Yes | Requests allowed within the window. |
| `mode` | string |  | Required for url_auth: signing scheme. a = ?<sign_arg>=md5(key+path+timestamp)&<time_arg>=timestamp; b = ?<sign_arg>=timestamp-random-uid-md5(path-timestamp-random-uid-key). Values: ["a","b"] |
| `name` | string | Yes | Name |
| `per_uri_max` | integer |  | rate: per-URI limit; 0 = unlimited. |
| `sign_arg` | string |  | Required for url_auth: name of the signature query parameter (letters, digits, underscore, hyphen). |
| `sign_use_max` | integer |  | url_auth: how many times the same signature may be used; 0 = unlimited. |
| `time_arg` | string |  | Required for url_auth with mode=a: name of the timestamp query parameter; not used with mode=b. |
| `ttl_s` | integer |  | Required for url_auth (1 to 2592000): maximum difference in seconds between the signed timestamp and the current time. |
| `type` | string | Yes | rate = request rate; challenge = challenge (the type is given by kind); url_auth = URL signature check (the signing key is the site's config.url_auth_salt). Values: ["rate","challenge","url_auth"] |
| `window_s` | integer | Yes | Counting window in seconds. |

```sh
curl -X PUT "https://panel.example.com/api/v2/cc-filters/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"max":20,"name":"rate-5-20","per_uri_max":10,"type":"rate","window_s":5}'
```

### DELETE `/api/v2/cc-filters/{id}`

Delete CC filter · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/cc-filters/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

CC filters object:

| Field | Type | Required | Description |
|---|---|---|---|
| `builtin` | boolean |  | Built-in object; cannot be deleted. (read-only) |
| `count_key` | string |  | Who to count by (rate/challenge): ip = client IP (default, existing behavior); ja4 = TLS fingerprint only, regardless of IP — real visitors sharing the fingerprint are counted together, so it is allowed with the log action only (a rule item with ban/deny is rejected with 422); to punish use ip_ja4 = same IP and same fingerprint combined. Plaintext requests have no fingerprint, so ja4/ip_ja4 fall back to IP. Not used by url_auth (must be ip). Values: ["ip","ja4","ip_ja4"] |
| `created_at` | string |  | Created at (read-only) |
| `des` | string |  | Remark; empty if omitted (omitting it on update also clears it, since the update replaces the whole object). |
| `id` | integer |  | Record ID (read-only) |
| `kind` | string |  | Challenge type (required for challenge): js/redirect/slide/captcha/click/delay_jump/rotate/cookie; not used for rate. |
| `max` | integer |  | Requests allowed within the window. |
| `mode` | string |  | Required for url_auth: signing scheme. a = ?<sign_arg>=md5(key+path+timestamp)&<time_arg>=timestamp; b = ?<sign_arg>=timestamp-random-uid-md5(path-timestamp-random-uid-key). Values: ["a","b"] |
| `name` | string |  | Name |
| `per_uri_max` | integer |  | rate: per-URI limit; 0 = unlimited. |
| `sign_arg` | string |  | Required for url_auth: name of the signature query parameter (letters, digits, underscore, hyphen). |
| `sign_use_max` | integer |  | url_auth: how many times the same signature may be used; 0 = unlimited. |
| `tenant_id` | integer |  | Owning tenant; null = platform object (built-in or created by an admin, read-only for tenants). (read-only) |
| `time_arg` | string |  | Required for url_auth with mode=a: name of the timestamp query parameter; not used with mode=b. |
| `ttl_s` | integer |  | Required for url_auth (1 to 2592000): maximum difference in seconds between the signed timestamp and the current time. |
| `type` | string |  | rate = request rate; challenge = challenge (the type is given by kind); url_auth = URL signature check (the signing key is the site's config.url_auth_salt). Values: ["rate","challenge","url_auth"] |
| `updated_at` | string |  | Updated at (read-only) |
| `window_s` | integer |  | Counting window in seconds. |

## CC rules (`cc-rules`)

Ordered combinations of matchers and filters; a site can pick one rule group.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/cc-rules

### GET `/api/v2/cc-rules`

List CC rule groups · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `enabled` | boolean |  | Filter by enabled state. |
| `name` | string |  | Filter by name substring. |
| `owner` | string |  | platform = platform objects only; tenant = tenant objects only. Values: ["platform","tenant"] |

```sh
curl "https://panel.example.com/api/v2/cc-rules" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/cc-rules`

Create CC rule group · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `des` | string |  | Remark; empty if omitted (omitting it on update also clears it, since the update replaces the whole object). |
| `enabled` | boolean |  | Whether enabled Default: true |
| `items` | CcRuleItem[] | Yes | Rule items, executed in order; the count is limited by the setting protect.cc_rule_items_max. |
| `name` | string | Yes | Name |
| `show_to` | integer[] |  | Tenants that can see a platform rule group; empty = visible to all tenants, otherwise only the listed tenants can see it and select it as a site's cc_level. Returned to admins only. |
| `sort` | integer |  | Panel sort order (smaller first). |
| `tenant_id` | integer |  | Owning tenant; null = platform object (built-in or created by an admin, read-only for tenants). |
| `visible` | boolean |  | Shown in the site's rule group drop-down. Default: true |

```sh
curl -X POST "https://panel.example.com/api/v2/cc-rules" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"action":"ban","filter_ids":[8],"match_id":3}],"name":"js"}'
```

### POST `/api/v2/cc-rules/batch-enable`

Batch enable or disable CC rule groups · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `enabled` | boolean | Yes | Whether enabled Default: true |
| `ids` | integer[] | Yes | IDs of the objects to change, at most 100. |

```sh
curl -X POST "https://panel.example.com/api/v2/cc-rules/batch-enable" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false,"ids":[1,2]}'
```

### GET `/api/v2/cc-rules/{id}`

Get CC rule group · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/cc-rules/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/cc-rules/{id}`

Update CC rule group · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `des` | string |  | Remark; empty if omitted (omitting it on update also clears it, since the update replaces the whole object). |
| `enabled` | boolean |  | Whether enabled Default: true |
| `items` | CcRuleItem[] | Yes | Rule items, executed in order; the count is limited by the setting protect.cc_rule_items_max. |
| `name` | string | Yes | Name |
| `show_to` | integer[] |  | Tenants that can see a platform rule group; empty = visible to all tenants, otherwise only the listed tenants can see it and select it as a site's cc_level. Returned to admins only. |
| `sort` | integer |  | Panel sort order (smaller first). |
| `tenant_id` | integer |  | Owning tenant; null = platform object (built-in or created by an admin, read-only for tenants). |
| `visible` | boolean |  | Shown in the site's rule group drop-down. Default: true |

```sh
curl -X PUT "https://panel.example.com/api/v2/cc-rules/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"action":"ban","filter_ids":[8],"match_id":3}],"name":"js"}'
```

### DELETE `/api/v2/cc-rules/{id}`

Delete CC rule group · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/cc-rules/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

CC rules object:

| Field | Type | Required | Description |
|---|---|---|---|
| `builtin` | boolean |  | Built-in object; cannot be deleted. (read-only) |
| `created_at` | string |  | Created at (read-only) |
| `des` | string |  | Remark; empty if omitted (omitting it on update also clears it, since the update replaces the whole object). |
| `enabled` | boolean |  | Whether enabled Default: true |
| `errors` | FieldError[] |  | Why each remaining item was not changed; field is /ids/<index>, code as in the single-item update (not_found, readonly and so on). |
| `id` | integer |  | Record ID (read-only) |
| `ids` | integer[] | Yes | IDs of the objects to change, at most 100. |
| `items` | CcRuleItem[] |  | Rule items, executed in order; the count is limited by the setting protect.cc_rule_items_max. |
| `name` | string |  | Name |
| `ruleset_id` | string |  | Compiled ruleset id: cc.level.<name> for platform groups (referenced by the site's cc_level and guard.default_cc_level); cc.user.r<id> for tenant groups (selectable as cc_level by the tenant's own sites; sites on a package need custom_cc_rule). (read-only) |
| `show_to` | integer[] |  | Tenants that can see a platform rule group; empty = visible to all tenants, otherwise only the listed tenants can see it and select it as a site's cc_level. Returned to admins only. |
| `sort` | integer |  | Panel sort order (smaller first). |
| `tenant_id` | integer |  | Owning tenant; null = platform object (built-in or created by an admin, read-only for tenants). (read-only) |
| `updated` | integer[] |  | IDs that were set (including those already in the target state), in request order. |
| `updated_at` | string |  | Updated at (read-only) |
| `visible` | boolean |  | Shown in the site's rule group drop-down. Default: true |

## Bans (`bans`)

Records of blocked IPs: bans created by hand, unban by IP or for a whole site with an unban history, and automatic bans reported by nodes.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/bans

### GET `/api/v2/bans`

List bans · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `ip` | string |  | A single address matches exactly; a CIDR returns every ban inside that network (single addresses and networks no shorter than it). |
| `site_id` | integer |  | Filter by site. |
| `active` | boolean |  | true = only bans that have not expired. |
| `level` | string |  | Filter by scope (global = global blacklist). Values: ["site","tenant","global"] |
| `source` | string |  | Filter by source: manual = created in the panel; node = banned automatically by a node (challenge limit exceeded, rule ban, handshake flood, etc.). Values: ["manual","node"] |
| `source_type` | string |  | Filter by source type (manual / cc / waf / scan / status / tls / intel). Accepts a comma-separated list (e.g. status,tls; duplicates are dropped); every item must be one of the seven values, otherwise 422. Omit for all. |
| `created_from` | string |  | Only bans created at or after this instant (e.g. "added today"). A repeat ban by a node extends the existing row and does not change its creation time, so only the first record counts. |

```sh
curl "https://panel.example.com/api/v2/bans" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/bans`

Create ban · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: tenant the ban belongs to. Required for level=tenant; optional for level=site (defaults to the site owner, and must match it when given). Ignored for tenant credentials |

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `expires_at` | string |  | Expiry time; null = permanent. |
| `ip` | string | Yes | IP or CIDR. |
| `level` | string | Yes | Scope: site = one site; tenant = all sites of the tenant; global = the whole network (admin only). Values: ["site","tenant","global"] |
| `reason` | string |  | Reason: manual by default for bans created in the panel; for automatic bans by a node, the triggering rule (ruleset/rule id such as cc.level.r5/r1) or module name (ssl_handshake = handshake flood), or auto when there is no rule. |
| `site_id` | integer |  | Site id, required when level=site; 0 otherwise. |

```sh
curl -X POST "https://panel.example.com/api/v2/bans" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ip":"198.51.100.7","level":"tenant"}'
```

### GET `/api/v2/bans/site-counts`

Count bans per site · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `source_type` | string |  | Filter by source type (manual / cc / waf / scan / status / tls / intel). Accepts a comma-separated list (e.g. status,tls; duplicates are dropped); every item must be one of the seven values, otherwise 422. Omit for all. |
| `created_from` | string |  | Only bans created at or after this instant (e.g. "added today"). A repeat ban by a node extends the existing row and does not change its creation time, so only the first record counts. |

```sh
curl "https://panel.example.com/api/v2/bans/site-counts" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/bans/source-counts`

Count bans per source type · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `created_from` | string |  | Only bans created at or after this instant (e.g. "added today"). A repeat ban by a node extends the existing row and does not change its creation time, so only the first record counts. |

```sh
curl "https://panel.example.com/api/v2/bans/source-counts" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/bans/unban`

Unban by IP · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `all` | boolean |  | true = unban the whole site, clearing every ban of site_id; level=site only and counts as one unban toward the daily limit. |
| `ips` | string[] |  | IPs/CIDRs to unban (omit when all=true, required otherwise); the number per request is limited by protect.unban_batch_max and the daily count per tenant by protect.unban_daily_max. |
| `level` | string | Yes | Scope: site = one site; tenant = all sites of the tenant; global = the whole network (admin only). Values: ["site","tenant","global"] |
| `site_id` | integer |  | Site id, required when level=site; 0 otherwise. |

```sh
curl -X POST "https://panel.example.com/api/v2/bans/unban" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ips":["198.51.100.7"],"level":"tenant"}'
```

### GET `/api/v2/bans/unban-quota`

Unban quota and today's usage · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/bans/unban-quota" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/bans/unbans`

List unban records · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `site_id` | integer |  | Filter by site (site-level unbans only). |
| `since` | string |  | Only records at or after this time (for example to count today's unbans). |
| `by_admin` | boolean |  | Filter by whether an administrator unbanned on the site owner's behalf; pass false to count the owner's own unbans today (those done by an administrator do not use the owner's quota). |

```sh
curl "https://panel.example.com/api/v2/bans/unbans" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/bans/{id}`

Delete ban · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/bans/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Bans object:

| Field | Type | Required | Description |
|---|---|---|---|
| `all` | boolean |  | true = unban the whole site, clearing every ban of site_id; level=site only and counts as one unban toward the daily limit. |
| `batch_max` | integer |  | Maximum IPs per unban request (setting protect.unban_batch_max). |
| `by_admin` | boolean |  | true = unbanned by an administrator on the site owner's behalf (recorded under and visible to the owner, does not use the owner's daily quota). |
| `count` | integer |  | Number of unexpired bans. |
| `created_at` | string |  | Created at (read-only) |
| `daily_max` | integer |  | Maximum unbans per tenant per day (setting protect.unban_daily_max). |
| `expires_at` | string |  | Expiry time; null = permanent. |
| `filter_id` | string |  | The filter (rule set or module) that triggered the ban, split from reason, such as cc.level.r5, waf, scan.builtin or ssl_handshake; absent for manual bans and for auto. (read-only) |
| `id` | integer |  | Record ID (read-only) |
| `intel` | BanIntel |  | Where a banned IP stands in the platform high-risk IP library right now (looked up against the current library when listing; fields the library does not carry, or entries already expired, are absent). |
| `intel_hit` | boolean |  | Whether the IP is in the platform high-risk IP library; null = not judged yet (always null until the library ships). |
| `ip` | string |  | IP or CIDR. |
| `ip_count` | integer |  | Number of unbanned IPs/CIDRs (0 for a whole-site unban). |
| `ips` | string[] |  | IPs/CIDRs to unban (omit when all=true, required otherwise); the number per request is limited by protect.unban_batch_max and the daily count per tenant by protect.unban_daily_max. |
| `items` | BanSourceCount[] |  | Items on this page |
| `level` | string |  | Scope: site = one site; tenant = all sites of the tenant; global = the whole network (admin only). Values: ["site","tenant","global"] |
| `reason` | string |  | Reason: manual by default for bans created in the panel; for automatic bans by a node, the triggering rule (ruleset/rule id such as cc.level.r5/r1) or module name (ssl_handshake = handshake flood), or auto when there is no rule. |
| `removed` | integer |  | Number of entries removed from the ban table at the same time. |
| `rule_id` | string |  | The rule number that triggered the ban, split from reason (r1 of cc.level.r5/r1, 941100 of waf:941100); absent when there is none. (read-only) |
| `site_id` | integer |  | Site id, required when level=site; 0 otherwise. |
| `source` | string |  | manual = created in the panel; node = banned automatically by a node and reported (one row per target while unexpired; a repeat ban extends it). Both are lifted the same way, by deleting or via /bans/unban. Values: ["manual","node"] (read-only) |
| `source_type` | BanSourceType |  | Source type: manual = created in the panel; cc = CC rule group (cc.level.*, cc.user.*, auto and other rule sets); waf = repeated WAF rule hits; scan = built-in scanner set; status = built-in status-code set; tls = handshake flood; intel = platform high-risk IP library. A ban made by a node is never manual. Values: ["manual","cc","waf","scan","status","tls","intel"] |
| `status` | string |  | Status of the push task pending/running/done/failed; null once the task has been purged. |
| `task_id` | integer |  | Id of the background task that pushes the change to nodes (poll progress with GET /tasks/{id}). |
| `tenant_id` | integer |  | Owning tenant; null = platform object (built-in or created by an admin, read-only for tenants). (read-only) |
| `updated_at` | string |  | Updated at (read-only) |
| `used` | integer |  | Unbans the tenant made itself today (since 00:00 UTC); unbans by an administrator do not count, and it is 0 when an administrator calls. |

## Site IP lists (`site-ip-lists`)

Permanent IP blacklist and whitelist of one site; blacklisted IPs are dropped and whitelisted IPs skip CC and other protections. The site's temporary whitelist on nodes can also be cleared.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/site-ip-lists

### GET `/api/v2/site-ip-lists/{site_id}`

Get site IP blacklist and whitelist · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/site-ip-lists/{site_id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/site-ip-lists/{site_id}`

Set site IP blacklist and whitelist · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `black` | string[] |  | Permanent blacklist (a match = connection dropped); the count is limited by protect.black_ip_max. |
| `black_items` | IpListItem[] |  | Blacklist entries with expiry, reason and source; expired entries are not returned. |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (when the read updated_at is null, i.e. never set, use 1970-01-01T00:00:00Z). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `white` | string[] |  | Permanent whitelist (a match = CC and other protections are skipped); the count is limited by protect.white_ip_max. |
| `white_items` | IpListItem[] |  | Whitelist entries with expiry, reason and source; expired entries are not returned. |

```sh
curl -X PUT "https://panel.example.com/api/v2/site-ip-lists/{site_id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"black":["198.51.100.0/24"],"white":["203.0.113.9"]}'
```

### GET `/api/v2/site-ip-lists/{site_id}/export`

Export site IP list entries · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `list` | string | Yes | Which list to export. Values: ["black","white"] |
| `source` | IpListSource |  | Only export entries from this source; omit for all. Values: ["manual","import","ip_detail","api"] |
| `format` | string |  | txt (default, one entry per line) or csv (with header, a source column and a UTF-8 BOM). Values: ["txt","csv"] Default: "txt" |

```sh
curl "https://panel.example.com/api/v2/site-ip-lists/{site_id}/export" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/site-ip-lists/{site_id}/import`

Bulk import site IP list entries · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `dry_run` | boolean |  | true only checks the file and saves nothing. Default: false |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (when the read updated_at is null, i.e. never set, use 1970-01-01T00:00:00Z). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `list` | string | Yes | Which list to import into. Values: ["black","white"] |
| `mode` | string |  | merge adds to the current list (the imported row wins for the same IP); replace swaps the whole list for the imported rows. Values: ["merge","replace"] Default: "merge" |
| `text` | string | Yes | File content (txt/csv), one entry per line: IP/CIDR[,expiry][,reason]. |

```sh
curl -X POST "https://panel.example.com/api/v2/site-ip-lists/{site_id}/import" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"dry_run":true,"list":"black","mode":"merge","text":"198.51.100.0/24,2030-01-01T00:00:00Z,scanner\n"}'
```

### GET `/api/v2/site-ip-lists/{site_id}/ip-detail`

IP detail · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `ip` | string | Yes | IPv4 or IPv6 address. |

```sh
curl "https://panel.example.com/api/v2/site-ip-lists/{site_id}/ip-detail" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/site-ip-lists/{site_id}/ip-detail`

Blacklist or whitelist from IP detail · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `expires_at` | string |  | Expiry time; empty = permanent. Must be in the future. |
| `ip` | string | Yes | Normalized address (IPv4-mapped addresses shown as IPv4). |
| `list` | string | Yes | Which list to import into. Values: ["black","white"] |
| `reason` | string |  | Reason (note), at most 200 characters. |
| `scope` | string | Yes | site = this site's list; global = the global list (admins only). Values: ["site","global"] |

```sh
curl -X POST "https://panel.example.com/api/v2/site-ip-lists/{site_id}/ip-detail" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/site-ip-lists/{site_id}/tmp-white-clear`

Clear site temporary whitelist · Authentication: User (some parameters admin only)

```sh
curl -X POST "https://panel.example.com/api/v2/site-ip-lists/{site_id}/tmp-white-clear" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Site IP lists object:

| Field | Type | Required | Description |
|---|---|---|---|
| `added` | integer |  | Entries added. |
| `applied` | boolean |  | Whether the import was saved (false for dry_run or when blocking issues exist). |
| `black` | string[] |  | Permanent blacklist (a match = connection dropped); the count is limited by protect.black_ip_max. |
| `black_items` | IpListItem[] |  | Blacklist entries with expiry, reason and source; expired entries are not returned. |
| `block_denied` | string |  | Why it cannot be blacklisted: private (private/loopback/link-local), node (a node IP; admins only), self (the caller's own IP), other (any other reason; tenants get this for node IPs so node addresses are not revealed); omitted when allowed. Values: ["private","node","self","other"] |
| `dry_run` | boolean |  | true only checks the file and saves nothing. Default: false |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (when the read updated_at is null, i.e. never set, use 1970-01-01T00:00:00Z). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `expires_at` | string |  | Expiry time; empty = permanent. Must be in the future. |
| `geo` | GeoipLookup |  | IP geolocation lookup result. |
| `global_black` | IpListItem |  | IP list entry (in IP details, the entry that matches the IP; omitted when none matches). |
| `global_white` | IpListItem |  | IP list entry (in IP details, the entry that matches the IP; omitted when none matches). |
| `ip` | string |  | Normalized address (IPv4-mapped addresses shown as IPv4). |
| `issues` | IpListImportIssue[] |  | Per-line issues (at most 1000, ascending by line). |
| `lines` | integer |  | Number of valid entries (after de-duplication). |
| `list` | string | Yes | Which list to import into. Values: ["black","white"] |
| `mode` | string |  | merge adds to the current list (the imported row wins for the same IP); replace swaps the whole list for the imported rows. Values: ["merge","replace"] Default: "merge" |
| `reason` | string |  | Reason (note), at most 200 characters. |
| `removed` | integer |  | In replace mode, existing entries that are not in the new list (entries with the same IP count as updated, not removed). |
| `scope` | string | Yes | site = this site's list; global = the global list (admins only). Values: ["site","global"] |
| `site_black` | IpListItem |  | IP list entry (in IP details, the entry that matches the IP; omitted when none matches). |
| `site_id` | integer |  | Site ID (read-only) |
| `site_white` | IpListItem |  | IP list entry (in IP details, the entry that matches the IP; omitted when none matches). |
| `task_id` | integer |  | Id of the background task that pushes the change to nodes (poll progress with GET /tasks/{id}). |
| `text` | string | Yes | File content (txt/csv), one entry per line: IP/CIDR[,expiry][,reason]. |
| `updated` | integer |  | Entries already in the list that were updated by the import. |
| `updated_at` | string |  | Updated at (read-only) |
| `white` | string[] |  | Permanent whitelist (a match = CC and other protections are skipped); the count is limited by protect.white_ip_max. |
| `white_items` | IpListItem[] |  | Whitelist entries with expiry, reason and source; expired entries are not returned. |

## Global IP lists (`global-ip-lists`)

Platform-wide IP blacklist and whitelist with optional expiry, reason and source per entry, plus bulk import and export; admin only.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/global-ip-lists

### GET `/api/v2/global-ip-lists`

Get global IP blacklist and whitelist · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/global-ip-lists" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/global-ip-lists`

Set global IP blacklist and whitelist · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `black` | string[] |  | IP/CIDR of the unexpired blacklist entries. |
| `black_items` | IpListItem[] |  | Blacklist entries with expiry, reason and source. |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (use 1970-01-01T00:00:00Z when it was null). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `white` | string[] |  | IP/CIDR of the unexpired whitelist entries. |
| `white_items` | IpListItem[] |  | Whitelist entries with expiry, reason and source. |

```sh
curl -X PUT "https://panel.example.com/api/v2/global-ip-lists" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"black_items":[{"expires_at":"2030-01-01T00:00:00Z","ip":"198.51.100.0/24","reason":"scanner"}]}'
```

### GET `/api/v2/global-ip-lists/export`

Export global IP list entries · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `list` | string | Yes | Which list to export. Values: ["black","white"] |
| `source` | IpListSource |  | Only export entries from this source; omit for all. Values: ["manual","import","ip_detail","api"] |
| `format` | string |  | txt (default, one entry per line) or csv (with header, a source column and a UTF-8 BOM). Values: ["txt","csv"] Default: "txt" |

```sh
curl "https://panel.example.com/api/v2/global-ip-lists/export" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/global-ip-lists/import`

Bulk import global IP list entries · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `dry_run` | boolean |  | true only checks the file and saves nothing. Default: false |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (use 1970-01-01T00:00:00Z when it was null). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `list` | string | Yes | Which list to import into. Values: ["black","white"] |
| `mode` | string |  | merge adds to the current list (the imported row wins for the same IP); replace swaps the whole list for the imported rows. Values: ["merge","replace"] Default: "merge" |
| `text` | string | Yes | File content (txt/csv), one entry per line: IP/CIDR[,expiry][,reason]. |

```sh
curl -X POST "https://panel.example.com/api/v2/global-ip-lists/import" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"dry_run":true,"list":"black","mode":"merge","text":"198.51.100.0/24,2030-01-01T00:00:00Z,scanner\n"}'
```

Global IP lists object:

| Field | Type | Required | Description |
|---|---|---|---|
| `added` | integer |  | Entries added. |
| `applied` | boolean |  | Whether the import was saved (false for dry_run or when blocking issues exist). |
| `black` | string[] |  | IP/CIDR of the unexpired blacklist entries. |
| `black_items` | IpListItem[] |  | Blacklist entries with expiry, reason and source. |
| `dry_run` | boolean |  | true only checks the file and saves nothing. Default: false |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (use 1970-01-01T00:00:00Z when it was null). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `issues` | IpListImportIssue[] |  | Per-line issues (at most 1000, ascending by line). |
| `lines` | integer |  | Number of valid entries (after de-duplication). |
| `list` | string | Yes | Which list to import into. Values: ["black","white"] |
| `mode` | string |  | merge adds to the current list (the imported row wins for the same IP); replace swaps the whole list for the imported rows. Values: ["merge","replace"] Default: "merge" |
| `removed` | integer |  | In replace mode, existing entries that are not in the new list (entries with the same IP count as updated, not removed). |
| `text` | string | Yes | File content (txt/csv), one entry per line: IP/CIDR[,expiry][,reason]. |
| `updated` | integer |  | Entries already in the list that were updated by the import. |
| `updated_at` | string |  | Updated at (read-only) |
| `white` | string[] |  | IP/CIDR of the unexpired whitelist entries. |
| `white_items` | IpListItem[] |  | Whitelist entries with expiry, reason and source. |

## CC settings (`cc-settings`)

Platform-level default CC rule group and automatic switching settings; admin only.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/cc-settings

### GET `/api/v2/cc-settings`

Get CC settings · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/cc-settings" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/cc-settings`

Update CC settings · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `auto_switch` | CcAutoSwitch | Yes | Settings for automatically switching to a stricter CC rule group. |
| `default_page_rule_id` | integer |  | Platform CC rule group id used by the default page (requests to unbound domains); null or omitted = no protection. |
| `default_rule_id` | integer | Yes | Platform CC rule group id used when a site has not chosen one; null = node default. |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (the version of the node.global config key, shared by CC and WAF settings; use 1970-01-01T00:00:00Z when the read value is null). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `updated_at` | string |  | Version in the response (updated_at of the node.global config key; null if never changed); ignored on update - use expected_updated_at for the optimistic lock. |

```sh
curl -X PUT "https://panel.example.com/api/v2/cc-settings" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"auto_switch":{"enabled":false,"hold_s":300,"qps_50x":10,"qps_total":500,"rule_id":2},"default_rule_id":10002}'
```

CC settings object:

| Field | Type | Required | Description |
|---|---|---|---|
| `auto_switch` | CcAutoSwitch |  | Settings for automatically switching to a stricter CC rule group. |
| `default_page_rule_id` | integer |  | Platform CC rule group id used by the default page (requests to unbound domains); null or omitted = no protection. |
| `default_rule_id` | integer |  | Platform CC rule group id used when a site has not chosen one; null = node default. |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (the version of the node.global config key, shared by CC and WAF settings; use 1970-01-01T00:00:00Z when the read value is null). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `updated_at` | string |  | Version in the response (updated_at of the node.global config key; null if never changed); ignored on update - use expected_updated_at for the optimistic lock. (read-only) |

## Bot settings (`bot-settings`)

Platform-wide bot management: the action per class (search engine / monitor and tool / automation tool / headless browser / malicious bot) and for fake crawlers, the challenge kind, behaviour score bands, protected endpoints and exceptions; admin only. Headless browser, malicious bot and fake crawler actions plus behaviour scoring belong to the paid feature WAF enhanced. Each site may override per class in the site config field bot.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/bot-settings

### GET `/api/v2/bot-settings`

Get bot settings · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/bot-settings" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/bot-settings`

Update bot settings · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `actions` | object |  | Action per class; defaults are allow for search engines and monitors/tools, log for automation tools, challenge for headless browsers and block for malicious bots and fake crawlers. headless, malicious and fake belong to WAF enhanced. |
| `challenge` | string |  | Challenge kind used when the action is challenge - js (invisible), slide, rotate, click or captcha (default js). Values: ["js","slide","rotate","click","captcha"] |
| `enabled` | boolean |  | Master switch; false = no site does bot detection. |
| `enhanced` | string |  | WAF enhanced state - full (active), readonly (expired but within the read-only buffer, License.readonly_days, 7 days by default; enhanced items still work but are read-only) or basic (not purchased or past the buffer; headless browsers, malicious bots and fake crawlers are treated as ordinary visitors and not scored, settings kept); ignored on update. Values: ["full","readonly","basic"] |
| `exceptions` | object[] |  | Exceptions - matching requests skip bot detection (at least one of path, cidr and ja4; several in one entry must all match). |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (the version of the node.global config key, shared with CC and WAF settings; use 1970-01-01T00:00:00Z when the read value is null). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `protect` | string[] |  | Protected endpoints (path prefixes such as /login, WAF enhanced) - requesting them without loading a page first adds to the score. |
| `score` | object |  | Behaviour score bands (WAF enhanced, 0-100) - below js pass; js to slide invisible challenge; slide to block challenge by kind; from block act as a malicious bot. Requires js < slide < block. A visitor who passes a challenge scores 0 for 30 minutes. |
| `updated_at` | string |  | Version in the response (updated_at of the node.global config key; null if never changed); ignored on update. |

```sh
curl -X PUT "https://panel.example.com/api/v2/bot-settings" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"actions":{"automation":"log","fake":"block","headless":"challenge","malicious":"block","monitor":"allow","search":"allow"},"challenge":"js","enabled":true,"exceptions":[{"cidr":"198.51.100.0/24","site_ids":[]}],"protect":["/login"],"score":{"block":85,"js":30,"kind":"slide","slide":60}}'
```

Bot settings object:

| Field | Type | Required | Description |
|---|---|---|---|
| `actions` | object |  | Action per class; defaults are allow for search engines and monitors/tools, log for automation tools, challenge for headless browsers and block for malicious bots and fake crawlers. headless, malicious and fake belong to WAF enhanced. |
| `challenge` | string |  | Challenge kind used when the action is challenge - js (invisible), slide, rotate, click or captcha (default js). Values: ["js","slide","rotate","click","captcha"] |
| `enabled` | boolean |  | Master switch; false = no site does bot detection. |
| `enhanced` | string |  | WAF enhanced state - full (active), readonly (expired but within the read-only buffer, License.readonly_days, 7 days by default; enhanced items still work but are read-only) or basic (not purchased or past the buffer; headless browsers, malicious bots and fake crawlers are treated as ordinary visitors and not scored, settings kept); ignored on update. Values: ["full","readonly","basic"] (read-only) |
| `exceptions` | object[] |  | Exceptions - matching requests skip bot detection (at least one of path, cidr and ja4; several in one entry must all match). |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (the version of the node.global config key, shared with CC and WAF settings; use 1970-01-01T00:00:00Z when the read value is null). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `protect` | string[] |  | Protected endpoints (path prefixes such as /login, WAF enhanced) - requesting them without loading a page first adds to the score. |
| `score` | object |  | Behaviour score bands (WAF enhanced, 0-100) - below js pass; js to slide invisible challenge; slide to block challenge by kind; from block act as a malicious bot. Requires js < slide < block. A visitor who passes a challenge scores 0 for 30 minutes. |
| `updated_at` | string |  | Version in the response (updated_at of the node.global config key; null if never changed); ignored on update. (read-only) |

## Bot stats (`bot-stats`)

Stats at the top of the bot management page and the per-class "requests today" column: today's requests, requests and action breakdown per bot class, number of sites with replay protection on, and node upgrade progress; tenants see their own sites only.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/bot-stats

### GET `/api/v2/bot-stats`

Get bot stats · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `site_id` | integer |  | Count this site only; omit for all sites. |

```sh
curl "https://panel.example.com/api/v2/bot-stats" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Bot stats object:

| Field | Type | Required | Description |
|---|---|---|---|
| `classes` | BotClassStat[] |  | Bot classes seen today with their action breakdown, in fixed class order (search, monitor, automation, headless, malicious, fake); classes not seen today are omitted; empty when the log database is unavailable. |
| `node_upgrade` | BotNodeUpgrade |  | Node upgrade progress - approved nodes whose reported version matches this master; visible to admins only, omitted from tenant responses. |
| `replay_sites` | BotReplaySites |  | Replay protection coverage. |
| `today_requests` | integer |  | All requests today; null when the log database is unavailable. |

## WAF settings (`waf-settings`)

Platform-wide WAF master switch, default blocking threshold and request body inspection limit; admin only. Per-site observe/block mode and exceptions live in the site config field waf.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/waf-settings

### GET `/api/v2/waf-settings`

Get WAF settings · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/waf-settings" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/waf-settings`

Update WAF settings · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `body_max_kb` | integer | Yes | Only the first this many KiB of the request body are inspected; 0 = body not inspected. |
| `enabled` | boolean | Yes | Platform-wide master switch; false = no site is inspected. |
| `engine_unavailable_nodes` | integer |  | Number of online primary nodes reporting the WAF engine (libinjection) unavailable (the sqli/xss rules do not take effect there); ignored on update. |
| `enhanced` | string |  | WAF enhanced state - full (active), readonly (expired but within the read-only buffer, License.readonly_days, 7 days by default; standard/strict level and semantic detection still work but are read-only) or basic (not purchased or past the buffer; not in effect, settings kept); ignored on update. Values: ["full","readonly","basic"] |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (the version of the node.global config key, shared by CC and WAF settings; use 1970-01-01T00:00:00Z when the read value is null). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `intel_share` | boolean |  | Threat intelligence sharing - anonymously report auto-banned risky IPs, upload-scan hits/false positives and new file fingerprints to the provider (records via GET /intel-reports). Omit to keep the stored value; on when never set (on by default; can be turned off anytime, reporting stops immediately). |
| `level` | string |  | Platform default protection level (used by sites that did not pick one); standard/strict are WAF enhanced and go through the enhanced gate (while read-only or not purchased the level can only be lowered, not raised). Omit to keep the stored value; basic when never set. Values: ["basic","standard","strict"] |
| `observe_days` | integer |  | Observation days for new rules/levels (log only, no blocking); 0 = no observation. Omit to keep the stored value; 3 when never set. |
| `rule_lib` | WafRuleLib |  | Rule library overview (read-only, ignored on update) - whether the paid feature rules_update is on, package version, counts per class and node alignment; without it nodes use the 23 built-in rules. Details via GET /waf-protections. |
| `scanner` | WafScanner |  | Scanner protection (scan section of the built-in guard) - master switch, action and ban duration, plus the five judging rules (any one triggers) each with its own switch and thresholds/custom lists. A rule that is switched off is not judged; custom paths live on the master and survive rule-pack updates. |
| `semantic` | WafSemantic |  | Semantic detection (WAF enhanced) - judges attacks by meaning on top of rule scoring; the node enforces it. |
| `threshold` | integer | Yes | Default blocking threshold (sum of anomaly scores of matched rules); a non-zero site waf.threshold takes precedence. |
| `trunc_action` | string |  | Action when part of a request was not inspected (rule 920901) - more than 1024 parameters, more than 256 headers, JSON/XML items over the limit, form fields over the budget, or a non-binary body larger than body_max_kb. Whatever can be re-checked is re-checked first; only what is still uncovered is handled by this setting. log only records (event tagged body_trunc) and never blocks; strict_block blocks at the strict level and only records at basic/standard; block blocks at every level. Default strict_block: at the strict level a non-binary body above body_max_kb (64 KB by default, e.g. a large JSON) is blocked by 920901 - raise body_max_kb, set log, or exempt 920901 in the site WAF exceptions. Note 920901 counts toward the repeated-hit ban (site waf.ban_on_repeat); on sites where it is enabled, client IPs that routinely send large JSON get banned. Sites in observe mode only record. Omit to keep the stored value; strict_block when never set. Values: ["log","strict_block","block"] |
| `updated_at` | string |  | Version in the response (updated_at of the node.global config key; null if never changed); ignored on update - use expected_updated_at for the optimistic lock. |

```sh
curl -X PUT "https://panel.example.com/api/v2/waf-settings" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"body_max_kb":64,"enabled":true,"intel_share":true,"level":"basic","observe_days":3,"scanner":{"action":"ban","ban_s":1800,"by_app_enabled":true,"decoys":{"enabled":true,"paths":[]},"enabled":true,"not_found":{"enabled":true,"max":30,"window_s":60},"sensitive":{"enabled":true,"extra_paths":[],"max":3},"ua_enabled":true},"semantic":{"enabled":false,"items":["sqli","xss"],"observe_days":3,"relation":"rules_first","sensitivity":"mid"},"threshold":5,"trunc_action":"strict_block"}'
```

WAF settings object:

| Field | Type | Required | Description |
|---|---|---|---|
| `body_max_kb` | integer |  | Only the first this many KiB of the request body are inspected; 0 = body not inspected. |
| `enabled` | boolean |  | Platform-wide master switch; false = no site is inspected. |
| `engine_unavailable_nodes` | integer |  | Number of online primary nodes reporting the WAF engine (libinjection) unavailable (the sqli/xss rules do not take effect there); ignored on update. (read-only) |
| `enhanced` | string |  | WAF enhanced state - full (active), readonly (expired but within the read-only buffer, License.readonly_days, 7 days by default; standard/strict level and semantic detection still work but are read-only) or basic (not purchased or past the buffer; not in effect, settings kept); ignored on update. Values: ["full","readonly","basic"] (read-only) |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (the version of the node.global config key, shared by CC and WAF settings; use 1970-01-01T00:00:00Z when the read value is null). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `intel_share` | boolean |  | Threat intelligence sharing - anonymously report auto-banned risky IPs, upload-scan hits/false positives and new file fingerprints to the provider (records via GET /intel-reports). Omit to keep the stored value; on when never set (on by default; can be turned off anytime, reporting stops immediately). |
| `level` | string |  | Platform default protection level (used by sites that did not pick one); standard/strict are WAF enhanced and go through the enhanced gate (while read-only or not purchased the level can only be lowered, not raised). Omit to keep the stored value; basic when never set. Values: ["basic","standard","strict"] |
| `observe_days` | integer |  | Observation days for new rules/levels (log only, no blocking); 0 = no observation. Omit to keep the stored value; 3 when never set. |
| `rule_lib` | WafRuleLib |  | Rule library overview (read-only, ignored on update) - whether the paid feature rules_update is on, package version, counts per class and node alignment; without it nodes use the 23 built-in rules. Details via GET /waf-protections. (read-only) |
| `scanner` | WafScanner |  | Scanner protection (scan section of the built-in guard) - master switch, action and ban duration, plus the five judging rules (any one triggers) each with its own switch and thresholds/custom lists. A rule that is switched off is not judged; custom paths live on the master and survive rule-pack updates. |
| `semantic` | WafSemantic |  | Semantic detection (WAF enhanced) - judges attacks by meaning on top of rule scoring; the node enforces it. |
| `threshold` | integer |  | Default blocking threshold (sum of anomaly scores of matched rules); a non-zero site waf.threshold takes precedence. |
| `trunc_action` | string |  | Action when part of a request was not inspected (rule 920901) - more than 1024 parameters, more than 256 headers, JSON/XML items over the limit, form fields over the budget, or a non-binary body larger than body_max_kb. Whatever can be re-checked is re-checked first; only what is still uncovered is handled by this setting. log only records (event tagged body_trunc) and never blocks; strict_block blocks at the strict level and only records at basic/standard; block blocks at every level. Default strict_block: at the strict level a non-binary body above body_max_kb (64 KB by default, e.g. a large JSON) is blocked by 920901 - raise body_max_kb, set log, or exempt 920901 in the site WAF exceptions. Note 920901 counts toward the repeated-hit ban (site waf.ban_on_repeat); on sites where it is enabled, client IPs that routinely send large JSON get banned. Sites in observe mode only record. Omit to keep the stored value; strict_block when never set. Values: ["log","strict_block","block"] |
| `updated_at` | string |  | Version in the response (updated_at of the node.global config key; null if never changed); ignored on update - use expected_updated_at for the optimistic lock. (read-only) |

## Upload scan settings (`upload-scan-settings`)

Platform-wide upload scanning (files in multipart form uploads only): master switch, dangerous extensions, double extensions, disguised files, webshell signatures, antivirus, per-file limit, scan timeout, action and exceptions; admin only. Webshell signatures, antivirus and the sha256 blocklist belong to the paid feature WAF enhanced.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/upload-scan-settings

### GET `/api/v2/upload-scan-settings`

Get upload scan settings · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/upload-scan-settings" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/upload-scan-settings`

Update upload scan settings · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | string |  | On a hit - block, log only, or observe for 3 days (log only, then block); dangerous files have no observation period. Values: ["block","log","observe"] |
| `disguise` | boolean |  | Disguised file detection (image extension but the magic bytes are a script or an executable). |
| `double_ext` | boolean |  | Double extension detection (e.g. a.php.jpg, a.asp;.jpg). |
| `enabled` | boolean |  | Master switch; false = no site is scanned. |
| `enhanced` | string |  | WAF enhanced state - full (active), readonly (expired but within the read-only buffer, License.readonly_days, 7 days by default; enhanced items still work but are read-only) or basic (not purchased or past the buffer; enhanced items are off, settings kept); ignored on update. Values: ["full","readonly","basic"] |
| `exceptions` | UploadScanException[] |  | Exceptions - skip the signature when the site and path prefix match (empty sig = the path is not scanned). Entries that tenants added by marking false positives (source=false_positive) must be sent back unchanged on replace, otherwise they are overwritten. |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (the version of the node.global config key, shared with CC and WAF settings; use 1970-01-01T00:00:00Z when the read value is null). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `exts` | string[] |  | Dangerous extensions (lowercase, without the dot); defaults to php phtml jsp jspx asp aspx ashx cer war sh cgi when omitted. A site may override with waf.upload_exts. |
| `max_file_mb` | integer |  | Per-file scan limit in MB (default 20); larger files only get the extension check and are handled per oversize. |
| `observe_until` | string |  | End of the observation period (set when action is observe); ignored on update, written by the server. |
| `on_parse_error` | string |  | On multipart parse errors (bad delimiter, oversized header, truncated body, too many parts) - block or allow (pass and log); default block. Values: ["allow","block"] |
| `on_timeout` | string |  | On scan timeout (or failure to read the body) - allow (pass and log) or block (default allow). Values: ["allow","block"] |
| `oversize` | string |  | Files over the limit - allow (pass and log) or block. Values: ["allow","block"] |
| `sha256` | object[] |  | Local sha256 blocklist (WAF enhanced). |
| `sig` | boolean |  | Webshell signatures (WAF enhanced). |
| `timeout_s` | integer |  | Scan timeout per request in seconds (default 2). |
| `updated_at` | string |  | Version in the response (updated_at of the node.global config key; null if never changed); ignored on update. |
| `virus` | string |  | Antivirus (ClamAV, WAF enhanced) - off, mem4g (nodes with at least 4 GB memory) or all nodes. Values: ["off","mem4g","all"] |

```sh
curl -X PUT "https://panel.example.com/api/v2/upload-scan-settings" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"action":"block","disguise":true,"double_ext":true,"enabled":true,"exceptions":[{"path":"/upload/avatar/","sig":"","site_id":0}],"exts":["php","jsp"],"max_file_mb":20,"on_parse_error":"block","on_timeout":"allow","oversize":"allow","sig":true,"timeout_s":2,"virus":"mem4g"}'
```

Upload scan settings object:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | string |  | On a hit - block, log only, or observe for 3 days (log only, then block); dangerous files have no observation period. Values: ["block","log","observe"] |
| `disguise` | boolean |  | Disguised file detection (image extension but the magic bytes are a script or an executable). |
| `double_ext` | boolean |  | Double extension detection (e.g. a.php.jpg, a.asp;.jpg). |
| `enabled` | boolean |  | Master switch; false = no site is scanned. |
| `enhanced` | string |  | WAF enhanced state - full (active), readonly (expired but within the read-only buffer, License.readonly_days, 7 days by default; enhanced items still work but are read-only) or basic (not purchased or past the buffer; enhanced items are off, settings kept); ignored on update. Values: ["full","readonly","basic"] (read-only) |
| `exceptions` | UploadScanException[] |  | Exceptions - skip the signature when the site and path prefix match (empty sig = the path is not scanned). Entries that tenants added by marking false positives (source=false_positive) must be sent back unchanged on replace, otherwise they are overwritten. |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (the version of the node.global config key, shared with CC and WAF settings; use 1970-01-01T00:00:00Z when the read value is null). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `exts` | string[] |  | Dangerous extensions (lowercase, without the dot); defaults to php phtml jsp jspx asp aspx ashx cer war sh cgi when omitted. A site may override with waf.upload_exts. |
| `max_file_mb` | integer |  | Per-file scan limit in MB (default 20); larger files only get the extension check and are handled per oversize. |
| `observe_until` | string |  | End of the observation period (set when action is observe); ignored on update, written by the server. (read-only) |
| `on_parse_error` | string |  | On multipart parse errors (bad delimiter, oversized header, truncated body, too many parts) - block or allow (pass and log); default block. Values: ["allow","block"] |
| `on_timeout` | string |  | On scan timeout (or failure to read the body) - allow (pass and log) or block (default allow). Values: ["allow","block"] |
| `oversize` | string |  | Files over the limit - allow (pass and log) or block. Values: ["allow","block"] |
| `sha256` | object[] |  | Local sha256 blocklist (WAF enhanced). |
| `sig` | boolean |  | Webshell signatures (WAF enhanced). |
| `timeout_s` | integer |  | Scan timeout per request in seconds (default 2). |
| `updated_at` | string |  | Version in the response (updated_at of the node.global config key; null if never changed); ignored on update. (read-only) |
| `virus` | string |  | Antivirus (ClamAV, WAF enhanced) - off, mem4g (nodes with at least 4 GB memory) or all nodes. Values: ["off","mem4g","all"] |

## Upload scan stats (`upload-scan-stats`)

Stats at the top of the upload scan page: today's scanned, blocked and over-limit unscanned counts, plus the list of sites that cannot be scanned; admin only.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/upload-scan-stats

### GET `/api/v2/upload-scan-stats`

Get upload scan stats · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/upload-scan-stats" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Upload scan stats object:

| Field | Type | Required | Description |
|---|---|---|---|
| `today_blocked` | integer |  | Uploads blocked by a scan hit today (excluding blocks caused by oversize, timeout or errors); null when the log database is unavailable. |
| `today_scanned` | integer |  | Upload requests scanned today (one request with several files counts once); null when the log database is unavailable. |
| `today_unscanned` | integer |  | Uploads left unscanned today because they exceeded the per-file limit; null when the log database is unavailable. |
| `unscanned_sites` | UploadScanStatsSite[] |  | Sites that cannot be scanned, ascending by site id, at most 200; when scanning is globally off, a single summary entry with reason=global_off (site_id 0). Online nodes whose WAF is switched off by the node-local out-of-band killswitch follow (reason=node_kill, site_id 0, name is the node name, at most 200, not counted in unscanned_total); omitted when scanning is globally off. |
| `unscanned_total` | integer |  | Total number of sites that cannot be scanned (not capped at 200); the number of all enabled sites when scanning is globally off. |

## High-risk IP library (`intel`)

The platform-provided high-risk IP library (paid item rules update, synced hourly): status, platform-wide handling policy, entry lookup, whitelisting a single entry and today's visits that hit it; read-only for tenants.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/intel

### GET `/api/v2/intel`

Get high-risk IP library status · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/intel" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/intel/allow`

Whitelist a high-risk IP · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `ip` | string | Yes | Address or network. |
| `reason` | string |  | Why it is whitelisted (kept on the whitelist entry); defaults to a fixed note when omitted. |

```sh
curl -X POST "https://panel.example.com/api/v2/intel/allow" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ip":"192.0.2.10"}'
```

### GET `/api/v2/intel/ips`

List high-risk IP library entries · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `ip` | string |  | A single address returns the entry covering it; anything else filters by substring of the CIDR text (admins only; tenants must give a single address). |
| `level` | IntelLevel |  | Filter by level. Values: ["high","medium"] |
| `reason` | IntelReason |  | Filter by type. Values: ["scanner","exploit","cc"] |

```sh
curl "https://panel.example.com/api/v2/intel/ips" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/intel/policy`

Update high-risk IP library policy · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | IntelAction | Yes | Handling: challenge (pass only after a JS check), block (ban on the site and answer 403), log (record only). Values: ["challenge","block","log"] |
| `level` | IntelLevel | Yes | Level: high (exploit type or score >= 80); medium. Values: ["high","medium"] |

```sh
curl -X PUT "https://panel.example.com/api/v2/intel/policy" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"action":"challenge","level":"high"}'
```

### GET `/api/v2/intel/visitors`

High-risk IPs that visited today · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `site_id` | integer |  | Filter by site. |

```sh
curl "https://panel.example.com/api/v2/intel/visitors" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

High-risk IP library object:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | IntelAction | Yes | Handling: challenge (pass only after a JS check), block (ban on the site and answer 403), log (record only). Values: ["challenge","block","log"] |
| `actions` | IntelAction[] |  | Handling applied to it today. |
| `added_at` | string |  | When the entry was added (kept when the same IP is written again). (read-only) |
| `cidr` | string |  | Address or network (canonical form). |
| `count` | integer |  | Number of entries. |
| `entry` | IntelEntry |  | One entry of the high-risk IP library. |
| `error` | string |  | Why the latest sync failed (the version in use is kept); empty on success. |
| `expires_at` | string |  | Expiry time; empty = permanent. An expired entry stops matching by itself (nodes judge by their own clock, independent of the master); the master then removes it and the next config push carries the removal. Must be in the future when written. |
| `first_seen_at` | string |  | When it first entered the library; absent when the library does not say. |
| `generated_at` | string |  | When the library was generated (on the super admin side). |
| `halt` | string |  | Why updates are halted by the license (expired, disabled, or the super admin unreachable for long) (admins only; not a failure, separate from error; absent when not halted). |
| `high` | integer |  | Number of high-risk entries. |
| `hits` | integer |  | Hits today. |
| `ip` | string | Yes | Address or network. |
| `last_at` | string |  | Time of the latest hit. |
| `level` | IntelLevel |  | Level: high (exploit type or score >= 80); medium. Values: ["high","medium"] |
| `masters` | integer |  | Number of controllers that reported it within 24 hours; absent when the library does not say. |
| `medium` | integer |  | Number of medium-risk entries. |
| `policy` | IntelPolicy |  | Platform-wide policy of the high-risk IP library; the site config field intel.action can override or turn it off. |
| `reason` | string |  | Why it is whitelisted (kept on the whitelist entry); defaults to a fixed note when omitted. |
| `reported_at` | string |  | When it was last reported. |
| `score` | integer |  | Risk score (1-100). |
| `serving` | boolean |  | Whether a library is being pushed to nodes (fetched at least once and not expired). |
| `sites` | integer |  | Number of sites involved. |
| `source` | IpListSource |  | Where the entry came from: manual (added by hand), import (bulk import), ip_detail (blacklisted/whitelisted from the IP detail page), api (written through the API). Values: ["manual","import","ip_detail","api"] |
| `stale` | boolean |  | The license no longer carries the library and the last version is over 24 hours old, so it is no longer served. |
| `sub` | string |  | Sub-type (for example WAF consecutive hits); absent when the library does not say. |
| `synced_at` | string |  | Time of the latest sync. |
| `version` | string |  | Version of the library being served; empty if none. |

## Value-added protection schemes (`protect-schemes`)

One scheme each for replay, data leak and web tamper protection (one per tenant; admins edit the platform scheme); saving also updates sites that follow the scheme, sites with their own settings are left alone; part of WAF Enhanced.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/protect-schemes

### GET `/api/v2/protect-schemes/leak`

Get the data leak protection scheme · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/protect-schemes/leak" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/protect-schemes/leak`

Save the data leak protection scheme · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `custom` | SiteLeakGuardCustom[] | Yes | Custom regular expressions, at most 16. |
| `detect` | SiteLeakGuardDetect | Yes | Action per built-in pattern; omit a pattern to not check it. |
| `exclude` | string[] | Yes | Path prefixes that are not checked (a trailing * is optional), at most 32. |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (when null, i.e. never saved, use 1970-01-01T00:00:00Z). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `observe_days` | integer | Yes | Days a site observes when turned on by the scheme (record only, no action); 0 = act right away. |

```sh
curl -X PUT "https://panel.example.com/api/v2/protect-schemes/leak" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/protect-schemes/replay`

Get the replay protection scheme · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/protect-schemes/replay" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/protect-schemes/replay`

Save the replay protection scheme · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | ReplayGuardAction |  | Without a token or on replay: block = 403; challenge = JS challenge. Omitted = block. Values: ["block","challenge"] |
| `app_salt` | string |  | Signing salt (app key, 32-byte hex); with app_sign on but no salt, signed headers are never accepted. |
| `app_sign` | boolean |  | App signed-header mode - requests carrying the signature headers are checked by signature, no token needed. |
| `bind_ip` | boolean |  | Also bind the token to the client IP (a new IP invalidates it). |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (when null, i.e. never saved, use 1970-01-01T00:00:00Z). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `methods` | ReplayGuardMethod[] |  | Protected methods; omitted = POST/PUT/PATCH/DELETE. |
| `paths` | ReplayGuardPath[] |  | Protected API paths, at most 64; empty = nothing protected. |
| `ttl_s` | integer |  | Token lifetime in seconds, also used for signed-header timestamps; omitted = 120. |

```sh
curl -X PUT "https://panel.example.com/api/v2/protect-schemes/replay" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/protect-schemes/tamper`

Get the web tamper protection scheme · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/protect-schemes/tamper" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/protect-schemes/tamper`

Save the web tamper protection scheme · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | ReplayGuardAction | Yes | Without a token or on replay: block = 403; challenge = JS challenge. Omitted = block. Values: ["block","challenge"] |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (when null, i.e. never saved, use 1970-01-01T00:00:00Z). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `mode` | string | Yes | What counts as tampering: structure = structural changes only; full = any change to the page. Values: ["structure","full"] |
| `observe_days` | integer | Yes | Days a site observes when turned on by the scheme (record only, no action); 0 = act right away. |
| `scopes` | SiteTamperScope[] | Yes | Protected scopes, at most 50. |

```sh
curl -X PUT "https://panel.example.com/api/v2/protect-schemes/tamper" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/protect-schemes/{kind}/apply`

Apply the scheme to sites in bulk · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `auto_join` | object |  | The rule that sites newly entering the range join the scheme automatically; omitted when off. |
| `dry_run` | boolean |  | When true, only return the counts of what would happen; nothing is written. Default: false |
| `override_custom` | boolean |  | When true, sites with their own settings are switched to the scheme too; by default they are left alone and counted in skipped_custom. Default: false |
| `selection` | ProtectSchemeSelection | Yes | The range of sites to apply to. |

```sh
curl -X POST "https://panel.example.com/api/v2/protect-schemes/{kind}/apply" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/protect-schemes/{kind}/sites`

List the sites that have this protection set up · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `q` | string |  | Fuzzy search by domain or site name (case-insensitive). |
| `status` | ProtectSchemeSiteStatus |  | Filter by status. Values: ["enabled","disabled","pending","capture_failed"] |
| `source` | ProtectSiteSource |  | Filter by source: scheme = follows the scheme; custom = set for the site alone (including turned off). Values: ["scheme","custom"] (read-only) |

```sh
curl "https://panel.example.com/api/v2/protect-schemes/{kind}/sites" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/protect-schemes/{kind}/sites/bulk`

Reset or turn off this protection for many sites · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | ReplayGuardAction | Yes | Without a token or on replay: block = 403; challenge = JS challenge. Omitted = block. Values: ["block","challenge"] |
| `filter` | ProtectSchemeSiteFilter |  | The same filters as the list; expanded to a set of sites by the server. |
| `ids` | integer[] |  | Site ids. |

```sh
curl -X POST "https://panel.example.com/api/v2/protect-schemes/{kind}/sites/bulk" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Value-added protection schemes object:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | ReplayGuardAction |  | Without a token or on replay: block = 403; challenge = JS challenge. Omitted = block. Values: ["block","challenge"] |
| `app_salt` | string |  | Signing salt (app key, 32-byte hex); with app_sign on but no salt, signed headers are never accepted. |
| `app_sign` | boolean |  | App signed-header mode - requests carrying the signature headers are checked by signature, no token needed. |
| `applied` | integer |  | Sites written (to be written on dry_run). |
| `auto_join` | object |  | The rule that sites newly entering the range join the scheme automatically; omitted when off. (read-only) |
| `bind_ip` | boolean |  | Also bind the token to the client IP (a new IP invalidates it). |
| `counts` | ProtectSchemeCounts |  | Site counts among the sites visible to the caller: total, turned on, following the scheme, and set alone. (read-only) |
| `custom` | SiteLeakGuardCustom[] |  | Custom regular expressions, at most 16. |
| `demoted_sites` | integer |  | Sites switched to their own settings with values kept because their owner lacks WAF Enhanced (only in the save response). (read-only) |
| `detect` | SiteLeakGuardDetect |  | Action per built-in pattern; omit a pattern to not check it. |
| `domain` | string |  | Primary domain (the site name when there is none). |
| `dry_run` | boolean |  | When true, only return the counts of what would happen; nothing is written. Default: false |
| `enabled` | boolean |  | Whether it is on. |
| `exclude` | string[] |  | Path prefixes that are not checked (a trailing * is optional), at most 32. |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (when null, i.e. never saved, use 1970-01-01T00:00:00Z). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `failed` | ProtectSchemeFailure[] |  | Sites that could not be handled, with the reason; at most 100. |
| `failed_total` | integer |  | Total sites that could not be handled (larger than failed when truncated). |
| `filter` | ProtectSchemeSiteFilter |  | The same filters as the list; expanded to a set of sites by the server. |
| `gate` | ProtectSchemeGate |  | WAF Enhanced status of the scheme owner (the tenant or the platform): full = editable; readonly and basic = may only be reduced. Values: ["full","readonly","basic"] (read-only) |
| `ids` | integer[] |  | Site ids. |
| `matched` | integer |  | Sites in the range the caller can see. |
| `methods` | ReplayGuardMethod[] |  | Protected methods; omitted = POST/PUT/PATCH/DELETE. |
| `mode` | string |  | What counts as tampering: structure = structural changes only; full = any change to the page. Values: ["structure","full"] |
| `observe_days` | integer |  | Days a site observes when turned on by the scheme (record only, no action); 0 = act right away. |
| `observe_until` | integer |  | End of the observation period (Unix milliseconds); set only for leak protection that is on and still observing, null when expired, never observing, off, or for other protections. |
| `override_custom` | boolean |  | When true, sites with their own settings are switched to the scheme too; by default they are left alone and counted in skipped_custom. Default: false |
| `owner` | string |  | Owner username. |
| `paths` | ReplayGuardPath[] |  | Protected API paths, at most 64; empty = nothing protected. |
| `scopes` | SiteTamperScope[] |  | Protected scopes, at most 50. |
| `selection` | ProtectSchemeSelection | Yes | The range of sites to apply to. |
| `site_id` | integer |  | Site id. |
| `skipped_custom` | integer |  | Sites left alone because they have their own settings (override_custom is false). |
| `skipped_gate` | integer |  | Sites not written because their owner's WAF Enhanced is not full. |
| `source` | ProtectSiteSource |  | Where the site's setting comes from: scheme = follows the scheme (changes when the scheme is saved); custom = set for this site alone (including turned off and rows migrated from before). Values: ["scheme","custom"] (read-only) |
| `status` | ProtectSchemeSiteStatus |  | Status of the site's setting: enabled; disabled; pending = on but the tamper baseline is still queued; capture_failed = on but baseline capture failed repeatedly and stopped retrying (the last two exist for web tamper protection only). Values: ["enabled","disabled","pending","capture_failed"] |
| `tenant_id` | integer |  | Owner user id. |
| `today_blocked` | integer |  | Block and error-page count today (UTC), excluding hits during the observation period (leak protection only, same basis as counts.today_blocked in the scheme response); null for other protections, or when ClickHouse is unavailable or the query fails. |
| `today_hits` | integer |  | Hits today (data leak protection = masked plus blocked outside the observation period, i.e. today_masked + today_blocked; web tamper protection = pages found tampered today); null for replay protection. |
| `today_masked` | integer |  | Masking count today (UTC), excluding hits during the observation period (leak protection only, same basis as counts.today_masked in the scheme response); null for other protections, or when ClickHouse is unavailable or the query fails. |
| `ttl_s` | integer |  | Token lifetime in seconds, also used for signed-header timestamps; omitted = 120. |
| `updated_at` | string |  | Version (null if never saved); use expected_updated_at on update. (read-only) |
| `updated_sites` | integer |  | Sites following the scheme that this save updated (only in the save response). (read-only) |

## Replay protection (`site-replay-guards`)

Replay protection of one site: protected APIs need a one-time token or the app signature headers, otherwise the request is blocked or challenged; can be set for the site alone or restored to the scheme; part of WAF Enhanced.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/site-replay-guards

### GET `/api/v2/site-replay-guards/{site_id}`

Get site replay protection settings · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/site-replay-guards/{site_id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/site-replay-guards/{site_id}`

Set site replay protection · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | ReplayGuardAction |  | Without a token or on replay: block = 403; challenge = JS challenge. Omitted = block. Values: ["block","challenge"] |
| `app_salt` | string |  | Signing salt (app key, 32-byte hex); with app_sign on but no salt, signed headers are never accepted. |
| `app_sign` | boolean |  | App signed-header mode - requests carrying the signature headers are checked by signature, no token needed. |
| `bind_ip` | boolean |  | Also bind the token to the client IP (a new IP invalidates it). |
| `enabled` | boolean | Yes | Whether enabled. |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (when null, i.e. never set, use 1970-01-01T00:00:00Z). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `methods` | ReplayGuardMethod[] |  | Protected methods; omitted = POST/PUT/PATCH/DELETE. |
| `paths` | ReplayGuardPath[] |  | Protected API paths, at most 64; empty = nothing protected. |
| `ttl_s` | integer |  | Token lifetime in seconds, also used for signed-header timestamps; omitted = 120. |

```sh
curl -X PUT "https://panel.example.com/api/v2/site-replay-guards/{site_id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/site-replay-guards/{site_id}`

Restore site replay protection to the scheme · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/site-replay-guards/{site_id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Replay protection object:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | ReplayGuardAction |  | Without a token or on replay: block = 403; challenge = JS challenge. Omitted = block. Values: ["block","challenge"] |
| `app_salt` | string |  | Signing salt (app key, 32-byte hex); with app_sign on but no salt, signed headers are never accepted. |
| `app_sign` | boolean |  | App signed-header mode - requests carrying the signature headers are checked by signature, no token needed. |
| `bind_ip` | boolean |  | Also bind the token to the client IP (a new IP invalidates it). |
| `enabled` | boolean |  | Whether enabled. |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (when null, i.e. never set, use 1970-01-01T00:00:00Z). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `gate` | string |  | WAF Enhanced status of the site's tenant: full = editable; readonly = expired but within the read-only buffer (License.readonly_days, 7 days by default), still in effect but read-only; basic = not subscribed or past the buffer, not in effect and settings kept. Values: ["full","readonly","basic"] (read-only) |
| `gate_expired_at` | string |  | Administrator responses with gate_reason=expired only - when the platform WAF Enhanced license expired; not present in tenant responses. (read-only) |
| `gate_owner` | WafGateOwner |  | The owner of the site, in administrator responses only; tenant responses carry no one else's information. (read-only) |
| `gate_packages` | string[] |  | Administrator responses only - names of the owner's unexpired packages (at most 20); not present in tenant responses. (read-only) |
| `gate_reason` | WafGateReason |  | Why WAF Enhanced (waf_enhanced) is not in effect; absent when gate is full or readonly. package = the platform is licensed but the owner has no unexpired package that includes WAF Enhanced; license = the platform has not enabled it (never licensed, revoked by the provider, or the license was disabled); expired = the platform license has expired and its read-only buffer period (License.readonly_days, default 7 days) has passed, so it fell back to basic. Values: ["package","license","expired"] (read-only) |
| `methods` | ReplayGuardMethod[] |  | Protected methods; omitted = POST/PUT/PATCH/DELETE. |
| `paths` | ReplayGuardPath[] |  | Protected API paths, at most 64; empty = nothing protected. |
| `site_id` | integer |  | Site ID (read-only) |
| `source` | ProtectSiteSource |  | Where the site's setting comes from: scheme = follows the scheme (changes when the scheme is saved); custom = set for this site alone (including turned off and rows migrated from before). Values: ["scheme","custom"] (read-only) |
| `ttl_s` | integer |  | Token lifetime in seconds, also used for signed-header timestamps; omitted = 120. |
| `updated_at` | string |  | Version (null if never set); use expected_updated_at on update. (read-only) |

## Data leak protection (`site-leak-guards`)

Data leak protection of one site: checks pages, JSON and text returned by the origin for phone numbers, ID card numbers, bank card numbers, keys and error messages, and masks, blocks or replaces them with an error page; part of WAF Enhanced.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/site-leak-guards

### GET `/api/v2/site-leak-guards/{site_id}`

Get site data leak protection settings · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/site-leak-guards/{site_id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/site-leak-guards/{site_id}`

Set site data leak protection · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `custom` | SiteLeakGuardCustom[] |  | Custom regular expressions, at most 16. |
| `detect` | SiteLeakGuardDetect |  | Action per built-in pattern; omit a pattern to not check it. |
| `enabled` | boolean | Yes | Whether enabled. |
| `exclude` | string[] |  | Path prefixes that are not checked, at most 32. |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (when null, i.e. never set, use 1970-01-01T00:00:00Z). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `observe_days` | integer |  | Days observed after turning on. |

```sh
curl -X PUT "https://panel.example.com/api/v2/site-leak-guards/{site_id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/site-leak-guards/{site_id}`

Restore site data leak protection to the scheme · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/site-leak-guards/{site_id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Data leak protection object:

| Field | Type | Required | Description |
|---|---|---|---|
| `custom` | SiteLeakGuardCustom[] |  | Custom regular expressions, at most 16. |
| `detect` | SiteLeakGuardDetect |  | Action per built-in pattern; omit a pattern to not check it. |
| `enabled` | boolean |  | Whether enabled. |
| `exclude` | string[] |  | Path prefixes that are not checked, at most 32. |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (when null, i.e. never set, use 1970-01-01T00:00:00Z). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `gate` | string |  | WAF Enhanced status of the site's tenant: full = editable; readonly = expired but within the read-only buffer (License.readonly_days, 7 days by default), still in effect but read-only; basic = not subscribed or past the buffer, nothing checked and settings kept. Values: ["full","readonly","basic"] (read-only) |
| `gate_expired_at` | string |  | Administrator responses with gate_reason=expired only - when the platform WAF Enhanced license expired; not present in tenant responses. (read-only) |
| `gate_owner` | WafGateOwner |  | The owner of the site, in administrator responses only; tenant responses carry no one else's information. (read-only) |
| `gate_packages` | string[] |  | Administrator responses only - names of the owner's unexpired packages (at most 20); not present in tenant responses. (read-only) |
| `gate_reason` | WafGateReason |  | Why WAF Enhanced (waf_enhanced) is not in effect; absent when gate is full or readonly. package = the platform is licensed but the owner has no unexpired package that includes WAF Enhanced; license = the platform has not enabled it (never licensed, revoked by the provider, or the license was disabled); expired = the platform license has expired and its read-only buffer period (License.readonly_days, default 7 days) has passed, so it fell back to basic. Values: ["package","license","expired"] (read-only) |
| `observe_days` | integer |  | Days observed after turning on. |
| `observe_until` | string |  | End of the observation period; null = not observing (never on, or 0 days chosen). (read-only) |
| `site_id` | integer |  | Site ID (read-only) |
| `source` | ProtectSiteSource |  | Where the site's setting comes from: scheme = follows the scheme (changes when the scheme is saved); custom = set for this site alone (including turned off and rows migrated from before). Values: ["scheme","custom"] (read-only) |
| `updated_at` | string |  | Version (null if never set); use expected_updated_at on update. (read-only) |

## Network-wide URL protection settings (`url-guard-settings`)

Platform-wide default switch and thresholds for network-wide URL protection; admin only. Per-site switch and thresholds live in the site security settings.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/url-guard-settings

### GET `/api/v2/url-guard-settings`

Get network-wide URL protection settings · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/url-guard-settings" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/url-guard-settings`

Update network-wide URL protection settings · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | string | Yes | Action on the URL once triggered - challenge, block or per-IP rate limit. Values: ["challenge","block","limit"] |
| `baseline_ratio` | integer |  | Triggering also requires this many times the usual level (usual = the URL's average QPS over the past hour, excluding the last minute). The panel asks for at least 1, default 5; 0 = ignore the multiple (legacy configs lack the field and keep the old rule). Applies to new triggers only - renewing an active rule looks at QPS alone. Omit on update to keep the stored value. |
| `enabled` | boolean | Yes | Global default switch, used by sites that do not set their own. |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (use 1970-01-01T00:00:00Z when the read value is null). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `gate` | string |  | WAF enhanced (waf_enhanced) gate - full (active), readonly (expired but within the read-only buffer, License.readonly_days, 7 days by default; still effective, settings read-only), basic (not purchased or fallen back, nothing is pushed); ignored on update. Values: ["full","readonly","basic"] |
| `gate_expired_at` | string |  | Administrator responses with gate_reason=expired only - when the platform WAF Enhanced license expired; not present in tenant responses. |
| `gate_owner` | WafGateOwner |  | The owner of the site, in administrator responses only; tenant responses carry no one else's information. |
| `gate_packages` | string[] |  | Administrator responses only - names of the owner's unexpired packages (at most 20); not present in tenant responses. |
| `gate_reason` | WafGateReason |  | Why WAF Enhanced (waf_enhanced) is not in effect; absent when gate is full or readonly. package = the platform is licensed but the owner has no unexpired package that includes WAF Enhanced; license = the platform has not enabled it (never licensed, revoked by the provider, or the license was disabled); expired = the platform license has expired and its read-only buffer period (License.readonly_days, default 7 days) has passed, so it fell back to basic. Values: ["package","license","expired"] |
| `hold_s` | integer | Yes | Seconds the rule is held; it is renewed while still over the threshold and lapses once traffic drops. |
| `kind` | string |  | Challenge type for action=challenge (default js); must be empty for other actions. Values: ["js","redirect","delay_jump","cookie","captcha","slide","click","rotate"] |
| `max_per_site` | integer | Yes | Max rules in effect at the same time per site. |
| `qps` | integer | Yes | Trigger only when the network-wide total QPS of a URL is at least this (and at least baseline_ratio times its usual level). |
| `rate` | integer |  | Max requests per IP per 10 seconds for action=limit; must be 0 for other actions. |
| `updated_at` | string |  | Version in the response (null if never changed); ignored on update - use expected_updated_at for the optimistic lock. |

```sh
curl -X PUT "https://panel.example.com/api/v2/url-guard-settings" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"action":"challenge","baseline_ratio":5,"enabled":true,"hold_s":600,"kind":"js","max_per_site":20,"qps":300,"rate":20}'
```

Network-wide URL protection settings object:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | string |  | Action on the URL once triggered - challenge, block or per-IP rate limit. Values: ["challenge","block","limit"] |
| `baseline_ratio` | integer |  | Triggering also requires this many times the usual level (usual = the URL's average QPS over the past hour, excluding the last minute). The panel asks for at least 1, default 5; 0 = ignore the multiple (legacy configs lack the field and keep the old rule). Applies to new triggers only - renewing an active rule looks at QPS alone. Omit on update to keep the stored value. |
| `enabled` | boolean |  | Global default switch, used by sites that do not set their own. |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (use 1970-01-01T00:00:00Z when the read value is null). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `gate` | string |  | WAF enhanced (waf_enhanced) gate - full (active), readonly (expired but within the read-only buffer, License.readonly_days, 7 days by default; still effective, settings read-only), basic (not purchased or fallen back, nothing is pushed); ignored on update. Values: ["full","readonly","basic"] (read-only) |
| `gate_expired_at` | string |  | Administrator responses with gate_reason=expired only - when the platform WAF Enhanced license expired; not present in tenant responses. (read-only) |
| `gate_owner` | WafGateOwner |  | The owner of the site, in administrator responses only; tenant responses carry no one else's information. (read-only) |
| `gate_packages` | string[] |  | Administrator responses only - names of the owner's unexpired packages (at most 20); not present in tenant responses. (read-only) |
| `gate_reason` | WafGateReason |  | Why WAF Enhanced (waf_enhanced) is not in effect; absent when gate is full or readonly. package = the platform is licensed but the owner has no unexpired package that includes WAF Enhanced; license = the platform has not enabled it (never licensed, revoked by the provider, or the license was disabled); expired = the platform license has expired and its read-only buffer period (License.readonly_days, default 7 days) has passed, so it fell back to basic. Values: ["package","license","expired"] (read-only) |
| `hold_s` | integer |  | Seconds the rule is held; it is renewed while still over the threshold and lapses once traffic drops. |
| `kind` | string |  | Challenge type for action=challenge (default js); must be empty for other actions. Values: ["js","redirect","delay_jump","cookie","captcha","slide","click","rotate"] |
| `max_per_site` | integer |  | Max rules in effect at the same time per site. |
| `qps` | integer |  | Trigger only when the network-wide total QPS of a URL is at least this (and at least baseline_ratio times its usual level). |
| `rate` | integer |  | Max requests per IP per 10 seconds for action=limit; must be 0 for other actions. |
| `updated_at` | string |  | Version in the response (null if never changed); ignored on update - use expected_updated_at for the optimistic lock. (read-only) |

## Network-wide URL protection (`url-guard`)

Temporary challenge/rate-limit/block rules pushed for a URL once its cross-node request volume exceeds the threshold, revoked automatically on expiry; read-only view of the entries in effect.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/url-guard

### GET `/api/v2/url-guard/active`

List URL protections in effect · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/url-guard/active" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Network-wide URL protection object:

| Field | Type | Required | Description |
|---|---|---|---|
| `items` | UrlGuardActive[] |  | Rules in effect, newest first. |

## Threat intelligence reports (`intel-reports`)

Batches of threat intelligence this master reports anonymously to the vendor (auto-banned high-risk IPs, upload scan hits and false positives, newly seen file fingerprints), one batch every 5 minutes; nothing is reported while intelligence sharing is off. Admin only.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/intel-reports

### GET `/api/v2/intel-reports`

List intelligence reports · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `type` | IntelReportType |  | Filter by type (ip / upload_hit / false_positive / sha256_report / samples); omit for all. Values: ["ip","upload_hit","false_positive","sha256_report","samples"] |

```sh
curl "https://panel.example.com/api/v2/intel-reports" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Threat intelligence reports object:

| Field | Type | Required | Description |
|---|---|---|---|
| `created_at` | string |  | Batch time. |
| `error` | string |  | Failure reason; empty on success. Consecutive failures keep only the latest. |
| `id` | integer |  | Record ID |
| `items` | integer |  | Number of items in the batch. |
| `result` | string |  | Result - ok delivered; failed not delivered (items stay queued for the next round). Values: ["ok","failed"] |
| `type` | IntelReportType |  | Intelligence type - risky IP / upload hit / false positive marked by an admin / newly seen file fingerprint / suspicious request samples. Values: ["ip","upload_hit","false_positive","sha256_report","samples"] |

## Web tamper protection (`site-tamper-guards`)

Web tamper protection of one site: protected scopes (home page, pages, directories), what counts as tampering (structure only / any change), serving the baseline page or alerting only, and the observation period; baseline update and diff; part of WAF Enhanced.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/site-tamper-guards

### GET `/api/v2/site-tamper-guards/{site_id}`

Get site web tamper protection settings · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/site-tamper-guards/{site_id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/site-tamper-guards/{site_id}`

Set site web tamper protection · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | string |  | Action on tampering. Values: ["restore","log"] |
| `enabled` | boolean | Yes | Whether enabled. |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (when null, i.e. never set, use 1970-01-01T00:00:00Z). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `mode` | string |  | What counts as tampering. Values: ["structure","full"] |
| `observe_days` | integer |  | Days observed after turning on. |
| `scopes` | SiteTamperScope[] |  | Protected scopes. |

```sh
curl -X PUT "https://panel.example.com/api/v2/site-tamper-guards/{site_id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/site-tamper-guards/{site_id}`

Restore site web tamper protection to the scheme · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/site-tamper-guards/{site_id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/site-tamper-guards/{site_id}/diff`

Get site web tamper protection diff · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `path` | string | Yes | Baseline page path (e.g. /, /about.html). |

```sh
curl "https://panel.example.com/api/v2/site-tamper-guards/{site_id}/diff" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/site-tamper-guards/{site_id}/rebase`

Update site web tamper protection baseline · Authentication: User (some parameters admin only)

```sh
curl -X POST "https://panel.example.com/api/v2/site-tamper-guards/{site_id}/rebase" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Web tamper protection object:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | string |  | Action on tampering. Values: ["restore","log"] |
| `assets` | SiteTamperAsset[] |  | JS/CSS whose fingerprint differs (recorded only, not replaced). |
| `base_at` | string |  | Baseline time (the last baseline update). (read-only) |
| `base_ver` | integer |  | Baseline version, +1 on every baseline update; 0 = no baseline yet. (read-only) |
| `changes` | SiteTamperChange[] |  | Structural changes (removed links and text changes are not listed). |
| `enabled` | boolean |  | Whether enabled. |
| `expected_updated_at` | string |  | Optimistic lock - the updated_at as the caller read it (when null, i.e. never set, use 1970-01-01T00:00:00Z). If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `fetch_error` | string |  | Why the page could not be fetched now (changes is then empty): fetch_failed = the origin did not return 200 or could not be reached; not_html = not a web page; too_large = over 1 MB. Absent when fetched. Values: ["fetch_failed","not_html","too_large"] |
| `gate` | string |  | WAF Enhanced status of the site's tenant: full = editable; readonly = expired but within the read-only buffer (License.readonly_days, 7 days by default), still in effect but read-only; basic = not subscribed or past the buffer, nothing checked and settings kept. Values: ["full","readonly","basic"] (read-only) |
| `gate_expired_at` | string |  | Administrator responses with gate_reason=expired only - when the platform WAF Enhanced license expired; not present in tenant responses. (read-only) |
| `gate_owner` | WafGateOwner |  | The owner of the site, in administrator responses only; tenant responses carry no one else's information. (read-only) |
| `gate_packages` | string[] |  | Administrator responses only - names of the owner's unexpired packages (at most 20); not present in tenant responses. (read-only) |
| `gate_reason` | WafGateReason |  | Why WAF Enhanced (waf_enhanced) is not in effect; absent when gate is full or readonly. package = the platform is licensed but the owner has no unexpired package that includes WAF Enhanced; license = the platform has not enabled it (never licensed, revoked by the provider, or the license was disabled); expired = the platform license has expired and its read-only buffer period (License.readonly_days, default 7 days) has passed, so it fell back to basic. Values: ["package","license","expired"] (read-only) |
| `mode` | string |  | What counts as tampering. Values: ["structure","full"] |
| `observe_days` | integer |  | Days observed after turning on. |
| `observe_until` | string |  | End of the observation period; null = not observing (never on, or 0 days chosen). (read-only) |
| `pages` | SiteTamperPage[] |  | Baseline pages (sorted by path), including scopes that were not included and why. (read-only) |
| `path` | string |  | Page path. |
| `scopes` | SiteTamperScope[] |  | Protected scopes. |
| `site_id` | integer |  | Site ID (read-only) |
| `source` | ProtectSiteSource |  | Where the site's setting comes from: scheme = follows the scheme (changes when the scheme is saved); custom = set for this site alone (including turned off and rows migrated from before). Values: ["scheme","custom"] (read-only) |
| `tampered` | boolean |  | Whether this counts as tampering under the site's mode. |
| `updated_at` | string |  | Version (null if never set); use expected_updated_at on update. (read-only) |

## Web tamper protection page overview (`tamper-guard`)

All protected pages across sites (site, path, scope, baseline time, state, last change) without picking a site first; tenants see their own sites only and may filter by site.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/tamper-guard

### GET `/api/v2/tamper-guard/pages`

List protected pages of all sites · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `site_id` | integer |  | Only this site; an empty list when the site is not the caller's. |
| `q` | string |  | Fuzzy search by site domain or page path (case-insensitive). |
| `changed` | boolean |  | When true, only pages with a detected tampering (a non-empty last change). |

```sh
curl "https://panel.example.com/api/v2/tamper-guard/pages" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Web tamper protection page overview object:

| Field | Type | Required | Description |
|---|---|---|---|
| `base_at` | string |  | When the site's baseline was last updated (whole site); null if never. |
| `domain` | string |  | Primary domain of the site (first in its domain list; the site name when it has none). |
| `enabled` | boolean |  | Whether tamper protection is switched on for the site; when off the pages are only a kept baseline and nothing is checked. |
| `gate` | string |  | WAF Enhanced status of the site's tenant: full = baseline can be updated; readonly and basic = cannot. Values: ["full","readonly","basic"] |
| `gate_expired_at` | string |  | Administrator responses with gate_reason=expired only - when the platform WAF Enhanced license expired; not present in tenant responses. (read-only) |
| `gate_owner` | WafGateOwner |  | The owner of the site, in administrator responses only; tenant responses carry no one else's information. (read-only) |
| `gate_packages` | string[] |  | Administrator responses only - names of the owner's unexpired packages (at most 20); not present in tenant responses. (read-only) |
| `gate_reason` | WafGateReason |  | Why WAF Enhanced (waf_enhanced) is not in effect; absent when gate is full or readonly. package = the platform is licensed but the owner has no unexpired package that includes WAF Enhanced; license = the platform has not enabled it (never licensed, revoked by the provider, or the license was disabled); expired = the platform license has expired and its read-only buffer period (License.readonly_days, default 7 days) has passed, so it fell back to basic. Values: ["package","license","expired"] (read-only) |
| `kind` | string |  | Which kind of scope the page comes from. Values: ["home","page","dir"] |
| `last_change_action` | string |  | What the node did the last time: replace = served the baseline page; log = recorded only (including the observation period). Values: ["replace","log"] |
| `last_change_at` | string |  | When tampering was last detected; null = none since the baseline. |
| `last_change_summary` | string |  | Summary of the last change, e.g. link=bad.example.net, title. |
| `note` | string |  | Why the page is not in the baseline, same values as SiteTamperPage.note; absent for included pages. Values: ["too_large","site_limit","fetch_failed","not_html"] |
| `observe_until` | string |  | End of the site's tamper-protection observation period; set only while protection is on and observing (same as SiteTamperGuard.observe_until), null when off or never observing; an already-passed deadline is returned as is, the caller compares it with the current time. |
| `path` | string |  | Page path. |
| `site_id` | integer |  | Site id. |
| `size` | integer |  | Size of the baseline page in bytes. |
| `state` | string |  | Page state: normal; tampered = tampering detected since the baseline; excluded = not in the baseline (see note). Values: ["normal","tampered","excluded"] |

## Packages (`packages`)

Base packages for sale, defining quotas such as traffic, bandwidth and domains, and prices.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/packages

### GET `/api/v2/packages`

List packages · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |

```sh
curl "https://panel.example.com/api/v2/packages" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/packages`

Create package · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `backup_group_id` | integer |  | Backup node group; null = none. |
| `bandwidth_mbps` | integer |  | Bandwidth in Mbps, -1 = unlimited. |
| `buy_limit` | integer |  | Maximum copies a tenant may hold, -1 = unlimited. |
| `cname_domain_id` | integer |  | CNAME root domain; null = the package binds no lines. |
| `cname_host_suffix` | string |  | Suffix for newly generated host records; empty = only the 8-character random name. |
| `cname_mode` | PackageCnameMode |  | CNAME mode. site = one access name per site or stream; package = sites and streams of a purchased package share one access name. Values: ["site","package"] |
| `connections` | integer |  | Concurrent connections, -1 = unlimited. |
| `custom_cc_rule` | boolean |  | Allows custom CC rules. |
| `des` | string |  | Description |
| `domains` | integer |  | Number of domains (each IP site counts as 1), -1 = unlimited. |
| `enabled` | boolean |  | Whether enabled |
| `group_ids` | integer[] |  | Package groups it belongs to. |
| `http_ports` | integer |  | Number of non-standard HTTP/HTTPS ports (other than 80/443), -1 = unlimited. |
| `ip_num` | integer |  | Number of selectable IPs ("My IPs"), -1 = unlimited. |
| `ip_pool_id` | integer |  | IP pool used to provision IP sites (IP product line); null = not provisioned. |
| `ip_site_port` | integer |  | IP-site ports: distinct ports across all IP sites of the package (80/443 included), -1 = unlimited; IP-site ports do not count toward non-standard ports. |
| `l2` | boolean |  | Includes tiered origin (tenant switch of the paid feature l2): tenants can turn on tiered origin for sites, and nodes use it, only while the master license l2 is active; existing packages are false (on upgrade, purchased packages of tenants already using tiered origin are set to true). |
| `name` | string | Yes | Name |
| `node_group_id` | integer |  | Primary node group; null = the package binds no lines. |
| `origin_ip_limit` | string[] |  | Allowed origin IPs or CIDR ranges; empty = any. When set, origins of sites under the package must fall inside them and domain origins are rejected. |
| `owners` | integer[] |  | Tenant ids allowed to buy; empty = no restriction. |
| `price_month_cents` | integer | Yes | Monthly price in cents. |
| `price_quarter_cents` | integer | Yes | Quarterly price in cents. |
| `price_year_cents` | integer | Yes | Yearly price in cents. |
| `region_id` | integer |  | Region; null = the package binds no lines. |
| `renew_before_days` | integer |  | Renewal is only allowed within this many days before expiry, -1 = unlimited. |
| `require_verified` | boolean |  | Real-name verification is required to purchase. |
| `sale_until` | string |  | Not sold after this time; omitted/null = no limit. |
| `sort` | integer |  | Display order (smaller first). |
| `stream_ports` | integer |  | Number of TCP/UDP forwarding ports, -1 = unlimited. |
| `traffic_gb` | integer |  | Traffic in GB per billing cycle, -1 = unlimited. |
| `waf_enhanced` | boolean |  | Includes WAF Enhanced (tenant switch of the paid feature waf_enhanced): tenants can use standard/strict levels, big screen, deep Bot, anti-tamper and so on only while the master license WAF Enhanced is not degraded; existing packages are false. |
| `websocket` | boolean |  | Allows WebSocket. |

```sh
curl -X POST "https://panel.example.com/api/v2/packages" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domains":10,"group_ids":[],"http_ports":2,"name":"基础版","price_month_cents":3000,"price_quarter_cents":8000,"price_year_cents":30000}'
```

### POST `/api/v2/packages/batch-enable`

Batch enable or disable packages · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `enabled` | boolean | Yes | Whether enabled |
| `ids` | integer[] | Yes | IDs of the objects to change, at most 100. |

```sh
curl -X POST "https://panel.example.com/api/v2/packages/batch-enable" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false,"ids":[1,2]}'
```

### GET `/api/v2/packages/{id}`

Get package · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/packages/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/packages/{id}`

Update package · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `backup_group_id` | integer |  | Backup node group; null = none. |
| `bandwidth_mbps` | integer |  | Bandwidth in Mbps, -1 = unlimited. |
| `buy_limit` | integer |  | Maximum copies a tenant may hold, -1 = unlimited. |
| `cname_domain_id` | integer |  | CNAME root domain; null = the package binds no lines. |
| `cname_host_suffix` | string |  | Suffix for newly generated host records; empty = only the 8-character random name. |
| `cname_mode` | PackageCnameMode |  | CNAME mode. site = one access name per site or stream; package = sites and streams of a purchased package share one access name. Values: ["site","package"] |
| `connections` | integer |  | Concurrent connections, -1 = unlimited. |
| `custom_cc_rule` | boolean |  | Allows custom CC rules. |
| `des` | string |  | Description |
| `domains` | integer |  | Number of domains (each IP site counts as 1), -1 = unlimited. |
| `enabled` | boolean |  | Whether enabled |
| `group_ids` | integer[] |  | Package groups it belongs to. |
| `http_ports` | integer |  | Number of non-standard HTTP/HTTPS ports (other than 80/443), -1 = unlimited. |
| `ip_num` | integer |  | Number of selectable IPs ("My IPs"), -1 = unlimited. |
| `ip_pool_id` | integer |  | IP pool used to provision IP sites (IP product line); null = not provisioned. |
| `ip_site_port` | integer |  | IP-site ports: distinct ports across all IP sites of the package (80/443 included), -1 = unlimited; IP-site ports do not count toward non-standard ports. |
| `l2` | boolean |  | Includes tiered origin (tenant switch of the paid feature l2): tenants can turn on tiered origin for sites, and nodes use it, only while the master license l2 is active; existing packages are false (on upgrade, purchased packages of tenants already using tiered origin are set to true). |
| `name` | string | Yes | Name |
| `node_group_id` | integer |  | Primary node group; null = the package binds no lines. |
| `origin_ip_limit` | string[] |  | Allowed origin IPs or CIDR ranges; empty = any. When set, origins of sites under the package must fall inside them and domain origins are rejected. |
| `owners` | integer[] |  | Tenant ids allowed to buy; empty = no restriction. |
| `price_month_cents` | integer | Yes | Monthly price in cents. |
| `price_quarter_cents` | integer | Yes | Quarterly price in cents. |
| `price_year_cents` | integer | Yes | Yearly price in cents. |
| `region_id` | integer |  | Region; null = the package binds no lines. |
| `renew_before_days` | integer |  | Renewal is only allowed within this many days before expiry, -1 = unlimited. |
| `require_verified` | boolean |  | Real-name verification is required to purchase. |
| `sale_until` | string |  | Not sold after this time; omitted/null = no limit. |
| `sort` | integer |  | Display order (smaller first). |
| `stream_ports` | integer |  | Number of TCP/UDP forwarding ports, -1 = unlimited. |
| `traffic_gb` | integer |  | Traffic in GB per billing cycle, -1 = unlimited. |
| `waf_enhanced` | boolean |  | Includes WAF Enhanced (tenant switch of the paid feature waf_enhanced): tenants can use standard/strict levels, big screen, deep Bot, anti-tamper and so on only while the master license WAF Enhanced is not degraded; existing packages are false. |
| `websocket` | boolean |  | Allows WebSocket. |

```sh
curl -X PUT "https://panel.example.com/api/v2/packages/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domains":10,"group_ids":[],"http_ports":2,"name":"基础版","price_month_cents":3000,"price_quarter_cents":8000,"price_year_cents":30000}'
```

### DELETE `/api/v2/packages/{id}`

Delete package · Authentication: Admin only

```sh
curl -X DELETE "https://panel.example.com/api/v2/packages/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/packages/{id}/syncs`

Sync base package to purchased packages · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `items` | PackageSyncField[] | Yes | Fields to sync. |

```sh
curl -X POST "https://panel.example.com/api/v2/packages/{id}/syncs" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"items":["traffic_gb","price_month_cents"]}'
```

Packages object:

| Field | Type | Required | Description |
|---|---|---|---|
| `backup_group_id` | integer |  | Backup node group; null = none. |
| `bandwidth_mbps` | integer |  | Bandwidth in Mbps, -1 = unlimited. |
| `buy_limit` | integer |  | Maximum copies a tenant may hold, -1 = unlimited. |
| `cname_domain_id` | integer |  | CNAME root domain; null = the package binds no lines. |
| `cname_host_suffix` | string |  | Suffix for newly generated host records; empty = only the 8-character random name. |
| `cname_mode` | PackageCnameMode |  | CNAME mode. site = one access name per site or stream; package = sites and streams of a purchased package share one access name. Values: ["site","package"] |
| `connections` | integer |  | Concurrent connections, -1 = unlimited. |
| `created_at` | string |  | Created at (read-only) |
| `custom_cc_rule` | boolean |  | Allows custom CC rules. |
| `des` | string |  | Description |
| `domains` | integer |  | Number of domains (each IP site counts as 1), -1 = unlimited. |
| `enabled` | boolean |  | Whether enabled |
| `errors` | FieldError[] |  | Why each remaining item was not changed; field is /ids/<index>, code as in the single-item update (not_found, readonly and so on). |
| `group_ids` | integer[] |  | Package groups it belongs to. |
| `http_ports` | integer |  | Number of non-standard HTTP/HTTPS ports (other than 80/443), -1 = unlimited. |
| `id` | integer |  | Record ID (read-only) |
| `ids` | integer[] | Yes | IDs of the objects to change, at most 100. |
| `ip_num` | integer |  | Number of selectable IPs ("My IPs"), -1 = unlimited. |
| `ip_pool_id` | integer |  | IP pool used to provision IP sites (IP product line); null = not provisioned. |
| `ip_site_port` | integer |  | IP-site ports: distinct ports across all IP sites of the package (80/443 included), -1 = unlimited; IP-site ports do not count toward non-standard ports. |
| `items` | PackageSyncField[] | Yes | Fields to sync. |
| `l2` | boolean |  | Includes tiered origin (tenant switch of the paid feature l2): tenants can turn on tiered origin for sites, and nodes use it, only while the master license l2 is active; existing packages are false (on upgrade, purchased packages of tenants already using tiered origin are set to true). |
| `name` | string |  | Name |
| `node_group_id` | integer |  | Primary node group; null = the package binds no lines. |
| `origin_ip_limit` | string[] |  | Allowed origin IPs or CIDR ranges; empty = any. When set, origins of sites under the package must fall inside them and domain origins are rejected. |
| `owners` | integer[] |  | Tenant ids allowed to buy; empty = no restriction. |
| `price_month_cents` | integer |  | Monthly price in cents. |
| `price_quarter_cents` | integer |  | Quarterly price in cents. |
| `price_year_cents` | integer |  | Yearly price in cents. |
| `region_id` | integer |  | Region; null = the package binds no lines. |
| `renew_before_days` | integer |  | Renewal is only allowed within this many days before expiry, -1 = unlimited. |
| `require_verified` | boolean |  | Real-name verification is required to purchase. |
| `sale_until` | string |  | Not sold after this time; omitted/null = no limit. |
| `skipped` | integer |  | Number skipped because they do not follow the base package or individually edited at least one changed field (other fields are still synced). |
| `skipped_ids` | integer[] |  | Ids of the skipped purchased packages. |
| `sort` | integer |  | Display order (smaller first). |
| `stream_ports` | integer |  | Number of TCP/UDP forwarding ports, -1 = unlimited. |
| `sync` | PackageSyncResult |  | Sync result. |
| `synced` | integer |  | Number of purchased packages synced (for automatic sync, those where every changed field was synced). |
| `traffic_gb` | integer |  | Traffic in GB per billing cycle, -1 = unlimited. |
| `updated` | integer[] |  | IDs that were set (including those already in the target state), in request order. |
| `updated_at` | string |  | Updated at (read-only) |
| `waf_enhanced` | boolean |  | Includes WAF Enhanced (tenant switch of the paid feature waf_enhanced): tenants can use standard/strict levels, big screen, deep Bot, anti-tamper and so on only while the master license WAF Enhanced is not degraded; existing packages are false. |
| `websocket` | boolean |  | Allows WebSocket. |

## Package groups (`package-groups`)

Groups that organize packages so customers can browse and choose.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/package-groups

### GET `/api/v2/package-groups`

List package groups · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |

```sh
curl "https://panel.example.com/api/v2/package-groups" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/package-groups`

Create package group · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `des` | string |  | Description |
| `name` | string | Yes | Name |
| `sort` | integer |  | Display order (smaller first). |

```sh
curl -X POST "https://panel.example.com/api/v2/package-groups" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"入门套餐"}'
```

### GET `/api/v2/package-groups/{id}`

Get package group · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/package-groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/package-groups/{id}`

Update package group · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `des` | string |  | Description |
| `name` | string | Yes | Name |
| `sort` | integer |  | Display order (smaller first). |

```sh
curl -X PUT "https://panel.example.com/api/v2/package-groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"入门套餐"}'
```

### DELETE `/api/v2/package-groups/{id}`

Delete package group · Authentication: Admin only

```sh
curl -X DELETE "https://panel.example.com/api/v2/package-groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Package groups object:

| Field | Type | Required | Description |
|---|---|---|---|
| `created_at` | string |  | Created at (read-only) |
| `des` | string |  | Description |
| `id` | integer |  | Record ID (read-only) |
| `name` | string |  | Name |
| `sort` | integer |  | Display order (smaller first). |
| `updated_at` | string |  | Updated at (read-only) |

## Upgrade packs (`package-upgrades`)

Quotas or features bought per unit on top of a base package.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/package-upgrades

### GET `/api/v2/package-upgrades`

List upgrade packs · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |

```sh
curl "https://panel.example.com/api/v2/package-upgrades" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/package-upgrades`

Create upgrade pack · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `amount` | integer |  | Amount added per unit; always 1 for feature items (custom_cc_rule/websocket). |
| `bind_packages` | integer[] |  | Only these base packages may buy it; empty = no restriction. |
| `des` | string |  | Description |
| `enabled` | boolean |  | Whether enabled |
| `name` | string | Yes | Name |
| `price_cents` | integer | Yes | Price per unit per month, in cents. |
| `type` | string | Yes | Add-on item: traffic GB, domains, non-standard ports, TCP/UDP forwarding ports, custom CC, bandwidth Mbps, connections, WebSocket, dedicated IPs. Values: ["traffic","domain","http_port","stream_port","custom_cc_rule","bandwidth","connection","websocket","ip_num"] |

```sh
curl -X POST "https://panel.example.com/api/v2/package-upgrades" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount":10,"name":"域名 +10","price_cents":1000,"type":"domain"}'
```

### GET `/api/v2/package-upgrades/{id}`

Get upgrade pack · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/package-upgrades/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/package-upgrades/{id}`

Update upgrade pack · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `amount` | integer |  | Amount added per unit; always 1 for feature items (custom_cc_rule/websocket). |
| `bind_packages` | integer[] |  | Only these base packages may buy it; empty = no restriction. |
| `des` | string |  | Description |
| `enabled` | boolean |  | Whether enabled |
| `name` | string | Yes | Name |
| `price_cents` | integer | Yes | Price per unit per month, in cents. |
| `type` | string | Yes | Add-on item: traffic GB, domains, non-standard ports, TCP/UDP forwarding ports, custom CC, bandwidth Mbps, connections, WebSocket, dedicated IPs. Values: ["traffic","domain","http_port","stream_port","custom_cc_rule","bandwidth","connection","websocket","ip_num"] |

```sh
curl -X PUT "https://panel.example.com/api/v2/package-upgrades/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount":10,"name":"域名 +10","price_cents":1000,"type":"domain"}'
```

### DELETE `/api/v2/package-upgrades/{id}`

Delete upgrade pack · Authentication: Admin only

```sh
curl -X DELETE "https://panel.example.com/api/v2/package-upgrades/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Upgrade packs object:

| Field | Type | Required | Description |
|---|---|---|---|
| `amount` | integer |  | Amount added per unit; always 1 for feature items (custom_cc_rule/websocket). |
| `bind_packages` | integer[] |  | Only these base packages may buy it; empty = no restriction. |
| `created_at` | string |  | Created at (read-only) |
| `des` | string |  | Description |
| `enabled` | boolean |  | Whether enabled |
| `id` | integer |  | Record ID (read-only) |
| `name` | string |  | Name |
| `price_cents` | integer |  | Price per unit per month, in cents. |
| `type` | string |  | Add-on item: traffic GB, domains, non-standard ports, TCP/UDP forwarding ports, custom CC, bandwidth Mbps, connections, WebSocket, dedicated IPs. Values: ["traffic","domain","http_port","stream_port","custom_cc_rule","bandwidth","connection","websocket","ip_num"] |
| `updated_at` | string |  | Updated at (read-only) |

## Package catalog (`package-catalog`)

The catalog of packages, groups and upgrade packs a customer can buy.

Authentication: User · Docs: https://www-demo.kuxitong.com/en/docs/api/package-catalog

### GET `/api/v2/package-catalog`

Purchasable package catalog · Authentication: User

```sh
curl "https://panel.example.com/api/v2/package-catalog" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Package catalog object:

| Field | Type | Required | Description |
|---|---|---|---|
| `groups` | PackageGroup[] |  | Package groups. |
| `packages` | Package[] |  | Base packages on sale. |
| `upgrades` | PackageUpgrade[] |  | Upgrade packs on sale. |

## Purchased packages (`user-packages`)

Packages a tenant has bought: purchase, renew, change, add upgrade packs and view usage.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/user-packages

### GET `/api/v2/user-packages`

List purchased packages · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `ids` | string |  | Only list these IDs: comma-separated positive integers, at most 200 after deduplication (same as the page_size limit; more returns 400); an empty value is the same as omitting it. Combined with other filters as an intersection; IDs you cannot see are silently left out. Pagination is unchanged: set page_size to at least the number of IDs to get them in one page |
| `package_id` | integer |  | Only list purchased packages of this base package. |
| `expire_within_days` | integer |  | Only list purchased packages expiring within this many days (including already expired ones). |
| `name` | string |  | Purchased package name contains (% and _ match literally). |
| `customized` | boolean |  | true lists only purchased packages with individually edited quotas or not following the base package; false lists only those following it unchanged. |

```sh
curl "https://panel.example.com/api/v2/user-packages" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/user-packages`

Purchase package · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `dry_run` | boolean |  | Quote only, do not save. |
| `duration` | string |  | Duration: month/quarter/year (adds 1/3/12 months; at month end the last day of the target month is used). Values: ["month","quarter","year"] |
| `end_at` | string |  | Expiry time of the purchased package. |
| `name` | string |  | Name |
| `package_id` | integer | Yes | Base package id. |

```sh
curl -X POST "https://panel.example.com/api/v2/user-packages" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"duration":"month","package_id":1}'
```

### GET `/api/v2/user-packages/{id}`

Get purchased package · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/user-packages/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/user-packages/{id}`

Update purchased package · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `backup_group_id` | integer |  | Backup node group; null = none. |
| `bandwidth_mbps` | integer |  | Bandwidth in Mbps, -1 = unlimited. |
| `cname_domain_id` | integer |  | CNAME root domain; null = the package binds no lines. |
| `cname_hostname` | string |  | Host record of the shared access name (used by sites and streams in per-package mode); empty = none yet. |
| `cname_mode` | PackageCnameMode |  | CNAME mode. site = one access name per site or stream; package = sites and streams of a purchased package share one access name. Values: ["site","package"] |
| `connections` | integer |  | Concurrent connections, -1 = unlimited. |
| `custom_cc_rule` | boolean |  | Allows custom CC rules. |
| `domains` | integer |  | Number of domains (each IP site counts as 1), -1 = unlimited. |
| `end_at` | string |  | Expiry time of the purchased package. |
| `follow_base` | boolean |  | Follows the base package; quota changes on the base package are synced here automatically (except fields in overrides). |
| `http_ports` | integer |  | Number of non-standard HTTP/HTTPS ports (other than 80/443), -1 = unlimited. |
| `ip_num` | integer |  | Number of selectable IPs ("My IPs"), -1 = unlimited. |
| `ip_pool_id` | integer |  | IP pool used to provision IP sites (IP product line); null = not provisioned. |
| `ip_site_port` | integer |  | IP-site ports: distinct ports across all IP sites of the package (80/443 included), -1 = unlimited; IP-site ports do not count toward non-standard ports. |
| `l2` | boolean |  | Includes tiered origin (tenant switch of the paid feature l2): tenants can turn on tiered origin for sites, and nodes use it, only while the master license l2 is active; existing packages are false (on upgrade, purchased packages of tenants already using tiered origin are set to true). |
| `name` | string |  | Name |
| `node_group_id` | integer |  | Primary node group; null = the package binds no lines. |
| `price_month_cents` | integer |  | Monthly price locked at purchase. |
| `price_quarter_cents` | integer |  | Quarterly price locked at purchase. |
| `price_year_cents` | integer |  | Yearly price locked at purchase. |
| `region_id` | integer |  | Region; null = the package binds no lines. |
| `reset_to_base` | boolean |  | Admin only; true = reset quota fields to the base package values, clear overrides and follow again (cannot be combined with other fields). |
| `stream_ports` | integer |  | Number of TCP/UDP forwarding ports, -1 = unlimited. |
| `traffic_gb` | integer |  | Traffic in GB per billing cycle, -1 = unlimited. |
| `waf_enhanced` | boolean |  | Includes WAF Enhanced (tenant switch of the paid feature waf_enhanced): tenants can use standard/strict levels, big screen, deep Bot, anti-tamper and so on only while the master license WAF Enhanced is not degraded; existing packages are false. |
| `websocket` | boolean |  | Allows WebSocket. |

```sh
curl -X PUT "https://panel.example.com/api/v2/user-packages/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"主站套餐"}'
```

### DELETE `/api/v2/user-packages/{id}`

Delete purchased package · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/user-packages/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/user-packages/{id}/changes`

Change package (upgrade or downgrade) · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `dry_run` | boolean |  | Quote only, do not save. |
| `package_id` | integer | Yes | Base package id. |

```sh
curl -X POST "https://panel.example.com/api/v2/user-packages/{id}/changes" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"package_id":2}'
```

### GET `/api/v2/user-packages/{id}/ips`

My IPs · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/user-packages/{id}/ips" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/user-packages/{id}/ips`

Select IPs · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `ip_ids` | integer[] | Yes | IDs of the pool IPs to select. |

```sh
curl -X POST "https://panel.example.com/api/v2/user-packages/{id}/ips" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/user-packages/{id}/ips/{ip_id}`

Update note of selected IP · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `remark` | string | Yes | Remark |

```sh
curl -X PUT "https://panel.example.com/api/v2/user-packages/{id}/ips/{ip_id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/user-packages/{id}/ips/{ip_id}`

Remove selected IP · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/user-packages/{id}/ips/{ip_id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/user-packages/{id}/node-groups`

Node groups of a purchased package · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/user-packages/{id}/node-groups" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/user-packages/{id}/renewals`

Renew purchased package · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `dry_run` | boolean |  | Quote only, do not save. |
| `duration` | string | Yes | Duration: month/quarter/year (adds 1/3/12 months; at month end the last day of the target month is used). Values: ["month","quarter","year"] |

```sh
curl -X POST "https://panel.example.com/api/v2/user-packages/{id}/renewals" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"duration":"month"}'
```

### GET `/api/v2/user-packages/{id}/upgrades`

List purchased upgrade packs · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/user-packages/{id}/upgrades" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/user-packages/{id}/upgrades`

Buy upgrade pack · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `amount` | integer |  | Number of units, default 1. |
| `dry_run` | boolean |  | Quote only, do not save. |
| `upgrade_id` | integer | Yes | Id of the upgrade pack to buy. |

```sh
curl -X POST "https://panel.example.com/api/v2/user-packages/{id}/upgrades" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount":1,"upgrade_id":1}'
```

### DELETE `/api/v2/user-packages/{id}/upgrades/{upgrade_id}`

Reduce or remove purchased upgrade pack · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `amount` | integer |  | Number of units to reduce; omitted = remove the whole item. |

```sh
curl -X DELETE "https://panel.example.com/api/v2/user-packages/{id}/upgrades/{upgrade_id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/user-packages/{id}/usage`

Purchased package usage · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/user-packages/{id}/usage" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Purchased packages object:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | string |  | Operation: buy, renew, change or upgrade. Values: ["buy","renew","change","upgrade"] |
| `added` | integer[] |  | IDs of the pool IPs newly selected by this call. |
| `amount` | integer |  | Number of units, default 1. |
| `amount_cents` | integer |  | Positive = charged, negative = refunded to balance (downgrade), 0 = no charge (including admin operations). |
| `backup_group_id` | integer |  | Backup node group; null = none. |
| `bandwidth_mbps` | integer |  | Bandwidth in Mbps, -1 = unlimited. |
| `cert_auto` | IpCertState |  | Platform IP certificate status. |
| `cname_domain_id` | integer |  | CNAME root domain; null = the package binds no lines. |
| `cname_hostname` | string |  | Host record of the shared access name (used by sites and streams in per-package mode); empty = none yet. |
| `cname_mode` | PackageCnameMode |  | CNAME mode. site = one access name per site or stream; package = sites and streams of a purchased package share one access name. Values: ["site","package"] |
| `connections` | integer |  | Concurrent connections, -1 = unlimited. |
| `created_at` | string |  | Created at (read-only) |
| `custom_cc_rule` | boolean |  | Allows custom CC rules. |
| `cycle_end` | string |  | End of the current billing cycle. |
| `cycle_start` | string |  | Start of the current billing cycle. |
| `domains` | integer |  | Number of domains (each IP site counts as 1), -1 = unlimited. |
| `dry_run` | boolean |  | Quote only, do not save. |
| `duration` | string |  | Duration: month/quarter/year (adds 1/3/12 months; at month end the last day of the target month is used). Values: ["month","quarter","year"] |
| `enabled` | boolean |  | Whether enabled |
| `end_at` | string |  | Expiry time of the purchased package. |
| `follow_base` | boolean |  | Follows the base package; quota changes on the base package are synced here automatically (except fields in overrides). (read-only) |
| `http_ports` | integer |  | Number of non-standard HTTP/HTTPS ports (other than 80/443), -1 = unlimited. |
| `id` | integer |  | Record ID (read-only) |
| `in_pool` | boolean |  | Still in the package's current IP pool. |
| `ip` | string |  | IP address. |
| `ip_ids` | integer[] | Yes | IDs of the pool IPs to select. |
| `ip_num` | integer |  | Number of selectable IPs ("My IPs"), -1 = unlimited. |
| `ip_pool_id` | integer |  | IP pool used to provision IP sites (IP product line); null = not provisioned. |
| `ip_site_port` | integer |  | IP-site ports: distinct ports across all IP sites of the package (80/443 included), -1 = unlimited; IP-site ports do not count toward non-standard ports. |
| `items` | UserPackageIp[] |  | Items on this page |
| `l2` | boolean |  | Includes tiered origin (tenant switch of the paid feature l2): tenants can turn on tiered origin for sites, and nodes use it, only while the master license l2 is active; existing packages are false (on upgrade, purchased packages of tenants already using tiered origin are set to true). |
| `limit` | integer |  | Number of IPs that can be selected; -1 means unlimited. |
| `limits` | PackageLimits |  | Effective limits (package values plus upgrade packs; -1 = unlimited, 0/1 for feature items). |
| `name` | string |  | Name |
| `node_group_id` | integer |  | Primary node group; null = the package binds no lines. |
| `node_state` | IpNodeState |  | State of the owning primary node: disabled if the node is disabled; otherwise online if a heartbeat arrived in the last 2 minutes, else offline. Values: ["online","offline","disabled"] |
| `order_id` | integer |  | Order id (when a charge or refund happened). |
| `overrides` | PackageQuotaField[] |  | Quota fields edited individually by an admin that differ from the base package; skipped by automatic sync. (read-only) |
| `package_id` | integer |  | Base package id. |
| `package_name` | string |  | Base package name (shown in package lists and orders). (read-only) |
| `period` | string |  | Price period used for proration when changing package. Values: ["month","quarter","year"] |
| `price_month_cents` | integer |  | Monthly price locked at purchase. |
| `price_quarter_cents` | integer |  | Quarterly price locked at purchase. |
| `price_year_cents` | integer |  | Yearly price locked at purchase. |
| `region` | string |  | Name of the region it belongs to. |
| `region_id` | integer |  | Region; null = the package binds no lines. |
| `remain_days` | integer |  | Remaining days used for proration. |
| `remark` | string | Yes | Remark |
| `requests` | integer |  | Number of requests in this billing cycle (sealed hours plus a live count of hours not yet sealed). |
| `reset_to_base` | boolean |  | Admin only; true = reset quota fields to the base package values, clear overrides and follow again (cannot be combined with other fields). |
| `selected` | integer |  | Number of IPs already selected. |
| `sites` | object[] |  | IP sites in this package that bind it. |
| `start_at` | string |  | Start time of the purchased package. |
| `status` | string |  | expired = expired (the tenant's sites are suspended with 512; renewing restores them). Values: ["active","expired"] |
| `stream_ports` | integer |  | Number of TCP/UDP forwarding ports, -1 = unlimited. |
| `tenant_id` | integer |  | Owner tenant (user) ID (read-only) |
| `traffic_bytes` | integer |  | Billable traffic used in this billing cycle (bytes, already multiplied by the compensation factor) — sealed hours plus a live count of hours not yet sealed, the same figure the over-quota stop uses. |
| `traffic_gb` | integer |  | Traffic in GB per billing cycle, -1 = unlimited. |
| `traffic_limit_gb` | integer |  | Effective traffic limit (GB, 1 GB = 1024^3 bytes, including upgrade packs); -1 = unlimited. |
| `updated_at` | string |  | Updated at (read-only) |
| `upgrade_id` | integer | Yes | Id of the upgrade pack to buy. |
| `user_package` | UserPackage |  | The purchased package after the operation. |
| `user_package_id` | integer |  | Id of the purchased package this operation applied to (for a purchase, the newly created one; equals user_package.id). Absent for a dry run, which saves nothing. |
| `waf_enhanced` | boolean |  | Includes WAF Enhanced (tenant switch of the paid feature waf_enhanced): tenants can use standard/strict levels, big screen, deep Bot, anti-tamper and so on only while the master license WAF Enhanced is not degraded; existing packages are false. |
| `websocket` | boolean |  | Allows WebSocket. |

## Package monitor (`package-monitor`)

Admin only: bandwidth and connections of each purchased package over the last minute.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/package-monitor

### GET `/api/v2/package-monitor`

Package monitor · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `user_package_id` | integer |  | Filter by user package id. |
| `node_id` | integer |  | Node ID |

```sh
curl "https://panel.example.com/api/v2/package-monitor" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/package-monitor/{id}/nodes`

Package monitor by node · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/package-monitor/{id}/nodes" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Package monitor object:

| Field | Type | Required | Description |
|---|---|---|---|
| `bandwidth_bps` | integer |  | Sum of outbound bandwidth across nodes over the last minute (bit/s). |
| `bandwidth_limit_mbps` | integer |  | Effective bandwidth quota (including upgrade packs); -1 means unlimited. |
| `connections` | integer |  | Sum of per-node peak connection counts over the last minute. |
| `connections_limit` | integer |  | Effective connection quota (including upgrade packs); -1 means unlimited. |
| `items` | PackageMonitorNode[] |  | Items on this page |
| `name` | string |  | Name |
| `tenant_id` | integer |  | Owner tenant (user) ID |
| `user_package_id` | integer |  | Purchased package ID. |

## Sites (`sites`)

CDN-accelerated sites. Creating one needs at least a domain and an origin; admins can manage any tenant's sites.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/sites

### GET `/api/v2/sites`

List sites · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `ids` | string |  | Only list these IDs: comma-separated positive integers, at most 200 after deduplication (same as the page_size limit; more returns 400); an empty value is the same as omitting it. Combined with other filters as an intersection; IDs you cannot see are silently left out. Pagination is unchanged: set page_size to at least the number of IDs to get them in one page |
| `domain` | string |  | Filter by domain substring (case-insensitive); a.example.com also matches www.a.example.com. Use domains for exact matching. |
| `domains` | string |  | Comma-separated domains; matches sites having any domain exactly equal to one of them (case-insensitive, no substring match). |
| `group_id` | integer |  | Only list sites in this group. |
| `group_ids` | string |  | Comma-separated group IDs (positive integers, at most 200 after deduplication); a site in any of them matches. May be combined with group_id (both must hold). |
| `app` | string |  | Filter by the application identified by the checkup (exact match, e.g. wordpress); sites never checked do not match. |
| `enabled` | boolean |  | Filter by enabled state. |
| `id` | integer |  | Filter by exact site id (panel search box "Site ID"). |
| `name` | string |  | Filter by name substring. |
| `origin` | string |  | Filter by origin host substring (panel search box "Origin IP"). |
| `stop_code` | integer |  | Filter by stop code; 0 = normal (panel search box "Site status"). |
| `upid` | integer |  | Filter by user package. |
| `http_port` | integer |  | Only list sites whose HTTP listen ports (config.http_ports) include this port; exact match (80 does not match 8080). |
| `https_port` | integer |  | Only list sites whose HTTPS listen ports (config.https.ports) include this port; sites without HTTPS never match. |
| `https_enabled` | boolean |  | true lists only sites with HTTPS enabled (config.https has listen ports); false lists only sites without HTTPS. |
| `package_id` | integer |  | Filter by base package (the owning user package was bought from it). |
| `region_id` | integer |  | Filter by region (the CNAME's primary line group belongs to it); owners without their own CNAME use their user package's shared one. |
| `node_group_id` | integer |  | Filter by line group (the CNAME's primary or backup line group). |
| `cname_domain_id` | integer |  | Filter by CNAME root domain. |
| `cname_hostname` | string |  | Filter by CNAME host label, exact match (case-insensitive, without the root domain). |
| `sync_state` | string |  | Filter by config sync state: behind = some online node has not yet installed the config version containing its latest change; Values: ["behind","synced"] |

```sh
curl "https://panel.example.com/api/v2/sites" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/sites`

Create site · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Owning tenant when an admin creates on behalf of a tenant (required); ignored for tenant callers, whose own account is always the owner. |

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `cname` | CnameBinding |  | Access name (CNAME). Users point their domain to fqdn with a CNAME record; the platform generates its DNS records from the node group. null when there is no access name. |
| `config` | object |  | Node config fields (top-level fields of schema/site.json, without id, uid, upid, domains, origins, enabled, stop_code and rulesets). |
| `domains` | string[] | Yes | Domains served by the site. |
| `enabled` | boolean |  | Whether enabled |
| `group_ids` | integer[] |  | Ids of the site groups the site belongs to. |
| `name` | string |  | Name |
| `origins` | SiteOrigin[] | Yes | Origin servers. |
| `stop_code` | integer |  | 0 = normal; non-zero = every request is answered with this status code (set by admin or billing). |
| `upid` | integer |  | User package id (may be empty before the package domain goes live; falls back to tenant_id in the snapshot when empty). |

```sh
curl -X POST "https://panel.example.com/api/v2/sites" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domains":["www.example.com"],"name":"demo","origins":[{"host":"203.0.113.10"}]}'
```

### POST `/api/v2/sites/batch`

Batch update sites · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Owning tenant when an admin creates on behalf of a tenant (required); ignored for tenant callers, whose own account is always the owner. |

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | string | Yes | Batch action: set_enabled, delete, set_groups, update_config or create. Values: ["set_enabled","delete","set_groups","update_config","create"] |
| `config` | object |  | Node config fields (top-level fields of schema/site.json, without id, uid, upid, domains, origins, enabled, stop_code and rulesets). |
| `enabled` | boolean |  | Whether enabled |
| `group_ids` | integer[] |  | Ids of the site groups the site belongs to. |
| `ids` | integer[] |  | Ids of the sites to operate on (up to 100; required by every action except create). |
| `items` | SiteWrite[] |  | Used by create: the sites to create (up to 100), each shaped like the POST /sites request body. |

```sh
curl -X POST "https://panel.example.com/api/v2/sites/batch" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"action":"set_enabled","enabled":false,"ids":[1]}'
```

### POST `/api/v2/sites/checkup-fix-batch`

Batch one-click fix for a checkup problem · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `batch` | string |  | rec_type only - pass the batch returned by the first segment so all segments share one undo batch; omit to start a new batch. |
| `ids` | integer[] | Yes | Ids of the sites to operate on (up to 100; required by every action except create). |
| `key` | string | Yes | Key of the checkup problem to fix, same meaning as the single-site fix. Values: ["no_https","tls_old","sec_headers","waf_block","cc_mid","rec_type"] |

```sh
curl -X POST "https://panel.example.com/api/v2/sites/checkup-fix-batch" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/sites/checkup-fix-batches`

List recent checkup fix batches · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

```sh
curl "https://panel.example.com/api/v2/sites/checkup-fix-batches" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/sites/origin-alerts`

Origin inconsistency alerts · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `site_id` | integer |  | Only this site. |

```sh
curl "https://panel.example.com/api/v2/sites/origin-alerts" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/sites/protect-recommend/apply`

Batch apply the recommended protection settings · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `ids` | integer[] | Yes | Ids of the sites to operate on (up to 100; required by every action except create). |
| `observe_days` | integer |  | Overrides the observe-first days of waf_mode in the table (0 blocks immediately); defaults to the table value. |
| `skip` | string[] |  | Recommended items the user unticked and that must not be applied this time. |
| `type` | string | Yes | Site type - corporate, shop, content, api, admin or download. Values: ["corporate","shop","content","api","admin","download"] |

```sh
curl -X POST "https://panel.example.com/api/v2/sites/protect-recommend/apply" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/sites/protect-recommend/preview`

Preview the impact of the recommended protection settings · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `ids` | integer[] | Yes | Ids of the sites to operate on (up to 100; required by every action except create). |
| `observe_days` | integer |  | Overrides the observe-first days of waf_mode in the table (0 blocks immediately); defaults to the table value. |
| `skip` | string[] |  | Recommended items the user unticked and that must not be applied this time. |
| `type` | string |  | Site type - corporate, shop, content, api, admin or download. Values: ["corporate","shop","content","api","admin","download"] |

```sh
curl -X POST "https://panel.example.com/api/v2/sites/protect-recommend/preview" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/sites/protect-recommend/undo`

Undo a batch of recommended protection settings · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `batch` | string | Yes | rec_type only - pass the batch returned by the first segment so all segments share one undo batch; omit to start a new batch. |
| `ids` | integer[] | Yes | Ids of the sites to operate on (up to 100; required by every action except create). |

```sh
curl -X POST "https://panel.example.com/api/v2/sites/protect-recommend/undo" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/sites/region-check`

Pre-save check of a region limit · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `region_limit` | object | Yes | The region limit to check (same as Site.config.region_limit). |

```sh
curl -X POST "https://panel.example.com/api/v2/sites/region-check" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"region_limit":{"country":["JP"],"mode":"block"}}'
```

### POST `/api/v2/sites/select-ids`

Get all site IDs matching a filter · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `app` | string |  | Filter by the application identified by the checkup (exact match). |
| `domain` | string |  | Filter by domain substring (case-insensitive). |
| `domains` | string[] |  | Domains served by the site. |
| `enabled` | boolean |  | Whether enabled |
| `group_id` | integer |  | Only sites in this group. |
| `group_ids` | integer[] |  | Ids of the site groups the site belongs to. |
| `name` | string |  | Name |
| `origin` | string |  | Filter by origin host substring. |
| `stop_code` | integer |  | 0 = normal; non-zero = every request is answered with this status code (set by admin or billing). |
| `upid` | integer |  | User package id (may be empty before the package domain goes live; falls back to tenant_id in the snapshot when empty). |

```sh
curl -X POST "https://panel.example.com/api/v2/sites/select-ids" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/sites/{id}`

Get site · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/sites/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/sites/{id}`

Update site · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `cname` | CnameBinding |  | Access name (CNAME). Users point their domain to fqdn with a CNAME record; the platform generates its DNS records from the node group. null when there is no access name. |
| `config` | object |  | Node config fields (top-level fields of schema/site.json, without id, uid, upid, domains, origins, enabled, stop_code and rulesets). |
| `domains` | string[] |  | Domains served by the site. |
| `enabled` | boolean |  | Whether enabled |
| `expected_updated_at` | string |  | Optimistic lock (used on update) - the updated_at of the resource as the caller read it. If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `group_ids` | integer[] |  | Ids of the site groups the site belongs to. |
| `name` | string |  | Name |
| `origins` | SiteOrigin[] |  | Origin servers. |
| `stop_code` | integer |  | 0 = normal; non-zero = every request is answered with this status code (set by admin or billing). |
| `upid` | integer |  | User package id (may be empty before the package domain goes live; falls back to tenant_id in the snapshot when empty). |

```sh
curl -X PUT "https://panel.example.com/api/v2/sites/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false}'
```

### DELETE `/api/v2/sites/{id}`

Delete site · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/sites/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/sites/{id}/checkup-fix`

One-click fix for a checkup problem · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `key` | string | Yes | Key of the checkup problem to fix, same meaning as the single-site fix. Values: ["no_https","tls_old","sec_headers","waf_block","cc_mid","rec_type"] |

```sh
curl -X POST "https://panel.example.com/api/v2/sites/{id}/checkup-fix" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"no_https"}'
```

### GET `/api/v2/sites/{id}/health`

Site origin health status · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/sites/{id}/health" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/sites/{id}/l2`

Get site tiered origin · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/sites/{id}/l2" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PATCH `/api/v2/sites/{id}/l2`

Update site tiered origin · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `enabled` | boolean |  | Whether enabled |
| `l2_group_id` | integer |  | L2 group used; null for region default. |
| `l2_pick` | string |  | How L2 is picked for this site; null to follow the defaults. Values: ["region","near"] |
| `origin_l2_only` | boolean |  | Origin allows only L2: dynamic requests also go via L2, borrowing other regions when needed. |
| `origin_max_conns` | integer |  | Max origin concurrency per node; 0 for unlimited. Excess requests queue instead of failing with 503. |

```sh
curl -X PATCH "https://panel.example.com/api/v2/sites/{id}/l2" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/sites/{id}/l2-egress-ips`

Origin allowlist · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `fmt` | string |  | Format of text: ip one per line, nginx allow lines, or iptables rules; defaults to ip. Values: ["ip","nginx","iptables"] Default: "ip" |

```sh
curl "https://panel.example.com/api/v2/sites/{id}/l2-egress-ips" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/sites/{id}/l2-stats`

Site tiered origin statistics · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |

```sh
curl "https://panel.example.com/api/v2/sites/{id}/l2-stats" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/sites/{id}/site-type`

Set the site type · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `type` | string | Yes | Site type - corporate, shop, content, api, admin or download. Values: ["corporate","shop","content","api","admin","download"] |

```sh
curl -X PUT "https://panel.example.com/api/v2/sites/{id}/site-type" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"shop"}'
```

### GET `/api/v2/sites/{id}/speed`

Site acceleration statistics · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `range` | string |  | Statistics window, default 24h. Values: ["1h","24h","7d"] Default: "24h" |

```sh
curl "https://panel.example.com/api/v2/sites/{id}/speed" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/sites/{id}/upload-scan/exceptions`

List a site's upload scan exceptions · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/sites/{id}/upload-scan/exceptions" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/sites/{id}/upload-scan/false-positive`

Mark a site's upload scan hit as a false positive · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `path` | string |  | Path prefix (starting with /); defaults to /. |
| `sha256` | string |  | sha256 of the hit file (lowercase hex); a tenant's value must appear in the site's upload hit events; if given, intelligence sharing is on and sig is not empty, a false-positive intelligence item is queued in the same transaction. |
| `sig` | string |  | Signature name of the false positive (the rule name in the event); empty = the whole path is not scanned (path is then required). |

```sh
curl -X POST "https://panel.example.com/api/v2/sites/{id}/upload-scan/false-positive" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"path":"/upload/avatar/","sig":"ext:php"}'
```

### DELETE `/api/v2/sites/{id}/upload-scan/false-positive`

Revoke a site's upload scan false-positive mark · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `path` | string | Yes | Path prefix of the exception. |
| `sig` | string |  | Signature name of the exception; omitted or empty = a whole-path exception. |

```sh
curl -X DELETE "https://panel.example.com/api/v2/sites/{id}/upload-scan/false-positive" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/sites/{id}/url-guard/active`

List a site's URL protections in effect · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/sites/{id}/url-guard/active" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/sites/{id}/url-guard/revoke`

Manually revoke a site's URL protections in effect · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `path` | string |  | Path prefix (starting with /); defaults to /. |

```sh
curl -X POST "https://panel.example.com/api/v2/sites/{id}/url-guard/revoke" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Sites object:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | string | Yes | Batch action: set_enabled, delete, set_groups, update_config or create. Values: ["set_enabled","delete","set_groups","update_config","create"] |
| `added_at` | string |  | When the entry was added; early manual entries have none. |
| `affected` | integer |  | Number of affected records |
| `app` | string |  | Filter by the application identified by the checkup (exact match). |
| `batch` | string |  | rec_type only - pass the batch returned by the first segment so all segments share one undo batch; omit to start a new batch. |
| `capacity_warnings` | SiteCapacityWarning[] |  | Only in the responses of creating a site (POST /sites) and moving it to another line group (PUT /sites/{id}), when a node of the target line group has reached its memory capacity limit; a warning only, never blocks. Omitted when no node is over the limit. (read-only) |
| `checked_at` | string |  | Time the details were requested from the nodes. |
| `cname` | CnameBinding |  | Access name (CNAME). Users point their domain to fqdn with a CNAME record; the platform generates its DNS records from the node group. null when there is no access name. |
| `config` | object |  | Node config fields (top-level fields of schema/site.json, without id, uid, upid, domains, origins, enabled, stop_code and rulesets). |
| `country` | string |  | Country/region code, omitted if unknown. |
| `created_at` | string |  | Created at (read-only) |
| `default_pick` | string |  | The current default method (applies when l2_pick is null). Values: ["region","near"] |
| `domain` | string |  | Filter by domain substring (case-insensitive). |
| `domains` | string[] |  | Domains served by the site. |
| `edge_origin_requests` | integer |  | Edge origin requests (hit rate denominator). |
| `enabled` | boolean |  | Whether enabled |
| `end` | string |  | Window end (exclusive, rounded to 5 minutes). |
| `expected_updated_at` | string |  | Optimistic lock (used on update) - the updated_at of the resource as the caller read it. If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check. |
| `found` | boolean |  | The IP was found in the database (otherwise judged as an unidentified visitor). |
| `gate_reasons` | SiteGateReasonIds |  | The sites in ids whose WAF Enhanced is not in effect, grouped by reason (only sites with a reason are listed; the whole field is omitted when all are in effect). The picker uses package to mark sites as not covered by the plan and disable them. (read-only) |
| `group_id` | integer |  | Only sites in this group. |
| `group_ids` | integer[] |  | Ids of the site groups the site belongs to. |
| `group_options` | L2SiteRefGroup[] |  | Custom L2 groups available. |
| `hit_rate` | number |  | Hit rate 0-1; 0 when there were no origin requests. |
| `id` | integer |  | Record ID (read-only) |
| `ids` | integer[] |  | Ids of the sites to operate on (up to 100; required by every action except create). |
| `ip` | string |  | The caller's IP. |
| `isp` | string |  | ISP code, omitted if unknown. |
| `items` | SiteWrite[] |  | Used by create: the sites to create (up to 100), each shaped like the POST /sites request body. |
| `key` | string | Yes | Key of the checkup problem to fix, same meaning as the single-site fix. Values: ["no_https","tls_old","sec_headers","waf_block","cc_mid","rec_type"] |
| `l2_group_id` | integer |  | L2 group used; null for region default. |
| `l2_hit_requests` | integer |  | Origin requests served by L2 (hit rate numerator). |
| `l2_pick` | string |  | How L2 is picked for this site; null to follow the defaults. Values: ["region","near"] |
| `log` | SiteSpeedLog |  | Protocol split, origin time percentiles, sampled cache-miss URLs and edge response times from the raw access log (weighted by sample weight), same window as totals. |
| `log_keep_days` | integer |  | Raw access log retention in days (reports.access_log_days); when the window is longer, log only covers what is still kept. |
| `missing` | string[] |  | Selected fields the current database has no data for (those selections never match). |
| `name` | string |  | Name |
| `node_issues` | NodeIssue[] |  | Ports or reasons for which the site is not fully in effect on online nodes (in the list and the detail); omitted when everything is in effect. (read-only) |
| `nodes` | SiteHealthNode[] |  | Details from each active node, ordered by node id. |
| `observe_days` | integer |  | Overrides the observe-first days of waf_mode in the table (0 blocks immediately); defaults to the table value. |
| `origin` | string |  | Filter by origin host substring. |
| `origin_l2_only` | boolean |  | Origin allows only L2: dynamic requests also go via L2, borrowing other regions when needed. |
| `origin_max_conns` | integer |  | Max origin concurrency per node; 0 for unlimited. Excess requests queue instead of failing with 503. |
| `origin_requests` | integer |  | Requests that reached the origin. |
| `origin_requests_est` | integer |  | Estimate without tiering (= edge origin requests). |
| `origins` | SiteOrigin[] |  | Origin servers. |
| `path` | string |  | Path prefix (starting with /); defaults to /. |
| `points` | SiteSpeedPoint[] |  | Per-bucket details (only buckets with traffic are listed). |
| `prov` | string |  | Province code, omitted if unknown. |
| `publish_seq` | integer |  | Only in the responses of creating and updating a site: the sequence number of this write (same as the Publish-Seq response header); pass it to GET /config-publish/applied?seq= to check whether it has reached the nodes. (read-only) |
| `range` | string |  | Statistics window (1h, 24h or 7d). |
| `rec_ver` | integer |  | Version of the recommendation table used. |
| `region_limit` | object | Yes | The region limit to check (same as Site.config.region_limit). |
| `region_names` | string[] |  | Names of the regions with a serving L2, ordered by region ID; as many as regions. |
| `regions` | integer |  | Regions with a serving L2. |
| `revoked` | string[] |  | Revoked paths. |
| `saved_bytes` | integer |  | Origin traffic saved (bytes served from L2 cache). |
| `sha256` | string |  | sha256 of the hit file (lowercase hex); a tenant's value must appear in the site's upload hit events; if given, intelligence sharing is on and sig is not empty, a false-positive intelligence item is queued in the same transaction. |
| `sig` | string |  | Signature name of the false positive (the rule name in the event); empty = the whole path is not scanned (path is then required). |
| `site_id` | integer |  | Site id; 0 = all sites. |
| `skip` | string[] |  | Recommended items the user unticked and that must not be applied this time. |
| `source` | string |  | user chosen manually; auto detected from traffic. Values: ["user","auto"] |
| `start` | string |  | Window start. |
| `state` | string |  | Effective state: on, off, not_open, paused, no_l2, or revoking. Values: ["on","off","not_open","paused","no_l2","revoking"] |
| `status` | string |  | disabled = active health checks are off; unknown = no node returned details; healthy = all origins up on the nodes that answered; unhealthy = some origin is removed. |
| `stop_code` | integer |  | 0 = normal; non-zero = every request is answered with this status code (set by admin or billing). |
| `tenant_id` | integer |  | Owner tenant (user) ID (read-only) |
| `text` | string |  | Text in the requested format, ready to paste on the origin. |
| `total` | integer |  | Actual number of matches (may exceed the length of ids). |
| `totals` | SiteSpeedTotals |  | Totals over the whole window. |
| `truncated` | boolean |  | True when more than 100000 sites matched and ids holds only the first 100000. |
| `type` | string | Yes | Site type - corporate, shop, content, api, admin or download. Values: ["corporate","shop","content","api","admin","download"] |
| `updated_at` | string |  | Updated at (read-only) |
| `upid` | integer |  | User package id (may be empty before the package domain goes live; falls back to tenant_id in the snapshot when empty). |
| `verdict` | string |  | Result for the caller visiting the home page: pass, block (blocked) or log (logged only). Values: ["pass","block","log"] |
| `waf_gate` | SiteWafGate |  | Only in the site detail (GET /sites/{id}): the WAF Enhanced status of the site's owner and why it is not in effect. owner, packages and expired_at are in administrator responses only; tenants get gate and reason only. List rows carry waf_gate_reason alone. (read-only) |
| `waf_gate_reason` | WafGateReason |  | Why WAF Enhanced (waf_enhanced) is not in effect; absent when gate is full or readonly. package = the platform is licensed but the owner has no unexpired package that includes WAF Enhanced; license = the platform has not enabled it (never licensed, revoked by the provider, or the license was disabled); expired = the platform license has expired and its read-only buffer period (License.readonly_days, default 7 days) has passed, so it fell back to basic. Values: ["package","license","expired"] (read-only) |

## Site groups (`site-groups`)

Groups for organizing sites; names are unique within a tenant and a group that still contains sites cannot be deleted.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/site-groups

### GET `/api/v2/site-groups`

List site groups · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `ids` | string |  | Only list these IDs: comma-separated positive integers, at most 200 after deduplication (same as the page_size limit; more returns 400); an empty value is the same as omitting it. Combined with other filters as an intersection; IDs you cannot see are silently left out. Pagination is unchanged: set page_size to at least the number of IDs to get them in one page |

```sh
curl "https://panel.example.com/api/v2/site-groups" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/site-groups`

Create site group · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Owning tenant when an admin creates on behalf of a tenant (required); ignored for tenant callers, whose own account is always the owner. |

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `des` | string |  | Description |
| `name` | string | Yes | Name |

```sh
curl -X POST "https://panel.example.com/api/v2/site-groups" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/site-groups/{id}`

Get site group · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/site-groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/site-groups/{id}`

Update site group · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `des` | string |  | Description |
| `name` | string | Yes | Name |

```sh
curl -X PUT "https://panel.example.com/api/v2/site-groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/site-groups/{id}`

Delete site group · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/site-groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Site groups object:

| Field | Type | Required | Description |
|---|---|---|---|
| `created_at` | string |  | Created at (read-only) |
| `des` | string |  | Description |
| `id` | integer |  | Record ID (read-only) |
| `name` | string |  | Name |
| `tenant_id` | integer |  | Owner tenant (user) ID (read-only) |
| `updated_at` | string |  | Updated at (read-only) |

## Cache groups (`cache-groups`)

A set of cache rules that many sites can reference; either platform groups or tenant-owned groups.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/cache-groups

### GET `/api/v2/cache-groups`

List cache groups · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

```sh
curl "https://panel.example.com/api/v2/cache-groups" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/cache-groups`

Create cache group · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `cache` | CacheConfig |  | Cache rules, same as the cache field ({rules, ignore_params}) of the node contract schema/site.json; validated by the server against the contract. |
| `des` | string |  | Description |
| `from_site_id` | integer |  | Save from a site (rules are taken from the site's current effective value; cache is ignored). |
| `is_default` | boolean |  | Platform default group (exactly one). |
| `name` | string | Yes | Name |
| `sort` | integer |  | Display order (smaller first). |
| `switch_site` | boolean |  | After saving, make the site reference the new group. |

```sh
curl -X POST "https://panel.example.com/api/v2/cache-groups" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/cache-groups/{id}`

Get cache group · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/cache-groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/cache-groups/{id}`

Update cache group · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `cache` | CacheConfig |  | Cache rules, same as the cache field ({rules, ignore_params}) of the node contract schema/site.json; validated by the server against the contract. |
| `des` | string |  | Description |
| `name` | string |  | Name |
| `sort` | integer |  | Display order (smaller first). |

```sh
curl -X PUT "https://panel.example.com/api/v2/cache-groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/cache-groups/{id}`

Delete cache group · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/cache-groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/cache-groups/{id}/copy`

Copy cache group · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `des` | string |  | Description |
| `name` | string | Yes | Name |
| `sort` | integer |  | Display order (smaller first). |

```sh
curl -X POST "https://panel.example.com/api/v2/cache-groups/{id}/copy" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/cache-groups/{id}/set-default`

Set as platform default group · Authentication: Admin only

```sh
curl -X POST "https://panel.example.com/api/v2/cache-groups/{id}/set-default" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Cache groups object:

| Field | Type | Required | Description |
|---|---|---|---|
| `cache` | CacheConfig |  | Cache rules, same as the cache field ({rules, ignore_params}) of the node contract schema/site.json; validated by the server against the contract. |
| `created_at` | string |  | Created at (read-only) |
| `des` | string |  | Description |
| `from_site_id` | integer |  | Save from a site (rules are taken from the site's current effective value; cache is ignored). |
| `id` | integer |  | Record ID (read-only) |
| `is_default` | boolean |  | Platform default group (exactly one). (read-only) |
| `name` | string |  | Name |
| `site_count` | integer |  | Number of sites that reference it explicitly (tenant callers only count their own). (read-only) |
| `sort` | integer |  | Display order (smaller first). |
| `switch_site` | boolean |  | After saving, make the site reference the new group. |
| `tenant_id` | integer |  | Owning tenant; null = platform group. (read-only) |
| `updated_at` | string |  | Updated at (read-only) |

## Cache policy (`cache-policy`)

Sets the tenant's default cache group and can reset all sites to follow it.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/cache-policy

### GET `/api/v2/cache-policy`

Cache policy · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

```sh
curl "https://panel.example.com/api/v2/cache-policy" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/cache-policy`

Set the tenant's default group · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `default_group_id` | integer | Yes | Platform default group; null = no group yet. |

```sh
curl -X PUT "https://panel.example.com/api/v2/cache-policy" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/cache-policy/reset-sites`

Reset all sites to the default group · Authentication: User (some parameters admin only)

```sh
curl -X POST "https://panel.example.com/api/v2/cache-policy/reset-sites" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Cache policy object:

| Field | Type | Required | Description |
|---|---|---|---|
| `applied` | integer |  | Number of sites updated. |
| `default_group_id` | integer |  | Platform default group; null = no group yet. |
| `my_default_group_id` | integer |  | Default group in effect for this tenant (the one it set, or the platform default if none). |
| `my_default_group_set` | boolean |  | Whether this tenant has set its own default group. |
| `sites` | object |  | Number of the tenant's sites by cache mode: group (follows a cache group) and custom (own rules). |

## Cache presets (`cache-presets`)

Factory default cache rules, usable as a reference when creating a cache group.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/cache-presets

### GET `/api/v2/cache-presets`

Factory default cache rules · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/cache-presets" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Cache presets object:

| Field | Type | Required | Description |
|---|---|---|---|
| `cache` | CacheConfig |  | Cache rules, same as the cache field ({rules, ignore_params}) of the node contract schema/site.json; validated by the server against the contract. |

## Site cache mode (`cache-sites`)

View or set the cache group or custom rules used by one site.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/cache-sites

### GET `/api/v2/cache-sites/{id}`

Site cache mode · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/cache-sites/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/cache-sites/{id}`

Set the cache group used by a site · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `expected_updated_at` | string |  | Optimistic lock (used on update) - the updated_at of the resource as the caller read it. If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check; ignored on create. |
| `group_id` | integer | Yes | Group referenced explicitly; null = follows the default group. |

```sh
curl -X PUT "https://panel.example.com/api/v2/cache-sites/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Site cache mode object:

| Field | Type | Required | Description |
|---|---|---|---|
| `effective_group_id` | integer |  | Group actually in effect when mode=group; null = no usable group (no cache rules are pushed). |
| `expected_updated_at` | string |  | Optimistic lock (used on update) - the updated_at of the resource as the caller read it. If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check; ignored on create. |
| `group_id` | integer |  | Group referenced explicitly; null = follows the default group. |
| `mode` | string |  | Cache mode: group = follows a cache group; custom = site's own rules. Values: ["group","custom"] |
| `site_id` | integer |  | Site ID |
| `updated_at` | string |  | updated_at of the site (the version of the site row, also bumped when the cache mode changes); send it back as expected_updated_at when changing the cache mode. (read-only) |

## Purge and prefetch (`cache-jobs`)

Purge cached URLs or directories, and prefetch given URLs.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/cache-jobs

### GET `/api/v2/cache-jobs`

List purge/prefetch jobs · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `type` | string |  | Only list jobs of this type. Values: ["purge_url","purge_dir","prefetch"] |
| `url` | string |  | If it starts with http:// or https://, exact match on the full URL, normalized as on submission (the job's URL list contains it); otherwise exact match on the host name. |

```sh
curl "https://panel.example.com/api/v2/cache-jobs" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/cache-jobs`

Create purge/prefetch job · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `type` | string | Yes | purge_url = purge URLs; purge_dir = purge directories (URL ends with /); prefetch = prefetch. Values: ["purge_url","purge_dir","prefetch"] |
| `urls` | string[] | Yes | http(s)://host/path (normalized, see create); at most 100 entries per prefetch. |

```sh
curl -X POST "https://panel.example.com/api/v2/cache-jobs" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"purge_url","urls":["https://www.example.com/index.html"]}'
```

### GET `/api/v2/cache-jobs/{id}`

Get purge/prefetch job · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/cache-jobs/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Purge and prefetch object:

| Field | Type | Required | Description |
|---|---|---|---|
| `created_at` | string |  | Created at (read-only) |
| `id` | integer |  | Record ID (read-only) |
| `results` | CacheJobNodeResult[] |  | Execution result of each node (returned in details only). (read-only) |
| `site_id` | integer |  | Site ID (read-only) |
| `status` | string |  | Status of the push task (if some nodes fail it stays pending and retries with backoff, then becomes failed when retries run out). Values: ["pending","running","done","failed"] (read-only) |
| `tenant_id` | integer |  | Owner tenant (user) ID (read-only) |
| `type` | string |  | purge_url = purge URLs; purge_dir = purge directories (URL ends with /); prefetch = prefetch. Values: ["purge_url","purge_dir","prefetch"] |
| `updated_at` | string |  | Updated at (read-only) |
| `urls` | string[] |  | http(s)://host/path (normalized, see create); at most 100 entries per prefetch. |

## Purge and prefetch quota (`cache-quota`)

Daily purge and prefetch limits and today's usage.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/cache-quota

### GET `/api/v2/cache-quota`

Daily purge/prefetch quota and today's usage · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/cache-quota" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Purge and prefetch quota object:

| Field | Type | Required | Description |
|---|---|---|---|
| `items` | CacheQuotaItem[] |  | Items on this page |

## TCP/UDP forwarding (`streams`)

TCP/UDP port forwarding rules. Each rule has listen ports and protocols, an origin list and a balancing method; (protocol, port) is unique platform-wide.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/streams

### GET `/api/v2/streams`

List streams · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `port` | integer |  | Only list forwarding rules listening on this port. |
| `group_id` | integer |  | Only list rules in this group. |
| `upid` | integer |  | Only list rules under this user package. |
| `enabled` | boolean |  | Filter by enabled state. |
| `id` | integer |  | Filter by exact forwarding rule ID. |
| `origin` | string |  | Filter by substring of the origin IP. |
| `protocol` | string |  | Only list rules that have a listener with this protocol. Values: ["tcp","udp"] |
| `stop_code` | integer |  | Filter by stop code; 0 = normal. |
| `package_id` | integer |  | Filter by base package (the owning user package was bought from it). |
| `region_id` | integer |  | Filter by region (the CNAME's primary line group belongs to it); owners without their own CNAME use their user package's shared one. |
| `node_group_id` | integer |  | Filter by line group (the CNAME's primary or backup line group). |
| `cname_domain_id` | integer |  | Filter by CNAME root domain. |
| `cname_hostname` | string |  | Filter by CNAME host label, exact match (case-insensitive, without the root domain). |
| `sync_state` | string |  | Filter by config sync state: behind = some online node has not yet installed the config version containing its latest change; Values: ["behind","synced"] |

```sh
curl "https://panel.example.com/api/v2/streams" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/streams`

Create stream · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Owner tenant when an admin creates on behalf of a tenant (required); ignored for tenant callers, where the owner is always the caller. |

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `acl` | StreamAcl |  | Layer-4 access control. Rules match the source IP or CIDR in order and the first hit decides; if none match, default_action applies; deny drops the connection immediately. |
| `balance` | StreamBalance |  | Load balancing method. rr is round robin, ip_hash hashes the source IP, least_conn picks the fewest connections, random picks at random. Values: ["rr","ip_hash","least_conn","random"] |
| `cname` | CnameBinding |  | Access name (CNAME). Users point their domain to fqdn with a CNAME record; the platform generates its DNS records from the node group. null when there is no access name. |
| `conn_limit` | integer |  | Per-IP concurrent connection limit; 0 = unlimited. |
| `enabled` | boolean |  | Whether enabled |
| `expected_updated_at` | string |  | Optimistic lock (used on update) - the updated_at of the resource as the caller read it. If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check; ignored on create. |
| `group_ids` | integer[] |  | IDs of the forwarding groups this rule belongs to. |
| `listen` | StreamListen[] | Yes | Listener list. Each item is a port and a protocol. |
| `name` | string |  | Name |
| `origin_port` | integer | Yes | Origin port to connect to. |
| `origins` | StreamOrigin[] | Yes | Origin list. Each item has an origin IP, a weight, and backup/down flags. |
| `proxy_protocol` | boolean |  | Whether to send a PROXY protocol header to the origin. |
| `stop_code` | integer |  | 0 = normal; non-zero = new connections are dropped immediately (set by admin or billing; 512 in the snapshot when the user package has expired). |
| `udp_responses` | integer |  | UDP responses. 1-100 = end each UDP session after this many packets from the origin (instead of waiting for the UDP idle timeout); 0 = not set. Only for request/response traffic (DNS, probes); do not set it for games or voice that send many replies per request, or their sessions get cut short. |
| `upid` | integer |  | User package ID (empty = no quota check; empty falls back to tenant_id in the snapshot). |

```sh
curl -X POST "https://panel.example.com/api/v2/streams" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"listen":[{"port":30001}],"name":"demo","origin_port":22,"origins":[{"host":"203.0.113.10"}]}'
```

### POST `/api/v2/streams/batch`

Batch operate streams · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Owner tenant when an admin creates on behalf of a tenant (required); ignored for tenant callers, where the owner is always the caller. |

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | string | Yes | Operation. set_enabled toggles enabled state, delete removes, set_groups replaces groups, update edits fields in bulk, create adds new ones, set_lines changes the CNAME's primary/backup line groups (admin only), set_cname_domain changes the CNAME root domain (admin only, host label kept). Values: ["set_enabled","delete","set_groups","update","create","set_lines","set_cname_domain"] |
| `backup_group_id` | integer |  | Used by set_lines: backup line group; 0 clears it, omitted leaves it unchanged. |
| `cname_domain_id` | integer |  | Used by set_cname_domain: the new CNAME root domain. |
| `enabled` | boolean |  | Whether enabled |
| `group_ids` | integer[] |  | IDs of the forwarding groups this rule belongs to. |
| `ids` | integer[] |  | IDs of the forwarding rules to operate on, at most 100 per call (required by every action except create). |
| `items` | StreamWrite[] |  | Used by create: the forwarding rules to create (up to 100), each shaped like the POST /streams request body. |
| `node_group_id` | integer |  | Used by set_lines: primary line group (required). |
| `patch` | StreamPatch |  | For batch update. Fields that are given are applied to every rule, with the same rules as a single update. |
| `region_id` | integer |  | Used by set_lines: region; when given, both line groups must belong to it. |

```sh
curl -X POST "https://panel.example.com/api/v2/streams/batch" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"action":"set_enabled","enabled":false,"ids":[1]}'
```

### GET `/api/v2/streams/{id}`

Get stream · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/streams/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/streams/{id}`

Update stream · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `acl` | StreamAcl |  | Layer-4 access control. Rules match the source IP or CIDR in order and the first hit decides; if none match, default_action applies; deny drops the connection immediately. |
| `balance` | StreamBalance |  | Load balancing method. rr is round robin, ip_hash hashes the source IP, least_conn picks the fewest connections, random picks at random. Values: ["rr","ip_hash","least_conn","random"] |
| `cname` | CnameBinding |  | Access name (CNAME). Users point their domain to fqdn with a CNAME record; the platform generates its DNS records from the node group. null when there is no access name. |
| `conn_limit` | integer |  | Per-IP concurrent connection limit; 0 = unlimited. |
| `enabled` | boolean |  | Whether enabled |
| `expected_updated_at` | string |  | Optimistic lock (used on update) - the updated_at of the resource as the caller read it. If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check; ignored on create. |
| `group_ids` | integer[] |  | IDs of the forwarding groups this rule belongs to. |
| `listen` | StreamListen[] | Yes | Listener list. Each item is a port and a protocol. |
| `name` | string |  | Name |
| `origin_port` | integer | Yes | Origin port to connect to. |
| `origins` | StreamOrigin[] | Yes | Origin list. Each item has an origin IP, a weight, and backup/down flags. |
| `proxy_protocol` | boolean |  | Whether to send a PROXY protocol header to the origin. |
| `stop_code` | integer |  | 0 = normal; non-zero = new connections are dropped immediately (set by admin or billing; 512 in the snapshot when the user package has expired). |
| `udp_responses` | integer |  | UDP responses. 1-100 = end each UDP session after this many packets from the origin (instead of waiting for the UDP idle timeout); 0 = not set. Only for request/response traffic (DNS, probes); do not set it for games or voice that send many replies per request, or their sessions get cut short. |
| `upid` | integer |  | User package ID (empty = no quota check; empty falls back to tenant_id in the snapshot). |

```sh
curl -X PUT "https://panel.example.com/api/v2/streams/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"listen":[{"port":30001}],"name":"demo","origin_port":22,"origins":[{"host":"203.0.113.10"}]}'
```

### DELETE `/api/v2/streams/{id}`

Delete stream · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/streams/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

TCP/UDP forwarding object:

| Field | Type | Required | Description |
|---|---|---|---|
| `acl` | StreamAcl |  | Layer-4 access control. Rules match the source IP or CIDR in order and the first hit decides; if none match, default_action applies; deny drops the connection immediately. |
| `action` | string | Yes | Operation. set_enabled toggles enabled state, delete removes, set_groups replaces groups, update edits fields in bulk, create adds new ones, set_lines changes the CNAME's primary/backup line groups (admin only), set_cname_domain changes the CNAME root domain (admin only, host label kept). Values: ["set_enabled","delete","set_groups","update","create","set_lines","set_cname_domain"] |
| `affected` | integer |  | Number of affected records |
| `backup_group_id` | integer |  | Used by set_lines: backup line group; 0 clears it, omitted leaves it unchanged. |
| `balance` | StreamBalance |  | Load balancing method. rr is round robin, ip_hash hashes the source IP, least_conn picks the fewest connections, random picks at random. Values: ["rr","ip_hash","least_conn","random"] |
| `cname` | CnameBinding |  | Access name (CNAME). Users point their domain to fqdn with a CNAME record; the platform generates its DNS records from the node group. null when there is no access name. |
| `cname_domain_id` | integer |  | Used by set_cname_domain: the new CNAME root domain. |
| `conn_limit` | integer |  | Per-IP concurrent connection limit; 0 = unlimited. |
| `created_at` | string |  | Created at (read-only) |
| `enabled` | boolean |  | Whether enabled |
| `expected_updated_at` | string |  | Optimistic lock (used on update) - the updated_at of the resource as the caller read it. If it differs from the stored one, 409 stale_version is returned and nothing is saved; omit to skip the check; ignored on create. |
| `group_ids` | integer[] |  | IDs of the forwarding groups this rule belongs to. |
| `id` | integer |  | Record ID (read-only) |
| `ids` | integer[] |  | IDs of the forwarding rules to operate on, at most 100 per call (required by every action except create). |
| `items` | StreamWrite[] |  | Used by create: the forwarding rules to create (up to 100), each shaped like the POST /streams request body. |
| `listen` | StreamListen[] |  | Listener list. Each item is a port and a protocol. |
| `name` | string |  | Name |
| `node_group_id` | integer |  | Used by set_lines: primary line group (required). |
| `node_issues` | NodeIssue[] |  | Ports or reasons for which the forward is not fully in effect on online nodes (in the list and the detail); omitted when everything is in effect. (read-only) |
| `origin_port` | integer |  | Origin port to connect to. |
| `origins` | StreamOrigin[] |  | Origin list. Each item has an origin IP, a weight, and backup/down flags. |
| `patch` | StreamPatch |  | For batch update. Fields that are given are applied to every rule, with the same rules as a single update. |
| `proxy_protocol` | boolean |  | Whether to send a PROXY protocol header to the origin. |
| `region_id` | integer |  | Used by set_lines: region; when given, both line groups must belong to it. |
| `stop_code` | integer |  | 0 = normal; non-zero = new connections are dropped immediately (set by admin or billing; 512 in the snapshot when the user package has expired). |
| `tenant_id` | integer |  | Owner tenant (user) ID (read-only) |
| `udp_responses` | integer |  | UDP responses. 1-100 = end each UDP session after this many packets from the origin (instead of waiting for the UDP idle timeout); 0 = not set. Only for request/response traffic (DNS, probes); do not set it for games or voice that send many replies per request, or their sessions get cut short. |
| `updated_at` | string |  | Updated at (read-only) |
| `upid` | integer |  | User package ID (empty = no quota check; empty falls back to tenant_id in the snapshot). |

## Forwarding groups (`stream-groups`)

Groups that organize forwarding rules for bulk management. Group names are unique per tenant, and a group that still has rules cannot be deleted.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/stream-groups

### GET `/api/v2/stream-groups`

List forwarding groups · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

```sh
curl "https://panel.example.com/api/v2/stream-groups" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/stream-groups`

Create forwarding group · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Owner tenant when an admin creates on behalf of a tenant (required); ignored for tenant callers, where the owner is always the caller. |

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `des` | string |  | Description |
| `name` | string | Yes | Name |

```sh
curl -X POST "https://panel.example.com/api/v2/stream-groups" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/stream-groups/{id}`

Get forwarding group · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/stream-groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/stream-groups/{id}`

Update forwarding group · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `des` | string |  | Description |
| `name` | string | Yes | Name |

```sh
curl -X PUT "https://panel.example.com/api/v2/stream-groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/stream-groups/{id}`

Delete forwarding group · Authentication: User (some parameters admin only)

```sh
curl -X DELETE "https://panel.example.com/api/v2/stream-groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Forwarding groups object:

| Field | Type | Required | Description |
|---|---|---|---|
| `created_at` | string |  | Created at (read-only) |
| `des` | string |  | Description |
| `id` | integer |  | Record ID (read-only) |
| `name` | string |  | Name |
| `tenant_id` | integer |  | Owner tenant (user) ID (read-only) |
| `updated_at` | string |  | Updated at (read-only) |

## Tiered origin (L2) (`l2`)

Edges fetch through L2 nodes in their region first, so cached content no longer hits the origin. Paid feature l2; inactive when not licensed.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/l2

### POST `/api/v2/l2/enable-batch`

Add L2 nodes in batch · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `apply_sites` | boolean |  | Also enable on existing sites (only those never set; sites turned off manually, or whose tenant package does not include tiered origin, stay unchanged). |
| `node_ids` | integer[] | Yes | Edge node IDs to turn into L2. |

```sh
curl -X POST "https://panel.example.com/api/v2/l2/enable-batch" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/l2/groups`

List L2 groups · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/l2/groups" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/l2/groups`

Create L2 group · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `all_regions` | boolean |  | Serve all regions (requires cross_region). |
| `balance` | string |  | Distribution: url_hash pins by URL, rr round-robin. Values: ["url_hash","rr"] |
| `cross_region` | boolean |  | Allow cross-region use (required for nodes outside the regions or all regions; adds about 50-200 ms). |
| `name` | string | Yes | Group name, unique platform-wide. |
| `node_ids` | integer[] |  | Edge node IDs to turn into L2. |
| `regions` | integer[] |  | Served region IDs; ignored when all_regions is true. |
| `rules` | L2Rule[] |  | Group rules (saved with the group). |

```sh
curl -X POST "https://panel.example.com/api/v2/l2/groups" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/l2/groups/{id}`

Get L2 group · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/l2/groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PATCH `/api/v2/l2/groups/{id}`

Update L2 group · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `all_regions` | boolean |  | Serve all regions (requires cross_region). |
| `balance` | string |  | Distribution: url_hash pins by URL, rr round-robin. Values: ["url_hash","rr"] |
| `cross_region` | boolean |  | Allow cross-region use (required for nodes outside the regions or all regions; adds about 50-200 ms). |
| `name` | string |  | Group name, unique platform-wide. |
| `node_ids` | integer[] |  | Edge node IDs to turn into L2. |
| `regions` | integer[] |  | Served region IDs; ignored when all_regions is true. |
| `rules` | L2Rule[] |  | Group rules (saved with the group). |

```sh
curl -X PATCH "https://panel.example.com/api/v2/l2/groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/l2/groups/{id}`

Delete L2 group · Authentication: Admin only

```sh
curl -X DELETE "https://panel.example.com/api/v2/l2/groups/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/l2/groups/{id}/copy`

Copy L2 group · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Group name, unique platform-wide. |

```sh
curl -X POST "https://panel.example.com/api/v2/l2/groups/{id}/copy" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/l2/groups/{id}/reset-sites`

Reset sites of a group to region default · Authentication: Admin only

```sh
curl -X POST "https://panel.example.com/api/v2/l2/groups/{id}/reset-sites" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/l2/nodes`

List L2 nodes · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/l2/nodes" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PATCH `/api/v2/l2/nodes/{id}`

Update L2 node · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `access_addr` | string |  | Private address edges use to reach it (IP only, no port; IPv4-mapped IPv6 is stored as IPv4); empty means the public IP. |
| `role` | string |  | Primary or backup: backups stand by while a primary in the same group is up. Values: ["primary","backup"] |
| `weight` | integer |  | Weight 1-100; multiple primaries share URLs by weight. |

```sh
curl -X PATCH "https://panel.example.com/api/v2/l2/nodes/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/l2/nodes/{id}/disable`

Disable L2 node · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `reason` | string |  | Reason for disabling. |

```sh
curl -X POST "https://panel.example.com/api/v2/l2/nodes/{id}/disable" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/l2/nodes/{id}/enable`

Enable L2 node · Authentication: Admin only

```sh
curl -X POST "https://panel.example.com/api/v2/l2/nodes/{id}/enable" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/l2/nodes/{id}/impact`

Impact before revert or disable · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/l2/nodes/{id}/impact" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/l2/nodes/{id}/mark-allowed`

Mark origins as allowing the node · Authentication: Admin only

```sh
curl -X POST "https://panel.example.com/api/v2/l2/nodes/{id}/mark-allowed" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/l2/nodes/{id}/probe-access`

Check whether origins allow the node · Authentication: Admin only

```sh
curl -X POST "https://panel.example.com/api/v2/l2/nodes/{id}/probe-access" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/l2/nodes/{id}/revert`

Revert to edge node · Authentication: Admin only

```sh
curl -X POST "https://panel.example.com/api/v2/l2/nodes/{id}/revert" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/l2/pause`

Pause tiered origin network-wide · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `auto_resume_at` | string |  | Automatic resume time (must be in the future); omit for manual resume. |

```sh
curl -X POST "https://panel.example.com/api/v2/l2/pause" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/l2/recommend`

One-click enable recommendation · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/l2/recommend" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/l2/resume`

Resume tiered origin network-wide · Authentication: Admin only

```sh
curl -X POST "https://panel.example.com/api/v2/l2/resume" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/l2/revoke-impact`

License loss impact and fallback window · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |

```sh
curl "https://panel.example.com/api/v2/l2/revoke-impact" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/l2/stats`

Tiered origin statistics · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |

```sh
curl "https://panel.example.com/api/v2/l2/stats" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/l2/stats/nodes`

Statistics by L2 node · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |

```sh
curl "https://panel.example.com/api/v2/l2/stats/nodes" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/l2/stats/sites`

Tiered origin statistics by site · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `domain` | string |  | Substring of the primary domain, case-insensitive. |

```sh
curl "https://panel.example.com/api/v2/l2/stats/sites" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/l2/status`

Tiered origin overview · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/l2/status" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/l2/tasks/{id}`

Get L2 task progress · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/l2/tasks/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Tiered origin (L2) object:

| Field | Type | Required | Description |
|---|---|---|---|
| `access_addr` | string |  | Private address edges use to reach it (IP only, no port; IPv4-mapped IPv6 is stored as IPv4); empty means the public IP. |
| `all_regions` | boolean |  | Serve all regions (requires cross_region). |
| `allow` | string |  | Origin allowlisting: empty if not applicable, pending (cacheable requests only), or allowed. |
| `allow_since` | string |  | Pending since; turns allowed after 24 hours. |
| `apply_sites` | boolean |  | Also enable on existing sites (only those never set; sites turned off manually, or whose tenant package does not include tiered origin, stay unchanged). |
| `auto_resume_at` | string |  | Automatic resume time (must be in the future); omit for manual resume. |
| `balance` | string |  | Distribution: url_hash pins by URL, rr round-robin. Values: ["url_hash","rr"] |
| `checked_at` | string |  | Checked at. |
| `created_at` | string |  | Started at. |
| `cross_region` | boolean |  | Allow cross-region use (required for nodes outside the regions or all regions; adds about 50-200 ms). |
| `defaults` | L2Defaults |  | Defaults (change via PUT /configs/l2.defaults). |
| `disabled_at` | string |  | Disabled at. |
| `disabled_reason` | string |  | Reason for disabling. |
| `edge_origin_requests` | integer |  | Edge origin requests (hit rate denominator). |
| `empty_group` | integer[] |  | Groups left without a serving L2 (their sites will bypass temporarily). |
| `enabled` | boolean |  | Enabled; a disabled node stays L2 and keeps its cache. |
| `feature` | string |  | Paid feature l2 level: on; limited when frozen, being revoked, or in the fallback window after the license lapses (still pushed, management only; see GET /l2/revoke-impact); off when not licensed (not pushed). Values: ["on","limited","off"] |
| `finished_at` | string |  | Finished at. |
| `groups` | integer[] |  | IDs of L2 groups containing it. |
| `hit_rate` | number |  | Hit rate 0-1; 0 when there were no origin requests. |
| `host` | string |  | Primary domain of the site. |
| `id` | integer |  | Group ID. |
| `ip` | string |  | Public IP. |
| `is_region_default` | boolean |  | Region default group (one per region; cannot be deleted, only its distribution can change). |
| `items` | L2Group[] |  | L2 groups. |
| `kind` | string |  | Task kind: enable adds L2, revert returns to edge. Values: ["enable","revert"] |
| `l2_group_id` | integer |  | L2 group chosen by the site; null means region default. |
| `l2_group_name` | string |  | L2 group name; empty for region default. |
| `l2_hit_requests` | integer |  | Origin requests served by L2 (hit rate numerator). |
| `l2_pick` | string |  | Effective L2 pick method (site value, else the default): region or near. Values: ["region","near"] |
| `low_hit` | boolean |  | Hit rate below 30% with at least 1000 origin requests (mostly dynamic content; consider turning L2 off). |
| `manual_off_sites` | integer |  | Sites whose tiered origin was turned off manually (left alone when enabling on existing sites; IP sites excluded). |
| `name` | string | Yes | Group name, unique platform-wide. |
| `node_id` | integer |  | Node ID. |
| `node_ids` | integer[] | Yes | Edge node IDs to turn into L2. |
| `nodes_pending` | integer |  | L2 nodes awaiting origin allowlisting. |
| `nodes_serving` | integer |  | L2 nodes serving (enabled and not reverting). |
| `nodes_total` | integer |  | Total L2 nodes (including disabled and reverting). |
| `online` | boolean |  | Online (heartbeat within 2 minutes). |
| `only_block` | boolean |  | Some sites allow only L2 at the origin (revoke its allowlisting after revert). |
| `only_block_sites` | integer |  | Sites with origin allowing only L2 (greater than 0 when only_block is true). |
| `origin_l2_only` | boolean |  | Origin allows only L2; after the window ends the site goes down unless the origin allows edge nodes. |
| `origin_requests` | integer |  | Requests that reached the origin. |
| `origin_requests_est` | integer |  | Estimate without tiering (= edge origin requests). |
| `owner_id` | integer |  | Owner user ID. |
| `owner_name` | string |  | Owner username. |
| `paused` | boolean |  | Paused network-wide. |
| `paused_at` | string |  | Paused at. |
| `progress` | integer |  | Progress 0-100. |
| `reason` | string |  | Reason for disabling. |
| `region_id` | integer |  | Region of a region default group; 0 for custom groups. |
| `region_last` | boolean |  | It is the last enabled L2 in its region. |
| `regions` | integer[] |  | Served region IDs; ignored when all_regions is true. |
| `revert_at` | string |  | Revert started at. |
| `role` | string |  | Primary or backup: backups stand by while a primary in the same group is up. Values: ["primary","backup"] |
| `rules` | L2Rule[] |  | Group rules (saved with the group). |
| `saved_bytes` | integer |  | Origin traffic saved (bytes served from L2 cache). |
| `site_id` | integer |  | Site ID. |
| `sites` | integer |  | Number of sites using this group; for a region default group, the sites with L2 enabled and no group chosen (same number for every region default group). |
| `sites_only_l2` | integer |  | Sites with origin allowing only L2 (shown in the pause and add-node warnings). |
| `state` | string |  | State: active, or reverting to edge. Values: ["active","reverting"] |
| `thresholds` | L2Thresholds |  | Detection thresholds (read-only). |
| `updated_at` | string |  | Last modified time. |
| `weight` | integer |  | Weight 1-100; multiple primaries share URLs by weight. |

## Messages (`messages`)

In-app messages and announcements; admins publish announcements and tenants read and mark them as read.

Authentication: User · Docs: https://www-demo.kuxitong.com/en/docs/api/messages

### GET `/api/v2/messages`

List messages · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `type` | string |  | Message type, such as announcement or cert-expire. |
| `site_id` | integer |  | Site ID |

```sh
curl "https://panel.example.com/api/v2/messages" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/messages`

Publish announcement · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `content` | string |  | Message body. |
| `is_bold` | boolean |  | Whether to show the title in bold. |
| `is_external` | boolean |  | When true, clicking jumps to url and url is required; otherwise content is required. |
| `is_popup` | boolean |  | Whether to show it as a pop-up. |
| `is_red` | boolean |  | Whether to show the title in red. |
| `is_show` | boolean |  | Whether the message is visible. |
| `sort` | integer |  | Display order (smaller first). |
| `title` | string | Yes | Message title. |
| `url` | string |  | Link opened on click when is_external is true. |

```sh
curl -X POST "https://panel.example.com/api/v2/messages" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content":"今晚 23 点维护。","title":"维护通知"}'
```

### POST `/api/v2/messages/read`

Mark messages as read · Authentication: User

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `ids` | integer[] |  | Empty means all messages sent to you. |

```sh
curl -X POST "https://panel.example.com/api/v2/messages/read" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/messages/recent`

Recent unread messages · Authentication: User

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `days` | integer |  | Only messages from the last this many days. Default: 30 |
| `limit` | integer |  | Maximum number of messages to return. Default: 8 |

```sh
curl "https://panel.example.com/api/v2/messages/recent" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/messages/unread-count`

Unread message count · Authentication: User

```sh
curl "https://panel.example.com/api/v2/messages/unread-count" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/messages/{id}`

Get message · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/messages/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/messages/{id}`

Update message · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `content` | string |  | Message body. |
| `is_bold` | boolean |  | Whether to show the title in bold. |
| `is_external` | boolean |  | When true, clicking jumps to url and url is required; otherwise content is required. |
| `is_popup` | boolean |  | Whether to show it as a pop-up. |
| `is_red` | boolean |  | Whether to show the title in red. |
| `is_show` | boolean |  | Whether the message is visible. |
| `sort` | integer |  | Display order (smaller first). |
| `title` | string |  | Message title. |
| `url` | string |  | Link opened on click when is_external is true. |

```sh
curl -X PUT "https://panel.example.com/api/v2/messages/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/messages/{id}`

Delete message · Authentication: Admin only

```sh
curl -X DELETE "https://panel.example.com/api/v2/messages/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Messages object:

| Field | Type | Required | Description |
|---|---|---|---|
| `content` | string |  | Message body. |
| `count` | integer |  | Number of unread messages. |
| `created_at` | string |  | Created at (read-only) |
| `id` | integer |  | Record ID (read-only) |
| `ids` | integer[] |  | Empty means all messages sent to you. |
| `is_bold` | boolean |  | Whether to show the title in bold. |
| `is_external` | boolean |  | When true, clicking jumps to url and url is required; otherwise content is required. |
| `is_popup` | boolean |  | Whether to show it as a pop-up. |
| `is_red` | boolean |  | Whether to show the title in red. |
| `is_show` | boolean |  | Whether the message is visible. |
| `items` | Message[] |  | Items on this page |
| `read` | boolean |  | Whether the message has been read. (read-only) |
| `site_id` | integer |  | Site ID (read-only) |
| `sort` | integer |  | Display order (smaller first). |
| `tenant_id` | integer |  | Recipient tenant; empty for announcements. (read-only) |
| `title` | string |  | Message title. |
| `title_args` | object |  | Arguments of the title key: name → string, number, or a clause {key, args} (a clause is looked up in the dictionary and filled the same way, and may nest). (read-only) |
| `title_key` | string |  | Title key (the Chinese sentence with {name} placeholders). When set, the panel looks it up in the UI-language dictionary and fills the placeholders from title_args; when null (old messages, announcements, edited titles) the panel shows title as is. (read-only) |
| `type` | string |  | announcement = announcement; others are notification types (such as cert-expire, package-expire). (read-only) |
| `updated_at` | string |  | Updated at (read-only) |
| `url` | string |  | Link opened on click when is_external is true. |

## Message subscriptions (`message-subscriptions`)

Choose which notification types to receive.

Authentication: User · Docs: https://www-demo.kuxitong.com/en/docs/api/message-subscriptions

### GET `/api/v2/message-subscriptions`

Message subscriptions · Authentication: User

```sh
curl "https://panel.example.com/api/v2/message-subscriptions" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/message-subscriptions`

Update message subscriptions · Authentication: User

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `items` | SubscriptionItem[] | Yes | Items on this page |

```sh
curl -X PUT "https://panel.example.com/api/v2/message-subscriptions" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Message subscriptions object:

| Field | Type | Required | Description |
|---|---|---|---|
| `items` | SubscriptionItem[] |  | Items on this page |

## Delivery channels (`message-channels`)

Configure and test the email (SMTP) and SMS delivery channels.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/message-channels

### GET `/api/v2/message-channels`

Delivery channels · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/message-channels" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/message-channels`

Update delivery channels · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `sms` | SmsChannel | Yes | SMS channel settings. |
| `smtp` | SmtpChannel | Yes | SMTP email channel settings. |

```sh
curl -X PUT "https://panel.example.com/api/v2/message-channels" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/message-channels/test`

Test delivery channel · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `channel` | string | Yes | Channel to test (email or sms). Values: ["email","sms"] |
| `draft` | ChannelTestDraft |  | Unsaved channel settings; same shape as the save endpoint, omitted fields and blank secrets fall back to the stored values. |
| `template_id` | string |  | An approved template id is required for the Tencent Cloud SMS channel (not needed for SMSBao). |
| `to` | string | Yes | Recipient address or phone number. |

```sh
curl -X POST "https://panel.example.com/api/v2/message-channels/test" \
  -H "X-API-Key: $GOCDN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"channel":"email","to":"admin@example.com"}'
```

Delivery channels object:

| Field | Type | Required | Description |
|---|---|---|---|
| `channel` | string | Yes | Channel to test (email or sms). Values: ["email","sms"] |
| `draft` | ChannelTestDraft |  | Unsaved channel settings; same shape as the save endpoint, omitted fields and blank secrets fall back to the stored values. |
| `error` | string |  | Error message when the test failed. |
| `ok` | boolean |  | Whether the test send succeeded. |
| `sms` | SmsChannel |  | SMS channel settings. |
| `smtp` | SmtpChannel |  | SMTP email channel settings. |
| `template_id` | string |  | An approved template id is required for the Tencent Cloud SMS channel (not needed for SMSBao). |
| `to` | string | Yes | Recipient address or phone number. |

## Alert webhooks (`message-webhooks`)

DingTalk, Feishu, WeCom, Telegram and generic JSON alert outputs; several can be configured, and addresses and secrets show only the last 4 characters.

Authentication: Admin only · Docs: https://www-demo.kuxitong.com/en/docs/api/message-webhooks

### GET `/api/v2/message-webhooks`

List alert webhooks · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |

```sh
curl "https://panel.example.com/api/v2/message-webhooks" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/message-webhooks`

Create alert webhook · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `chat_id` | string |  | Telegram chat (numeric id or @channel name). |
| `created_at` | string | Yes | Created at |
| `enabled` | boolean |  | Whether it is enabled; disabled ones are skipped. Enabled by default. |
| `id` | integer | Yes | Record ID |
| `kind` | string | Yes | Kind: dingtalk, feishu, wecom, telegram or generic JSON. Values: ["dingtalk","feishu","wecom","telegram","generic"] |
| `last4` | string |  | Last 4 characters of the address (the token for Telegram), for recognition only. |
| `name` | string | Yes | Name, unique across all webhooks. |
| `secret` | string |  | Bot token for Telegram (required); signing secret for the others (optional). Write-only, never echoed. |
| `updated_at` | string | Yes | Updated at |
| `url` | string |  | Bot URL (https, public; required for DingTalk, Feishu, WeCom and generic; unused for Telegram). Write-only, never echoed. |

```sh
curl -X POST "https://panel.example.com/api/v2/message-webhooks" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/message-webhooks/test`

Test unsaved alert webhook · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `chat_id` | string |  | Telegram chat (numeric id or @channel name). |
| `id` | integer |  | Record ID |
| `kind` | string | Yes | Kind: dingtalk, feishu, wecom, telegram or generic JSON. Values: ["dingtalk","feishu","wecom","telegram","generic"] |
| `secret` | string |  | Bot token for Telegram (required); signing secret for the others (optional). Write-only, never echoed. |
| `url` | string |  | Bot URL (https, public; required for DingTalk, Feishu, WeCom and generic; unused for Telegram). Write-only, never echoed. |

```sh
curl -X POST "https://panel.example.com/api/v2/message-webhooks/test" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/message-webhooks/{id}`

Update alert webhook · Authentication: Admin only

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `chat_id` | string |  | Telegram chat (numeric id or @channel name). |
| `created_at` | string | Yes | Created at |
| `enabled` | boolean |  | Whether it is enabled; disabled ones are skipped. Enabled by default. |
| `id` | integer | Yes | Record ID |
| `kind` | string | Yes | Kind: dingtalk, feishu, wecom, telegram or generic JSON. Values: ["dingtalk","feishu","wecom","telegram","generic"] |
| `last4` | string |  | Last 4 characters of the address (the token for Telegram), for recognition only. |
| `name` | string | Yes | Name, unique across all webhooks. |
| `secret` | string |  | Bot token for Telegram (required); signing secret for the others (optional). Write-only, never echoed. |
| `updated_at` | string | Yes | Updated at |
| `url` | string |  | Bot URL (https, public; required for DingTalk, Feishu, WeCom and generic; unused for Telegram). Write-only, never echoed. |

```sh
curl -X PUT "https://panel.example.com/api/v2/message-webhooks/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### DELETE `/api/v2/message-webhooks/{id}`

Delete alert webhook · Authentication: Admin only

```sh
curl -X DELETE "https://panel.example.com/api/v2/message-webhooks/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/message-webhooks/{id}/test`

Test saved alert webhook · Authentication: Admin only

```sh
curl -X POST "https://panel.example.com/api/v2/message-webhooks/{id}/test" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Alert webhooks object:

| Field | Type | Required | Description |
|---|---|---|---|
| `chat_id` | string |  | Telegram chat (numeric id or @channel name). |
| `created_at` | string |  | Created at (read-only) |
| `enabled` | boolean |  | Whether it is enabled; disabled ones are skipped. Enabled by default. |
| `error` | string |  | Error message when the test failed. |
| `id` | integer |  | Record ID (read-only) |
| `kind` | string |  | Kind: dingtalk, feishu, wecom, telegram or generic JSON. Values: ["dingtalk","feishu","wecom","telegram","generic"] |
| `last4` | string |  | Last 4 characters of the address (the token for Telegram), for recognition only. (read-only) |
| `name` | string |  | Name, unique across all webhooks. |
| `ok` | boolean |  | Whether the test send succeeded. |
| `secret` | string |  | Bot token for Telegram (required); signing secret for the others (optional). Write-only, never echoed. |
| `updated_at` | string |  | Updated at (read-only) |
| `url` | string |  | Bot URL (https, public; required for DingTalk, Feishu, WeCom and generic; unused for Telegram). Write-only, never echoed. |

## Reports (`reports`)

Real-time curves, rankings and usage for sites and TCP/UDP forwarding, node monitoring curves and rankings, unconfigured-domain Top, temporary whitelist and access logs; tenants see only their own data.

Authentication: User (some parameters admin only) · Docs: https://www-demo.kuxitong.com/en/docs/api/reports

### GET `/api/v2/reports/access-log-export-quota`

Get today's access log download quota · Authentication: User

```sh
curl "https://panel.example.com/api/v2/reports/access-log-export-quota" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/access-log-exports`

List access log download requests · Authentication: User

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |

```sh
curl "https://panel.example.com/api/v2/reports/access-log-exports" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/reports/access-log-exports`

Request an access log download · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |
| `site_ids` | string |  | Only these sites: comma-separated positive integers, at most 200 after deduplication (more returns 422); for more than 200 sites send a filter (group_id / upid / tenant_ids) instead. Intersected with group_id, upid and tenant_ids; sites a tenant cannot see have no data. |
| `group_id` | integer |  | Only sites in this site group (expanded to a site set on the server and intersected with the other scope filters; a tenant only expands its own sites). |
| `upid` | integer |  | Only sites under this user package. |
| `tenant_ids` | string |  | Admin only: only these tenants, comma-separated positive integers, at most 200 after deduplication (more returns 422); tenant credentials sending it get 403. |
| `host` | string |  | Exact match on request Host (case-insensitive). |
| `server_port` | integer |  | Filter by access port. |
| `ip` | string |  | Exact match on client address. |
| `uri` | string |  | Filter by request URI prefix (must start with /). |
| `referer` | string |  | Exact, case-sensitive match on the Referer request header; an empty value (referer=) selects direct visits that sent no Referer. |
| `http_ver` | string |  | Filter by HTTP protocol version (same spelling as http_ver in the response). Values: ["1.0","1.1","2","3"] |
| `method` | string |  | Filter by request method (upper case, such as GET or POST). |
| `status` | integer |  | Filter by status code. |
| `node_id` | integer |  | Filter by node. |
| `cache_status` | string |  | Filter by cache status. Values: ["NONE","HIT","MISS","EXPIRED","STALE","UPDATING","REVALIDATED","BYPASS"] |
| `country` | string |  | Filter by client country (ISO 3166-1 alpha-2, e.g. CN). |
| `prov` | string |  | Filter by client province (first two digits of GB/T 2260, e.g. 44). |
| `isp` | string |  | Filter by client ISP (telecom, unicom, mobile, cbn, edu, cloud, other). |
| `blocked` | boolean |  | true = only blocked requests (action = block). |
| `block_reason` | string |  | Filter by block reason, same meaning as block_reason on a log row: region = blocked by region limit, region_log = region limit log-only hit (not blocked), acl = blocked by ACL, other = blocked by other protection. Values: ["region","region_log","acl","other"] |
| `status_class` | string |  | Filter by status class: 1xx / 2xx / 3xx / 4xx / 5xx; error = failed requests (status ≥ 400 and not a blocked request). Intersected with status when both are given. Values: ["1xx","2xx","3xx","4xx","5xx","error"] |
| `slow_ms` | integer |  | Only slow requests, i.e. request time (request_time_ms) greater than this many milliseconds. |
| `cache_status_not` | string |  | Exclude this cache status (negation of cache_status); the miss count in facets equals cache_status_not=HIT. Intersected with cache_status when both are given. Values: ["NONE","HIT","MISS","EXPIRED","STALE","UPDATING","REVALIDATED","BYPASS"] |
| `q` | string |  | Contains search: a hit on any of status code (exactly three digits matches the status; 1xx to 5xx matches the class), client IP (contains), URL (contains) or host (contains, case-insensitive) counts; intersected with the other filters. |

```sh
curl -X POST "https://panel.example.com/api/v2/reports/access-log-exports" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/access-log-exports/{id}`

Get an access log download request · Authentication: User

```sh
curl "https://panel.example.com/api/v2/reports/access-log-exports/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/access-log-exports/{id}/file`

Download the access log file · Authentication: User

```sh
curl "https://panel.example.com/api/v2/reports/access-log-exports/{id}/file" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/access-logs`

List access logs · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `sort` | string |  | Sort fields, comma-separated; prefix `-` for descending, e.g. `-created_at,id` |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |
| `cursor` | string |  | Time cursor; pass back next_cursor of the previous page (same sort and filters). page is ignored when set. |
| `site_ids` | string |  | Only these sites: comma-separated positive integers, at most 200 after deduplication (more returns 422); for more than 200 sites send a filter (group_id / upid / tenant_ids) instead. Intersected with group_id, upid and tenant_ids; sites a tenant cannot see have no data. |
| `group_id` | integer |  | Only sites in this site group (expanded to a site set on the server and intersected with the other scope filters; a tenant only expands its own sites). |
| `upid` | integer |  | Only sites under this user package. |
| `tenant_ids` | string |  | Admin only: only these tenants, comma-separated positive integers, at most 200 after deduplication (more returns 422); tenant credentials sending it get 403. |
| `host` | string |  | Exact match on request Host (case-insensitive). |
| `server_port` | integer |  | Filter by access port. |
| `ip` | string |  | Exact match on client address. |
| `uri` | string |  | Filter by request URI prefix (must start with /). |
| `referer` | string |  | Exact, case-sensitive match on the Referer request header; an empty value (referer=) selects direct visits that sent no Referer. |
| `http_ver` | string |  | Filter by HTTP protocol version (same spelling as http_ver in the response). Values: ["1.0","1.1","2","3"] |
| `method` | string |  | Filter by request method (upper case, such as GET or POST). |
| `status` | integer |  | Filter by status code. |
| `node_id` | integer |  | Filter by node. |
| `cache_status` | string |  | Filter by cache status. Values: ["NONE","HIT","MISS","EXPIRED","STALE","UPDATING","REVALIDATED","BYPASS"] |
| `country` | string |  | Filter by client country (ISO 3166-1 alpha-2, e.g. CN). |
| `prov` | string |  | Filter by client province (first two digits of GB/T 2260, e.g. 44). |
| `isp` | string |  | Filter by client ISP (telecom, unicom, mobile, cbn, edu, cloud, other). |
| `blocked` | boolean |  | true = only blocked requests (action = block). |
| `block_reason` | string |  | Filter by block reason, same meaning as block_reason on a log row: region = blocked by region limit, region_log = region limit log-only hit (not blocked), acl = blocked by ACL, other = blocked by other protection. Values: ["region","region_log","acl","other"] |
| `status_class` | string |  | Filter by status class: 1xx / 2xx / 3xx / 4xx / 5xx; error = failed requests (status ≥ 400 and not a blocked request). Intersected with status when both are given. Values: ["1xx","2xx","3xx","4xx","5xx","error"] |
| `slow_ms` | integer |  | Only slow requests, i.e. request time (request_time_ms) greater than this many milliseconds. |
| `cache_status_not` | string |  | Exclude this cache status (negation of cache_status); the miss count in facets equals cache_status_not=HIT. Intersected with cache_status when both are given. Values: ["NONE","HIT","MISS","EXPIRED","STALE","UPDATING","REVALIDATED","BYPASS"] |
| `q` | string |  | Contains search: a hit on any of status code (exactly three digits matches the status; 1xx to 5xx matches the class), client IP (contains), URL (contains) or host (contains, case-insensitive) counts; intersected with the other filters. |
| `facets` | boolean |  | true / 1 = include facets (counts for each quick filter) in the response. Counted under all filters except the quick ones (status_class, slow_ms, cache_status_not, blocked, block_reason), so they do not move with the quick filter. |
| `total` | boolean |  | false / 0 = skip counting the total (the response omits total, saving one full count); defaults to true. |

```sh
curl "https://panel.example.com/api/v2/reports/access-logs" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/checkup`

Site checkup overview · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `q` | string |  | Search by site name or domain (substring match). |
| `sort` | string |  | Sort: score ascending (worst first, default) or name. Values: ["score","name"] |
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |

```sh
curl "https://panel.example.com/api/v2/reports/checkup" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/checkup/history`

Checkup score history · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `site_id` | integer |  | Filter by site. |

```sh
curl "https://panel.example.com/api/v2/reports/checkup/history" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/checkup/ignores`

Checkup ignores · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `site_id` | integer |  | Filter by site. |

```sh
curl "https://panel.example.com/api/v2/reports/checkup/ignores" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/reports/checkup/ignores/batch`

Add or remove checkup ignores in bulk · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `keys` | string[] | Yes | Check keys. |
| `op` | string | Yes | add puts keys on the ignore list (existing ones are untouched); remove takes them off. Values: ["add","remove"] |
| `site_ids` | integer[] | Yes | Sites to process, at most 500. |

```sh
curl -X POST "https://panel.example.com/api/v2/reports/checkup/ignores/batch" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/checkup/issues`

Checkup problems grouped by check · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `q` | string |  | Search by site name or domain (substring match); only matching sites are counted. |

```sh
curl "https://panel.example.com/api/v2/reports/checkup/issues" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/checkup/issues/{key}/sites`

Sites affected by a checkup problem · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `q` | string |  | Search by site name or domain (substring match). |
| `sort` | string |  | Sort order: score is lowest checkup score first (default); attacks is most attacks in the last 7 days first (WAF and default-CC problems carry the count, other checks count as 0; ties fall back to score). Values: ["score","attacks"] |
| `type` | string |  | rec_type only - filter by site type; undecided lists sites whose type could not be determined (the checkup only noted them and did not count them as problems; this is the only place they are listed). 422 when key is not rec_type. Values: ["corporate","shop","content","api","admin","download","undecided"] |
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |

```sh
curl "https://panel.example.com/api/v2/reports/checkup/issues/{key}/sites" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/reports/checkup/recheck`

Run the checkup again · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `site_ids` | integer[] |  | Sites to process, at most 500. |

```sh
curl -X POST "https://panel.example.com/api/v2/reports/checkup/recheck" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/checkup/runs/active`

The checkup batch in progress · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

```sh
curl "https://panel.example.com/api/v2/reports/checkup/runs/active" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/checkup/runs/{id}`

Checkup batch progress · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/reports/checkup/runs/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/checkup/settings`

Get checkup settings · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/reports/checkup/settings" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/reports/checkup/settings`

Save checkup settings · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `frequency` | string | Yes | Frequency: daily at 03:00, weekly on Monday at 03:00, or manual only. Values: ["daily","weekly","manual"] |
| `notify_critical` | boolean | Yes | Raise an alert when a critical problem appears (uses the channels in alert settings). |

```sh
curl -X PUT "https://panel.example.com/api/v2/reports/checkup/settings" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/checkup/sites/{id}`

Site checkup report · Authentication: User (some parameters admin only)

```sh
curl "https://panel.example.com/api/v2/reports/checkup/sites/{id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/reports/checkup/sites/{id}/app`

Override a site's detected application · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string |  | Site name. |
| `reset` | boolean |  | true clears the override and returns to automatic detection (name and version are ignored). |
| `version` | string |  | Application version; may be left empty. |

```sh
curl -X PUT "https://panel.example.com/api/v2/reports/checkup/sites/{id}/app" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### PUT `/api/v2/reports/checkup/sites/{id}/ignores`

Set a site's checkup ignores · Authentication: User (some parameters admin only)

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `keys` | string[] | Yes | Check keys. |

```sh
curl -X PUT "https://panel.example.com/api/v2/reports/checkup/sites/{id}/ignores" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/host-not-found/top`

Unconfigured domain Top · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |
| `node_id` | integer |  | Only entries reported by this node. |
| `domain` | string |  | Filter by host substring (case-insensitive, literal match). |
| `kind` | HostNotFoundKind |  | Only this kind (kinds are judged by the master, not stored; the top 500 domains by count are judged and filtered before paging, total counts the filtered rows). Values: ["site","pointed_here","not_pointed","typo","scanner","unknown"] |
| `order` | string |  | Sort direction by count, default desc. Values: ["desc","asc"] |

```sh
curl "https://panel.example.com/api/v2/reports/host-not-found/top" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/ip-seen`

First and last time a visitor IP was seen · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `site_ids` | string |  | Only these sites: comma-separated positive integers, at most 200 after deduplication (more returns 422); for more than 200 sites send a filter (group_id / upid / tenant_ids) instead. Intersected with group_id, upid and tenant_ids; sites a tenant cannot see have no data. |
| `group_id` | integer |  | Only sites in this site group (expanded to a site set on the server and intersected with the other scope filters; a tenant only expands its own sites). |
| `upid` | integer |  | Only sites under this user package. |
| `tenant_ids` | string |  | Admin only: only these tenants, comma-separated positive integers, at most 200 after deduplication (more returns 422); tenant credentials sending it get 403. |
| `ip` | string | Yes | Client address to look up (IPv4 or IPv6). |

```sh
curl "https://panel.example.com/api/v2/reports/ip-seen" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/nodes/realtime`

Node real-time curves · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |
| `node_id` | integer | Yes | Node. A sub-IP id returns the curves of the main node it belongs to (metrics are collected per main node). |
| `metric` | string | Yes | Node metric (same as node_metrics.metric). tls_hs_reject is the number of TLS handshakes rejected by handshake-flood protection; worker_drain_killed is the number of draining old nginx workers force-terminated (SIGTERM). Values: ["nic_in_bps","nic_out_bps","cpu_pct","mem_pct","load1","disk_space_pct","disk_inode_pct","tcp_conn","nginx_active","nginx_reading","nginx_writing","nginx_waiting","log_dropped","tls_hs_reject","worker_drain_killed"] |
| `interval` | string |  | Bucket width; chosen automatically from the time span by default. Values: ["1m","2m","5m","10m","30m","1h"] |

```sh
curl "https://panel.example.com/api/v2/reports/nodes/realtime" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/nodes/realtime-batch`

Node real-time curves (several metrics in one call) · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |
| `node_id` | integer | Yes | Node. A sub-IP id returns the curves of the main node it belongs to (metrics are collected per main node). |
| `metrics` | string | Yes | Node metrics, comma-separated. |
| `interval` | string |  | Bucket width; chosen automatically from the time span by default. Values: ["1m","2m","5m","10m","30m","1h"] |

```sh
curl "https://panel.example.com/api/v2/reports/nodes/realtime-batch" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/nodes/summary`

Node ranking table · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `window` | integer |  | Averaging window in minutes; 5 by default. Values: [1,5,30,60] |

```sh
curl "https://panel.example.com/api/v2/reports/nodes/summary" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/nodes/top`

Node ranking · Authentication: Admin only

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |
| `metric` | string | Yes | Node metric (same as node_metrics.metric). tls_hs_reject is the number of TLS handshakes rejected by handshake-flood protection; worker_drain_killed is the number of draining old nginx workers force-terminated (SIGTERM). Values: ["nic_in_bps","nic_out_bps","cpu_pct","mem_pct","load1","disk_space_pct","disk_inode_pct","tcp_conn","nginx_active","nginx_reading","nginx_writing","nginx_waiting","log_dropped","tls_hs_reject","worker_drain_killed"] |

```sh
curl "https://panel.example.com/api/v2/reports/nodes/top" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/nodes/visitor-rtt`

Visitor round-trip time by node · Authentication: Admin only

```sh
curl "https://panel.example.com/api/v2/reports/nodes/visitor-rtt" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/security/overview`

Security overview · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |
| `site_id` | integer |  | Filter by site. |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

```sh
curl "https://panel.example.com/api/v2/reports/security/overview" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/security/screen`

Security big-screen aggregate · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |
| `site_id` | integer |  | Filter by site. |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

```sh
curl "https://panel.example.com/api/v2/reports/security/screen" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/sites/geo`

Visitor location statistics · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |
| `site_ids` | string |  | Only these sites: comma-separated positive integers, at most 200 after deduplication (more returns 422); for more than 200 sites send a filter (group_id / upid / tenant_ids) instead. Intersected with group_id, upid and tenant_ids; sites a tenant cannot see have no data. |
| `group_id` | integer |  | Only sites in this site group (expanded to a site set on the server and intersected with the other scope filters; a tenant only expands its own sites). |
| `upid` | integer |  | Only sites under this user package. |
| `tenant_ids` | string |  | Admin only: only these tenants, comma-separated positive integers, at most 200 after deduplication (more returns 422); tenant credentials sending it get 403. |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `ip` | string |  | Exact match on client address. |

```sh
curl "https://panel.example.com/api/v2/reports/sites/geo" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/sites/overview`

Site traffic overview · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `site_ids` | string |  | Only these sites: comma-separated positive integers, at most 200 after deduplication (more returns 422); for more than 200 sites send a filter (group_id / upid / tenant_ids) instead. Intersected with group_id, upid and tenant_ids; sites a tenant cannot see have no data. |
| `group_id` | integer |  | Only sites in this site group (expanded to a site set on the server and intersected with the other scope filters; a tenant only expands its own sites). |
| `upid` | integer |  | Only sites under this user package. |
| `tenant_ids` | string |  | Admin only: only these tenants, comma-separated positive integers, at most 200 after deduplication (more returns 422); tenant credentials sending it get 403. |
| `interval` | string |  | Bucket width; chosen automatically from the time span by default. Values: ["1m","2m","5m","10m","30m","1h"] |
| `include_noconfig` | boolean |  | Admin only: also include unconfigured traffic (site_id = 0). |
| `compare` | string |  | prev = also return the previous period of the same length (the compare field); omitted by default. Values: ["prev"] |

```sh
curl "https://panel.example.com/api/v2/reports/sites/overview" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/sites/protocol-config`

Site protocol configuration distribution · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

```sh
curl "https://panel.example.com/api/v2/reports/sites/protocol-config" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/sites/realtime`

Site real-time curves · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |
| `site_ids` | string |  | Only these sites: comma-separated positive integers, at most 200 after deduplication (more returns 422); for more than 200 sites send a filter (group_id / upid / tenant_ids) instead. Intersected with group_id, upid and tenant_ids; sites a tenant cannot see have no data. |
| `group_id` | integer |  | Only sites in this site group (expanded to a site set on the server and intersected with the other scope filters; a tenant only expands its own sites). |
| `upid` | integer |  | Only sites under this user package. |
| `tenant_ids` | string |  | Admin only: only these tenants, comma-separated positive integers, at most 200 after deduplication (more returns 422); tenant credentials sending it get 403. |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `metric` | string | Yes | Metric. Values: ["bandwidth","bandwidth_in","traffic","traffic_in","backend_bandwidth","backend_traffic","req","qps","req_hit_ratio","byte_hit_ratio","status_4xx","status_5xx","backend_resp_time","backend_timeout","blocked","challenged"] |
| `interval` | string |  | Bucket width; chosen automatically from the time span by default. Values: ["1m","2m","5m","10m","30m","1h"] |
| `include_noconfig` | boolean |  | Admin only: when no site scope (site_ids / group_id) is given, also include unconfigured traffic (site_id = 0). |

```sh
curl "https://panel.example.com/api/v2/reports/sites/realtime" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/sites/realtime-batch`

Site real-time curves (several metrics in one call) · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |
| `site_ids` | string |  | Only these sites: comma-separated positive integers, at most 200 after deduplication (more returns 422); for more than 200 sites send a filter (group_id / upid / tenant_ids) instead. Intersected with group_id, upid and tenant_ids; sites a tenant cannot see have no data. |
| `group_id` | integer |  | Only sites in this site group (expanded to a site set on the server and intersected with the other scope filters; a tenant only expands its own sites). |
| `upid` | integer |  | Only sites under this user package. |
| `tenant_ids` | string |  | Admin only: only these tenants, comma-separated positive integers, at most 200 after deduplication (more returns 422); tenant credentials sending it get 403. |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `metrics` | string | Yes | Metrics, comma-separated. |
| `interval` | string |  | Bucket width; chosen automatically from the time span by default. Values: ["1m","2m","5m","10m","30m","1h"] |
| `include_noconfig` | boolean |  | Admin only: when no site scope (site_ids / group_id) is given, also include unconfigured traffic (site_id = 0). |

```sh
curl "https://panel.example.com/api/v2/reports/sites/realtime-batch" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/sites/top`

Site ranking · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |
| `site_ids` | string |  | Only these sites: comma-separated positive integers, at most 200 after deduplication (more returns 422); for more than 200 sites send a filter (group_id / upid / tenant_ids) instead. Intersected with group_id, upid and tenant_ids; sites a tenant cannot see have no data. |
| `group_id` | integer |  | Only sites in this site group (expanded to a site set on the server and intersected with the other scope filters; a tenant only expands its own sites). |
| `upid` | integer |  | Only sites under this user package. |
| `tenant_ids` | string |  | Admin only: only these tenants, comma-separated positive integers, at most 200 after deduplication (more returns 422); tenant credentials sending it get 403. |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `dim` | string |  | Ranking dimension; use either dim or dims. Values: ["ip","prefix","url","referer","site","status","http_ver","scheme","cache_status","ctype","country","prov","isp","port","block_reason"] |
| `dims` | string |  | Fetch the top limit rows of several dimensions at once, comma-separated dimension names (values as for dim, at most 12); use either dim or dims. |
| `limit` | integer |  | Rows per dimension; defaults to and capped by the reports.top_size system setting. |
| `metric` | string |  | Which item to sort by and use as value, default count. Values: ["count","bytes"] |
| `action` | string |  | Only count requests with this action (pass = allowed, challenge = challenged, block = blocked). Values: ["pass","challenge","block"] |
| `server_port` | integer |  | Only count requests to this listening port; not supported for the prefix dimension (422). |

```sh
curl "https://panel.example.com/api/v2/reports/sites/top" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/streams/realtime`

TCP/UDP forwarding real-time curves · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `port` | integer |  | Filter by stream listen port. |
| `metric` | string | Yes | Metric. Values: ["bandwidth","bandwidth_in","traffic","traffic_in","req","conn_peak"] |
| `interval` | string |  | Bucket width; chosen automatically from the time span by default. Values: ["1m","2m","5m","10m","30m","1h"] |

```sh
curl "https://panel.example.com/api/v2/reports/streams/realtime" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/streams/top`

TCP/UDP forwarding ranking · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `dim` | string |  | Ranking dimension, default port. Values: ["port","ip"] |
| `metric` | string |  | Which item to sort by and use as value, default count. Values: ["count","bytes"] |

```sh
curl "https://panel.example.com/api/v2/reports/streams/top" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/tmp-whitelist`

List node temporary whitelist records · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `site_id` | integer |  | Filter by site. |
| `ip` | string |  | Exact match on client address (IPv6 as /64 prefix address). |
| `active` | boolean |  | true = only entries not yet expired. |

```sh
curl "https://panel.example.com/api/v2/reports/tmp-whitelist" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/usage`

Usage curves · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `start` | string | Yes | Start time (inclusive), RFC 3339 |
| `end` | string | Yes | End time (exclusive), RFC 3339 |
| `site_ids` | string |  | Only these sites: comma-separated positive integers, at most 200 after deduplication (more returns 422); for more than 200 sites send a filter (group_id / upid / tenant_ids) instead. Intersected with group_id, upid and tenant_ids; sites a tenant cannot see have no data. |
| `group_id` | integer |  | Only sites in this site group (expanded to a site set on the server and intersected with the other scope filters; a tenant only expands its own sites). |
| `upid` | integer |  | Only sites under this user package. |
| `tenant_ids` | string |  | Admin only: only these tenants, comma-separated positive integers, at most 200 after deduplication (more returns 422); tenant credentials sending it get 403. |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `kind` | string |  | site (default) = sites; stream = TCP/UDP forwarding. Values: ["site","stream"] |
| `metric` | string | Yes | Metric. Values: ["bandwidth","traffic","req","qps","req_hit_ratio","status_5xx_ratio","status_4xx_ratio","backend_bandwidth","backend_traffic","backend_resp_time","blocked"] |
| `interval` | string |  | auto (default), 5m or 1h. Values: ["auto","5m","1h"] |
| `port` | integer |  | Filter by stream listen port. |
| `include_noconfig` | boolean |  | Admin only: when no site scope (site_ids / group_id) is given, also include unconfigured traffic (site_id = 0). |

```sh
curl "https://panel.example.com/api/v2/reports/usage" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/waf-events`

List WAF events · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer |  | Page number, starting from 1 Default: 1 |
| `page_size` | integer |  | Items per page Default: 20 |
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `start` | string |  | Start time (inclusive), RFC 3339; no lower bound when omitted (events are kept for 7 days). |
| `end` | string |  | End time (exclusive), RFC 3339; no upper bound when omitted. |
| `site_id` | integer |  | Filter by site. |
| `ip` | string |  | Filter by client address: a single IPv4/IPv6 address matches exactly; a CIDR range (IPv4-mapped notation accepted) matches by address interval. Prefixes shorter than /8 (IPv4) or /32 (IPv6) are rejected with 422. |
| `url` | string |  | URL contains (case-sensitive substring of the masked path and query string). |
| `ja4` | string |  | Exact match on the client TLS fingerprint JA4 (events without a fingerprint never match). |
| `rule_id` | integer |  | Filter by the matched rule id. |
| `action` | string |  | Filter by action: block, log, challenge, mask or replace. Values: ["block","log","challenge","mask","replace"] |
| `type` | string[] |  | Filter by event type; repeat to select several, omit for all. |
| `country` | string |  | Filter by country name as recorded by the node geo database (exact match). |
| `province` | string |  | Filter by province (exact match). |
| `node_id` | integer |  | Filter by reporting node; tenants cannot see node ids, so the parameter is ignored for them (no error). |
| `observe` | boolean |  | true = only events recorded in observe mode; false or omitted = no such filter. |
| `blocked` | boolean |  | true = only blocks in block mode (action = block, not observe); false = only the rest. |
| `disposition` | string |  | other = only events outside blocked / challenge / observe-only (not observe mode and action is neither block nor challenge, i.e. log, mask, replace); omitted = no such filter. Values: ["other"] |

```sh
curl "https://panel.example.com/api/v2/reports/waf-events" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/waf-events/export`

Export WAF events as CSV · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `start` | string |  | Start time (inclusive), RFC 3339; no lower bound when omitted (events are kept for 7 days). |
| `end` | string |  | End time (exclusive), RFC 3339; no upper bound when omitted. |
| `site_id` | integer |  | Filter by site. |
| `ip` | string |  | Filter by client address: a single IPv4/IPv6 address matches exactly; a CIDR range (IPv4-mapped notation accepted) matches by address interval. Prefixes shorter than /8 (IPv4) or /32 (IPv6) are rejected with 422. |
| `url` | string |  | URL contains (case-sensitive substring of the masked path and query string). |
| `ja4` | string |  | Exact match on the client TLS fingerprint JA4 (events without a fingerprint never match). |
| `rule_id` | integer |  | Filter by the matched rule id. |
| `action` | string |  | Filter by action: block, log, challenge, mask or replace. Values: ["block","log","challenge","mask","replace"] |
| `type` | string[] |  | Filter by event type; repeat to select several, omit for all. |
| `country` | string |  | Filter by country name as recorded by the node geo database (exact match). |
| `province` | string |  | Filter by province (exact match). |
| `node_id` | integer |  | Filter by reporting node; tenants cannot see node ids, so the parameter is ignored for them (no error). |
| `observe` | boolean |  | true = only events recorded in observe mode; false or omitted = no such filter. |
| `blocked` | boolean |  | true = only blocks in block mode (action = block, not observe); false = only the rest. |
| `disposition` | string |  | other = only events outside blocked / challenge / observe-only (not observe mode and action is neither block nor challenge, i.e. log, mask, replace); omitted = no such filter. Values: ["other"] |

```sh
curl "https://panel.example.com/api/v2/reports/waf-events/export" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/waf-events/summary`

Summarize WAF events · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |
| `start` | string | Yes | Start of the time window (inclusive), RFC 3339. |
| `end` | string | Yes | End of the time window (exclusive), RFC 3339; must be after start, at most 31 days apart. |
| `site_id` | integer |  | Filter by site. |
| `ip` | string |  | Filter by client address: a single IPv4/IPv6 address matches exactly; a CIDR range (IPv4-mapped notation accepted) matches by address interval. Prefixes shorter than /8 (IPv4) or /32 (IPv6) are rejected with 422. |
| `url` | string |  | URL contains (case-sensitive substring of the masked path and query string). |
| `ja4` | string |  | Exact match on the client TLS fingerprint JA4 (events without a fingerprint never match). |
| `rule_id` | integer |  | Filter by the matched rule id. |
| `action` | string |  | Filter by action: block, log, challenge, mask or replace. Values: ["block","log","challenge","mask","replace"] |
| `type` | string[] |  | Filter by event type; repeat to select several, omit for all. |
| `country` | string |  | Filter by country name as recorded by the node geo database (exact match). |
| `province` | string |  | Filter by province (exact match). |
| `node_id` | integer |  | Filter by reporting node; tenants cannot see node ids, so the parameter is ignored for them (no error). |
| `observe` | boolean |  | true = only events recorded in observe mode; false or omitted = no such filter. |
| `blocked` | boolean |  | true = only blocks in block mode (action = block, not observe); false = only the rest. |
| `disposition` | string |  | other = only events outside blocked / challenge / observe-only (not observe mode and action is neither block nor challenge, i.e. log, mask, replace); omitted = no such filter. Values: ["other"] |

```sh
curl "https://panel.example.com/api/v2/reports/waf-events/summary" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### GET `/api/v2/reports/waf-events/{event_id}`

Get a WAF event · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

```sh
curl "https://panel.example.com/api/v2/reports/waf-events/{event_id}" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

### POST `/api/v2/reports/waf-events/{event_id}/allow`

Allow-list a WAF event · Authentication: User (some parameters admin only)

Query parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_id` | integer |  | Admin only: filter by tenant. Ignored for tenant credentials, which only see their own tenant's data |

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `param` | string |  | Name of the matched parameter, header or cookie; empty for unnamed locations. |
| `path` | string |  | Limit to this path prefix; must equal the event URL path or be a prefix ending in / (e.g. /api/); a bare / must be combined with a rule or parameter. Omit for any path. |
| `rule` | boolean |  | Skip only the matched rule (rule_ids); not allowed for non-rule events (rule id 0) or semantic events. Default: false |

```sh
curl -X POST "https://panel.example.com/api/v2/reports/waf-events/{event_id}/allow" \
  -H "X-API-Key: $GOCDN_API_KEY"
```

Reports object:

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | string |  | Protection action: pass, challenge or block. |
| `advice` | integer |  | Number of suggestions. |
| `app` | string |  | Detected application; empty when none was recognised. |
| `app_manual` | boolean |  | The application was overridden by hand. |
| `app_version` | string |  | Application version; empty when it could not be read. |
| `approx` | boolean |  | The ranking comes from each node's per-minute top K (ip, prefix, url, referer); nodes report only their own top K, so totals are approximate. False when read from access logs (action or server_port given). |
| `at` | string |  | Time it happened (the last hit inside the window). |
| `attack_types` | WafEventCount[] |  | Attack type distribution (sqli, xss, scanner, rce, traversal, other and semantic, bot, upload, tamper, leak), by rows descending; adds up to total. |
| `attacked_sites` | integer |  | Distinct attacked sites. |
| `ban_hits` | integer |  | Revisits stopped by banned IPs - the sum of the auto_ban and status_ban items of blocked_by_feature. Always 0 until nodes tag ban blocks. |
| `block` | integer |  | Blocks in block mode (action = block, not observe). |
| `block_reason` | string |  | Block reason: region = blocked by region limit, region_log = region limit log-only hit (not blocked), acl = blocked by ACL, other = blocked by other protection; omitted when neither blocked nor a region-limit hit. Values: ["region","region_log","acl","other"] |
| `blocked` | integer |  | Blocked requests (action = block). |
| `blocked_acl` | integer |  | Requests blocked by ACLs (including ACLs without location conditions). |
| `blocked_by_feature` | SecurityFeatureCount[] |  | Blocked requests per protection feature (blocked requests scaled by sampling weight, challenges excluded). Always 12 items (genuine blocks only; leak and tamper protection replace the response body and are not listed here, see handled_by_feature), descending by count then in a fixed order; the counts add up to summary.blocked for the same window and site scope. A feature is derived from the rule_ref the node writes into the access log; blocks from nodes that do not tag them are counted under waf_rule. |
| `blocked_region` | integer |  | Requests blocked by the region limit. |
| `body_trunc` | boolean |  | The request body exceeded the inspection limit and only the head was checked. |
| `bot_score` | integer |  | Bot behaviour score 0–100; only on Bot events (type = bot), absent for other types (score is the WAF anomaly score and means something else). |
| `bytes_sent` | integer |  | Raw downstream bytes. |
| `cache_status` | string |  | Cache status, such as HIT, MISS or BYPASS. |
| `category` | string |  | Rule category (sqli, xss, scanner, rce, php, nodejs, java, lfi, rfi, protocol, ...); empty when unknown. |
| `challenge` | integer |  | Challenges outside observe mode (action = challenge). |
| `checked_at` | string |  | Time of the checkup. |
| `checkup` | object |  | Site checkup feed; the protection score (site checkup scores weighted by traffic) and the to-do suggestions. Follows site_id but not the time window. |
| `client_ip` | string |  | Client IP; IPv4 as is. |
| `combo` | object |  | Distribution of HTTP/2 and HTTP/3 combinations (number of sites). |
| `compare` | SitesOverviewCompare |  | The previous period (compare=prev) with its window [start, end), per-bucket points and totals; points align one-to-one with the current period. h3_ratio, backend_resp_time_p95 and noconfig_req are always null. |
| `content_encoding` | string |  | Response Content-Encoding. |
| `count` | integer |  | 530 count within the time window (sum across nodes). |
| `countries` | SecurityCountryCount[] |  | Attack source countries (blocked and challenged requests scaled by sampling weight), descending; an empty country means unknown. |
| `country` | string |  | Client country (ISO 3166-1 alpha-2); omitted when unknown. |
| `covered` | integer |  | Known vulnerabilities of the recognised program already covered by built-in rules, from the protection list; 0 when no program is recognised or the list has no such product. |
| `created_at` | string |  | Created at |
| `critical` | integer |  | Number of critical problems (ignored and locked ones excluded). |
| `ctype` | string |  | Response Content-Type (parameters removed, lowercase). |
| `current_site_id` | integer |  | Id of the site whose result is being saved; only present in stage 5 (stages 1-4 run on the whole batch and have no single current site), omitted otherwise. |
| `cves` | SecurityCve[] |  | CVEs hit in the last 7 days (matched against the rule pack meta section), descending by hits, at most 50; empty when no rule pack is connected. |
| `dims` | object |  | Result of a dims request, mapping each dimension name to its ranking (which has no dims of its own); absent for a single-dimension request. |
| `domain` | string |  | First domain. |
| `done_rows` | integer |  | Rows written to the file so far. |
| `done_site_ids` | integer[] |  | Ids of the sites whose checkup results this batch has saved (ascending); sites not listed are not finished yet (or failed). Cleared when the batch is retried. |
| `done_sites` | integer |  | Number of sites with saved results; below total_sites after completion means some sites failed. |
| `error` | string |  | Reason of the latest failure (kept while retrying, cleared once the export succeeds; the final reason when failed). |
| `exception` | WafEventAllowException |  | The site exception written by this allow-list action (or already present on the site). |
| `expires_at` | string |  | When the file is removed (completion time plus the retention hours). |
| `extra` | integer |  | Known vulnerabilities on the same basis that can additionally be covered once WAF rule updates are enabled. |
| `facets` | object |  | Facet counts for the filter buttons. |
| `file_bytes` | integer |  | Compressed file size in bytes (set once done). |
| `file_count` | integer |  | Number of uploaded files scanned in this request. |
| `file_ctype` | string |  | Content-Type of the uploaded file as given by the client. |
| `file_field` | string |  | Form field name of the uploaded file. |
| `file_magic` | string |  | Type recognised from the file header; empty when unknown. |
| `file_name` | string |  | Uploaded file name as given by the client; only on upload events (type = upload). |
| `file_sha256` | string |  | SHA-256 of the uploaded file; empty when not computed. |
| `file_size` | integer |  | Uploaded file size in bytes; only on upload events (type = upload). |
| `filter` | AccessLogExportFilter |  | Filters given with the request (same as the list query parameters). |
| `findings` | CheckupFinding[] |  | Checks with problems (passed checks are not listed), in check order. |
| `finished_at` | string |  | Completion time. |
| `first_seen` | string |  | When this problem began in its current run; omitted for the first checkup and for results saved before this was recorded. |
| `first_seen_at` | string |  | Time of the earliest access log in the retention period; absent when there is none. |
| `flows` | SecurityFlow[] |  | Attack flows top 50 (source country to the country and city of the node that took the traffic, blocked and challenged requests scaled by sampling weight), descending; the rest and nodes at unknown locations are merged into one trailing row with an empty node_country (not drawn); the total equals the total of countries. Part of WAF Enhanced, an empty array when it is off; only data written after the flow table was created is counted, so older windows are empty. |
| `foreign` | integer |  | Requests from outside mainland China (known country other than CN); excluded from the ISP breakdown. |
| `frequency` | string |  | Frequency: daily at 03:00, weekly on Monday at 03:00, or manual only. Values: ["daily","weekly","manual"] |
| `geo_summary` | TopGeoSummary |  | Summary for the region dimensions (country, prov, isp) over the whole time window and the same filters, not limited by the number of ranked rows. |
| `handled_by_feature` | SecurityHandledCount[] |  | Requests handled by replacing content - leak and tamper protection rewrite the response body, keep the status code and are recorded as pass, so they are not blocks and are in neither blocked_by_feature nor summary.blocked, and should not enter block percentages. Only features with a count above 0 are listed, descending by count then leak before tamper; an empty array when there is no data (including before nodes tag body replacement), which the UI uses to hide it. Scaled by sampling weight and follows the time window and site scope. |
| `headers` | string[] |  | First 20 request headers as "lowercase-name: value", values cut to 256 bytes; Cookie and Authorization values are masked. |
| `history` | CheckupHistoryPoint[] |  | Scores of this site over the last 8 weeks. |
| `hit_layer` | string |  | Upload scan layer that matched; empty when nothing matched. |
| `host` | string |  | Requested Host. |
| `http2` | ProtocolSwitch |  | Number of sites by switch state of a protocol (on, off, unset). |
| `http3` | ProtocolSwitch |  | Number of sites by switch state of a protocol (on, off, unset). |
| `http_ver` | string |  | HTTP protocol version (HTTP/x.y normalized to 1.0, 1.1, 2, 3). |
| `https_off` | integer |  | Number of sites with HTTPS disabled. |
| `https_on` | integer |  | Number of sites with HTTPS enabled. |
| `id` | integer |  | Record ID |
| `intel_hit` | boolean |  | Whether the IP is currently in the platform high-risk IP library (looked up against the current library); null = the library is not being served, not judged. |
| `interval` | integer |  | Bucket width in seconds. |
| `invalid` | integer |  | Number of sites whose configuration cannot be parsed or whose switch value is not a boolean. |
| `ip` | string |  | The address looked up (canonical form). |
| `isp` | string |  | Client ISP code; omitted when unknown. |
| `isps` | GeoCount[] |  | Requests per ISP in mainland China, descending (empty key = ISP unknown). |
| `items` | CheckupHistoryPoint[] |  | Items on this page |
| `ja4` | string |  | Client TLS fingerprint JA4 (computed by the node during the TLS handshake, same for all requests on a connection); omitted for plain HTTP or when unavailable. |
| `keys` | string[] | Yes | Check keys. |
| `kind` | string |  | Site kind: domain (domain site) or ip (IP site); they have different edit pages. Values: ["domain","ip"] |
| `last_seen` | string |  | Most recent minute it appeared. |
| `last_seen_at` | string |  | Time of the latest access log in the retention period; absent when there is none. |
| `level` | string |  | Protection level of the site at the time: basic, standard or strict. |
| `limit` | integer |  | Requests allowed per day (reports.access_log_export_daily; 0 = downloads not allowed). |
| `max` | number |  | bandwidth, backend_bandwidth: peak 5-minute bandwidth in the time window. |
| `medium` | integer |  | Number of medium problems. |
| `message` | string |  | Legacy note (action and score) kept for older clients; build texts from type, action and category instead. |
| `method` | string |  | HTTP request method. |
| `metrics` | NodeRealtimeMetric[] |  | Curves of each metric, in request order. |
| `name` | string |  | Site name. |
| `next_retry_at` | string |  | When the next retry starts (only for pending requests waiting to retry after a failure). |
| `node_city` | string |  | City of the node that reported it; empty when only the country is known or the location is unknown. |
| `node_id` | integer |  | ID of the node that served the request. |
| `node_ids` | integer[] |  | Nodes that reported it (up to 50, ascending). |
| `node_ips` | string[] |  | Public addresses of the nodes in the site's own line groups (deduplicated and sorted, at most 500; private or management egress addresses are never listed); allow-listing them on the origin firewall restricts access to the CDN. Returned only when the origin-direct problem exists, otherwise an empty array. |
| `node_lat` | number |  | Latitude of the node's landing point, as for node_lon. |
| `node_lon` | number |  | Longitude of the node's landing point (the city, or the country centroid when there is no city); absent when the location is unknown. |
| `node_names` | string[] |  | Node names in the same order as node_ids; "#<id>" if a node has no name or was deleted. |
| `node_spots` | SecurityNodeSpot[] |  | Node landing points (country and city to the number of nodes in use), administrators only and always an empty array for tenants; same basis as the flows landing points (enabled primary nodes, nodes at unknown locations are not counted); independent of whether WAF Enhanced is on, the time window and site_id; descending by node count, then by country and city. |
| `node_unknown` | integer |  | Number of nodes in use whose location is unknown (enabled primary nodes whose country cannot be determined, not counted in node_spots); administrators only, the field is absent from tenant responses; independent of whether WAF Enhanced is on, the time window and site_id. |
| `nodes` | integer |  | Number of nodes that reported it. |
| `notify_critical` | boolean |  | Raise an alert when a critical problem appears (uses the channels in alert settings). |
| `observe` | boolean |  | The site was in observe mode at the time (record only). |
| `observe_rule` | boolean |  | true = the hit was a new rule still in its observation period, recorded only and not blocked (observe is then also true); emergency rules have no observation period, so always false. |
| `op` | string | Yes | add puts keys on the ignore list (existing ones are untouched); remove takes them off. Values: ["add","remove"] |
| `open_url` | string |  | Clickable address of this request (the log's own scheme + host + non-default port + path and query); only http/https with a valid domain or IP and shorter than 2048 characters, otherwise omitted. |
| `other` | integer |  | The rest: log, mask or replace outside observe mode. |
| `p95` | number |  | bandwidth, backend_bandwidth: 95th percentile (5-minute samples for interval=5m, hourly peaks otherwise; only samples with data; the (n×5/100+1)-th largest, equal to the peak when n=1). |
| `param` | string |  | Name of the matched parameter, header or cookie; empty for unnamed locations. |
| `params` | object |  | Numbers and names for display, same as params of this check in the checkup result (including open_url). |
| `path` | string |  | Limit to this path prefix; must equal the event URL path or be a prefix ending in / (e.g. /api/); a bare / must be combined with a rule or parameter. Omit for any path. |
| `points` | SecurityPoint[] |  | Attack trend; the bucket width follows the time span (about 288 points at most) and empty buckets are zero. |
| `prefix_len` | integer |  | 32 (IPv4) or 64 (IPv6). |
| `prev_score` | integer |  | Score of the previous checkup; null for the first checkup. |
| `previous` | object |  | Comparison with the previous window of the same length. |
| `probes` | CheckupProbeExtra[] |  | Rows of the last 7 days of probing that do not come from WAF events (CC cut, origin 404); blocked / logged rows are aggregated by the panel from WAF events. |
| `progress` | integer |  | Progress percentage (rows written / total rows). |
| `protocol` | string |  | HTTP version, e.g. HTTP/2.0. |
| `prov` | string |  | Client province (first two digits of GB/T 2260, mainland China only); omitted when unknown. |
| `province` | string |  | Province; empty when unknown. |
| `provinces` | SecurityProvinceCount[] |  | Attack source provinces in mainland China (blocked and challenged requests scaled by sampling weight), descending; requests with an unknown province are left out. |
| `provs` | GeoCount[] |  | Requests per mainland China province, descending (empty key = province unknown). |
| `publish_seq` | integer |  | Publish sequence number triggered (when result = added). |
| `reason_args` | object |  | Parameters of the basis text (id site/cert id, at deletion time, domain the similar site domain, n differing characters, names node names, cname, ip, nodes, count). |
| `reason_code` | string |  | Basis of the kind (message code; the panel renders the text per language): site_unsynced the site still Values: ["site_unsynced","site_disabled","site_deleted","cert_cover","bare_ip","bad_domain","typo","not_resolved","dns_error","nxdomain","nxdomain_random","pointed_a","pointed_cname","elsewhere_ip","elsewhere_cname","no_a","unavailable"] |
| `recent` | WafEventRecent |  | Events from the same IP in the past hour (total, rule breakdown, latest items). |
| `referer` | string |  | Request Referer header. |
| `referer_open_url` | string |  | Clickable address of the Referer; omitted when it is not an http/https page (such as javascript: or android-app://), is malformed, or is 2048 characters or longer. |
| `region_logged` | integer |  | Requests matching a log-only region limit (not blocked). |
| `remaining` | integer |  | Requests still available today. |
| `request_id` | string |  | Request ID. |
| `request_length` | integer |  | Request length in bytes. |
| `request_time_ms` | integer |  | Request processing time in milliseconds. |
| `reset` | boolean |  | true clears the override and returns to automatic detection (name and version are ignored). |
| `result` | string |  | added; exists = the site already has the same exception, nothing added; full = the exception list is full, nothing added. Values: ["added","exists","full"] |
| `retries` | integer |  | Number of failed attempts; when pending or running and above 0 the request is on retry N. |
| `rule` | boolean |  | Skip only the matched rule (rule_ids); not allowed for non-rule events (rule id 0) or semantic events. Default: false |
| `rule_id` | integer |  | ID of the rule that matched. |
| `rule_name` | string |  | Short rule name. |
| `rules_ver` | string |  | Rule pack version; empty means the built-in rules. |
| `run` | object |  | The batch in progress; omitted when there is none. |
| `sample_factor` | integer |  | Sampling factor N (the largest sample_weight in the access logs read); 1 when not sampled. |
| `sample_weight` | integer |  | Sampling weight (1 means not sampled). |
| `sampled` | boolean |  | The access logs read contain sampled rows (sample_weight > 1); counts are scaled by weight and incomplete. |
| `saved_bytes` | integer |  | Origin traffic saved (bytes) - bytes sent to clients by requests served from cache and not blocked in the window, scaled by sampling weight. |
| `scan_status` | string |  | Upload scan result (type = upload); empty for other types. |
| `scan_steps` | string[] |  | Upload scan steps in order, one "layer:result" per layer. |
| `scheme` | string |  | Request scheme (http or https). |
| `score` | integer |  | Checkup score (100 at most). |
| `series` | LabeledLine[] |  | Time series grouped by sub-object. |
| `server_port` | integer |  | Port the request arrived on. |
| `severity` | string |  | Severity of this problem on this site. Values: ["critical","medium","advice"] |
| `site_deleted` | boolean |  | true when the site this log belongs to has been deleted; omitted when the site still exists (or there is none). |
| `site_id` | integer |  | Site ID |
| `site_ids` | integer[] | Yes | Sites to process, at most 500. |
| `site_name` | string |  | Name of the site it belongs to (primary domain, or the site name when it has none); for a deleted site the domain recorded when it was deleted, else the most common host in the last 7 days of access logs, omitted when none is found. |
| `snippet` | string |  | Matched snippet (masked on the node; the value of the matched parameter or header). The list cuts it to the first 200 characters and the detail returns it whole. It carries no match offset, so it cannot be used to locate a highlight. For Bot events it is the triggering signal names. |
| `source_ips` | integer |  | Distinct source addresses. |
| `stage` | integer |  | Current stage: 0 queued, 1 reading configuration, 2 counting logs, 3 direct probe, 4 per-check evaluation, 5 saving and alerting. With more than 200 sites the stage restarts for every batch of 200; evaluation is instantaneous on the server. |
| `started_at` | string |  | Time the batch was created. |
| `state` | string |  | State: pending, running, done, or failed after retries are exhausted. Values: ["pending","running","done","failed"] |
| `status` | string |  | Status: pending, running, done (downloadable), failed, or expired (file removed after the retention period). Values: ["pending","running","done","failed","expired"] |
| `step` | integer |  | Bucket width of series, in seconds. |
| `summary` | CheckupRunSummary |  | Per-stage volume summary; a stage's key is absent until it is reached. Multi-batch tasks add up across batches. |
| `target` | string |  | Where it matched: uri, query_string, args, args_names, cookies, headers, body, ... |
| `tenant_id` | integer |  | Owner tenant (user) ID of the site. |
| `top_rules` | SecurityRuleCount[] |  | Top 10 matched rules (by hits, descending). |
| `top_sites` | SecuritySiteCount[] |  | Top 10 attacked sites (by blocked and challenged requests, descending). |
| `total` | integer |  | Requests in the time window (scaled by sampling weight). |
| `total_rows` | integer |  | Total matching rows (counted when the export starts). |
| `total_sites` | integer |  | Number of sites in this batch; may still be 0 while queued. |
| `totals` | SitesOverviewTotals |  | Totals over the time window. |
| `traffic_month` | integer |  | Month-to-date outbound bytes, from the 1st of the month containing end (Asia/Shanghai) up to end; sealed hours come from the 5-minute summary, later ones from live data. |
| `traffic_today` | integer |  | Today's outbound bytes, from midnight (Asia/Shanghai) of the day containing end up to end; same scope as totals.traffic. |
| `ts` | string |  | Request time. |
| `type` | string |  | Event type: rule, semantic, bot, upload scan, tamper protection or leak protection. Values: ["waf","semantic","bot","upload","tamper","leak"] |
| `types` | SecurityTypeCount[] |  | Attack type distribution (WAF event hits by type), descending by count. |
| `unavailable` | string[] |  | Supplementary items that could not be loaded this time (only present when the rule store or ClickHouse failed briefly; everything else is returned as usual and the missing items come back empty). coverage = covered / extra, probes = probes, node_ips = node_ips. Absent when everything loaded. |
| `unknown` | integer |  | Requests whose country is unknown (no database, private address or not in the database). |
| `unlimited` | boolean |  | true = no daily cap (administrators); limit and remaining are then absent. |
| `upid` | integer |  | Origin (upstream) ID the request went to. |
| `upstream_addr` | string |  | Origin address:port actually used; comma-separated hops when retried on another origin; empty when not fetched from origin. |
| `upstream_bytes` | integer |  | Bytes received from the origin. |
| `upstream_status` | string |  | Status code returned by each origin hop, matching upstream_addr; empty when not fetched from origin. |
| `upstream_time_ms` | integer |  | Origin response time in milliseconds; null when not fetched from origin. |
| `uri` | string |  | Request URI including the query string. |
| `url` | string |  | Request path and query string (masked). |
| `used` | integer |  | Requests created today. |
| `user_agent` | string |  | Request User-Agent header. |
| `version` | string |  | Application version; may be left empty. |
| `window_min` | integer |  | Averaging window used, in minutes. |
| `window_s` | integer |  | Window length in seconds. |
