Copy the prompt and send it to an AI to generate the App API code

App API

CZL Connect provides a set of "App APIs" for your backend. They let an app owner use a dedicated API key to manage the app's users through CZL Connect. Unlike OAuth2 / OIDC user tokens, this API does not depend on any end-user login session. Requests are authenticated with Authorization: Bearer <api-key>.

Use cases

  • Show the app's user list in your admin console, with search and pagination
  • Programmatically change a user's groups group
  • Sign a user out by revoking all of their OAuth2 tokens for the app
  • Pull the app's authorization event stream for incremental data sync
  • Monitor operational metrics such as authorization counts, active users, and MAU

Differences from OAuth2 user tokens

AspectOAuth2 access tokenApp API key
Identity representedA specific userThe whole app (as a client)
OriginThe user completes the authorization code flow in a browserCreated or rotated manually in the app console
Default permissionsData visible to that userManagement operations on all users of the app
Cross-userNo, limited to the user's own userinfoYes, within the granted scopes
Cross-appNoNo (one key is bound to one app)
Intended callerThe user's browser / appYour backend server process

Do not hand an App API key to a browser or client app. It is effectively a "god mode" key: anyone who holds it can manage all users of the app.

Key management

Open the app detail page and go to the "API access" tab to do the following:

ActionDescriptionConfirmation
Create keyGenerate a key after choosing scopesPasskey
Copy keyCopy the full key from the list at any time; each copy is recorded in the audit logPasskey
Edit keyChange the name and scopes; changes take effect on save and the key itself stays the samePasskey
Rotate keyGenerate a new key; the old key enters a 24-hour grace period and is disabled automatically when it endsPasskey
Revoke immediatelySkip the grace period and disable the key right awayPasskey
Delete keyRemove it from the list; this cannot be undonePasskey

Key format: czlc_live_<32-character base62 suffix>. Keys are verified by SHA-256 hash lookup, and the full key is also stored encrypted with AES-GCM so it can be copied. Keys created before the copy feature launched have no encrypted copy and cannot be copied; rotate them once.

  1. Click "Rotate" in the console and get the new plaintext key
  2. Deploy the new key to your production configuration (environment variable / secrets manager)
  3. Wait until production actually starts using the new key (you can see LastUsedAt update in the key list)
  4. If everything works within 24 hours, the old key expires automatically and no manual action is needed
  5. To retire the old key sooner, click "Revoke immediately" on it

Authentication

Request header:

Authorization: Bearer czlc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

If key verification fails, the API returns 401 with this body:

{
  "error": "invalid_api_key",
  "message": "Invalid or expired API key"
}

If the key is valid but lacks the required scope, the API returns 403:

{
  "error": "insufficient_scope",
  "message": "The API key lacks the required scope",
  "required_any_of": ["user:read"],
  "granted_scopes": ["stats:read"]
}

Scopes

ScopeEndpoints covered
user:readGET /users, GET /users/:userId, GET /events
user:writePATCH /users/:userId (change enabled)
group:writePATCH /users/:userId (change group)
stats:readGET /stats
relation:revokePOST /users/:userId/revoke
support:identityPOST /support/visitor-identity
wechat:miniapp_loginPOST /wechat/miniapp-login (also requires the site admin to bind this app)
push:send/push/* link devices, list a user's devices, send pushes (also requires push to be enabled for this app, see Push)
email:codePOST /email-codes, POST /email-codes/verify send verification codes to any address and verify them (see Email)
email:sendPOST /emails send custom emails to this app's users
broadcast:write/broadcasts/* create / query / cancel broadcasts

GET /me and GET /groups require no scope; any valid key can call them.

PATCH /users/:userId accepts both group and enabled in one request. In that case it requires both group:write and user:write, and a missing scope is rejected with 403.

Rate limits

  • Limit unit: a single app, regardless of key (all keys of the same app share one bucket)
  • Default quota: 600 requests per minute
  • Response headers: X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset / Retry-After
  • Exceeding the limit returns 429:
{
  "error": "rate_limited",
  "message": "Too many requests",
  "retry_after": 23
}

API endpoints

Base URL for all endpoints: https://connect.czl.net/api/v1/app

The URL does not contain an appID. The key is bound to exactly one app and the server looks the app up from the key, which also prevents you from accidentally using someone else's app ID.

GET /me

Returns metadata of the app bound to the current key, plus the key's scopes. Commonly used for a key self-check when your backend starts.

Request example:

curl -H "Authorization: Bearer czlc_live_xxx" \
  https://connect.czl.net/api/v1/app/me

Response:

{
  "app": {
    "id": 42,
    "name": "My App",
    "description": "...",
    "homeUrl": "https://app.example.com",
    "logo": "https://...",
    "enabled": true,
    "isPublic": false,
    "createdAt": "2026-05-01T08:00:00+08:00"
  },
  "apiKey": {
    "id": 7,
    "name": "prod backend",
    "keyPrefix": "czlc_live_a3",
    "scopes": ["user:read", "user:write", "group:write"],
    "status": "active",
    "lastUsedAt": "2026-05-18T10:23:45+08:00",
    "createdAt": "2026-04-20T11:30:00+08:00"
  }
}

GET /users

Lists the app's users, with pagination and filters.

Query parameters:

NameTypeDefaultDescription
pageint1Page number, starting at 1
pageSizeint20Items per page, max 100
keywordstring-Fuzzy match on username / email
groupstring-Exact match on group, for example admin / t3
enabledtrue/false-Filter by enabled state; omit for no filtering

Response:

{
  "items": [
    {
      "userId": "1001",
      "username": "alice",
      "email": "[email protected]",
      "nickname": "",
      "avatar": "https://...",
      "emailVerified": true,
      "group": "t2",
      "enabled": true,
      "globalBanned": false,
      "authCount": 42,
      "firstAuthAt": "2026-01-15T09:00:00+08:00",
      "lastAuthAt": "2026-05-17T22:01:00+08:00",
      "lastLoginAt": "2026-05-17T22:01:00+08:00",
      "createdAt": "2026-01-15T09:00:00+08:00"
    }
  ],
  "total": 1234,
  "page": 1,
  "pageSize": 20
}

enabled is the user's enabled state within this app. globalBanned set to true means the account is banned site-wide by CZL Connect. Such a user cannot sign in regardless of enabled, and the ban cannot be lifted through this API.

GET /users/:userId

Gets one user's details in this app. The fields are the same as a single item of /users. userId is the user's numeric ID inside CZL Connect; you can get it from the /users list or from the OAuth2 userinfo response.

If the user does not exist or has not authorized this app, the API returns 404:

{
  "error": "user_not_found",
  "message": "The user does not exist or has not authorized this app"
}

PATCH /users/:userId

Updates the user's group or enabled state in this app.

Request body (both fields are optional, but provide at least one):

{
  "group": "t3",
  "enabled": true
}

Valid group values: admin / viewer / t0 ~ t5. See GET /groups for details.

When enabled is false, all of the user's OAuth2 tokens for this app are revoked at the same time (forced sign-out). The response returns the updated user view.

POST /users/:userId/revoke

Revokes all of the user's OAuth2 tokens for this app, which signs the user out. It does not delete the user's relation record in this app; the next time the user visits this app, they must go through the authorization flow to sign in again.

Response:

{
  "revoked": true,
  "revokedTokens": 3
}

GET /stats

Aggregate statistics for this app. Response:

{
  "totalUsers": 1234,
  "totalAuths": 56789,
  "activeUsers30d": 432,
  "newUsers30d": 87,
  "dailyAuthCounts": [
    { "date": "2026-05-17", "count": 234 },
    { "date": "2026-05-16", "count": 189 }
  ]
}

GET /events

The app's authorization event stream, in reverse chronological order. It suits incremental data sync on your side: poll continuously and record the last id or createdAt you received.

Query parameters:

NameTypeDefaultDescription
limitint50Max items per request, up to 200
beforeRFC3339-Return only events earlier than this time
eventTypestring-Filter by event type, for example authorization_granted

Response:

{
  "items": [
    {
      "id": 99837,
      "eventType": "authorization_granted",
      "protocol": "oauth2",
      "status": "success",
      "userId": "1001",
      "scope": "openid profile email",
      "description": "",
      "createdAt": "2026-05-17T22:01:00+08:00"
    }
  ],
  "limit": 50
}

Common event types: authorization_granted / authorization_revoked / code_issued / token_issued / token_refreshed / token_revoked.

GET /groups

Returns all available group values with their hierarchy expansion. You can use it to render a dropdown in your app.

Response:

{
  "items": [
    {
      "code": "admin",
      "description": "Administrator; can manage users and view statistics within this app",
      "impliedGroups": ["t0", "t1", "t2", "t3", "t4", "t5", "viewer", "admin"]
    },
    {
      "code": "t3",
      "description": "Level 3",
      "impliedGroups": ["t0", "t1", "t2", "t3"]
    }
  ],
  "note": "t0-t5 inherit lower-level groups in ascending order; admin is independent and has all permissions; viewer is independent and read-only"
}

POST /support/visitor-identity

Issues a verified visitor identity token for online support. Your backend calls it while the user is signed in, then writes the returned token into the data-identity attribute of the support widget script. Agents then see the user's profile with a "Verified" mark. For usage, see Online support.

Request body:

{ "externalId": "10086", "name": "Zhang San", "email": "[email protected]", "phone": "" }
  • externalId is required and is the user identifier in your system (1-128 characters); the same externalId shares one conversation across devices
  • name / email / phone are optional; an invalid format returns 400 invalid_request

Response:

{ "token": "svi_...", "expiresAt": "2026-11-04T12:00:00+08:00" }

The token is valid for 24 hours and can only be used with this app's support widget.

POST /wechat/miniapp-login

Signs in or signs up a CZL Connect account with the code returned by wx.login in a WeChat Mini Program. The Mini Program sends the code to your backend, and your backend calls this endpoint with its credential. The Mini Program should not call CZL Connect directly.

Prerequisite: the site admin has bound a Mini Program (AppID + AppSecret) to this app. Each app can have at most one, and a Mini Program can be bound to only one app. Only Mini Programs under CZL Connect's WeChat Open Platform account are accepted, because only their UnionIDs merge with WeChat web sign-in into the same account; that is why binding is done by the admin and not by app owners. Apps without a binding get 503 miniapp_not_configured.

  • Users who have signed in to CZL Connect with WeChat before match the same account by UnionID
  • New users get an account right away with a placeholder email (emailVerified: false). They can use this app normally, but must verify a real email before authorizing other apps
  • The first sign-in creates the user's authorization for this app (counted toward MAU). No OAuth2 token is issued; your backend manages the Mini Program session
  • A code can only be used once, so your backend must not call jscode2session itself. The openid needed for subscription messages is returned in the response

Request body:

{ "code": "0a3Xxx...", "clientIp": "203.0.113.8", "userAgent": "Mozilla/5.0 ... MicroMessenger/8.0", "nickname": "" }
  • code is required
  • clientIp / userAgent are optional. Pass the Mini Program user's real IP and UA for sign-in history and risk checks. Only public addresses are accepted; if the IP is missing or invalid, your server's IP is recorded. This IP is asserted by your backend and is not verified by CZL Connect
  • nickname is optional and only used as the nickname of a new account; defaults to "微信用户"

Response:

{
  "isNewUser": true,
  "openid": "oAbc...",
  "unionid": "oXyz...",
  "user": {
    "userId": "123",
    "username": "wx_k2j4m5q7",
    "email": "[email protected]",
    "emailVerified": false,
    "nickname": "微信用户",
    "group": "",
    "impliedGroups": ["t0"],
    "enabled": true,
    "globalBanned": false
  }
}

The other user fields match GET /users/:userId. Use userId to link your local user to CZL Connect; impliedGroups is the group list after hierarchy expansion.

StatuserrorMeaning
400invalid_requestThe body is not JSON or code is missing
400invalid_codeThe code is invalid, expired or already used; call wx.login again
403user_bannedThe account is banned site-wide
403user_disabledThe user is disabled in this app, the app is disabled, or the app is not open to new users (non-public / restricted user list)
403wechat_risk_blockedWeChat flagged the user as high-risk and blocked sign-in; retrying will not help
403quota_exceededThe app owner's account reached its plan MAU limit (existing users are unaffected)
422unionid_unavailableThe Mini Program is not bound to the WeChat Open Platform, so no UnionID is available
500login_failedInternal error; retry later
502wechat_unavailableWeChat is temporarily unavailable; retry later
503miniapp_not_configuredNo Mini Program is bound to this app, or WeChat sign-in is not configured on this site
503miniapp_misconfiguredWeChat rejected the bound AppID / AppSecret, or the AppSecret cannot be decrypted; the admin must re-enter it

Unified error format

All 4xx / 5xx responses follow this format:

{
  "error": "<machine_readable_code>",
  "message": "<human readable text>"
}

Common error values:

codeHTTPMeaning
missing_api_key401The Authorization header is missing
invalid_api_key401The key is malformed, expired, or does not exist
insufficient_scope403The key is valid but lacks the required scope
rate_limited429Rate limit triggered
user_not_found404The user does not exist or has not authorized this app
invalid_body400The request body is invalid
empty_body400A PATCH request contained no updatable fields
invalid_user_id400The :userId parameter is invalid
query_failed500Internal query error
context_missing500Internal context missing (usually a deployment issue)

Examples

cURL

# List users
curl -H "Authorization: Bearer czlc_live_xxx" \
  "https://connect.czl.net/api/v1/app/users?page=1&pageSize=20"

# Change group
curl -X PATCH -H "Authorization: Bearer czlc_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"group":"t3"}' \
  https://connect.czl.net/api/v1/app/users/1001

# Sign a user out
curl -X POST -H "Authorization: Bearer czlc_live_xxx" \
  https://connect.czl.net/api/v1/app/users/1001/revoke

Go

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
)

const apiKey = "czlc_live_xxx"

func listUsers() {
    req, _ := http.NewRequest("GET", "https://connect.czl.net/api/v1/app/users?pageSize=50", nil)
    req.Header.Set("Authorization", "Bearer "+apiKey)
    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    fmt.Println(string(body))
    _ = json.RawMessage{}
}

Node.js

const API_KEY = process.env.CZL_CONNECT_API_KEY!;

async function setGroup(userId: string, group: string) {
  const r = await fetch(`https://connect.czl.net/api/v1/app/users/${userId}`, {
    method: "PATCH",
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ group }),
  });
  if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
  return r.json();
}

Security recommendations

  • Issue keys to servers only; never put them in frontend code, mobile apps, or public repositories
  • Use different keys for different environments (production / staging / sandbox) so you can revoke precisely during an incident
  • Minimize scopes: grant only the scopes you need, and do not select all by default
  • If you suspect a key has leaked, revoke it immediately and create a new one instead of using the grace period
  • Rotate keys proactively on a schedule (for example quarterly) to reduce the risk of long-term exposure
  • Monitor your backend logs for abnormal 429 / 403 responses