This documentation guides you through integrating CZL Connect into your app.
Have a question? Leave a message on the forumSend email to your users through CZL Connect's mail infrastructure, without running your own SMTP server, sending domain or unsubscribe handling. Three ways to send:
| Method | Recipients | Content | How |
|---|---|---|---|
| Verification codes | Any address | Fixed template; CZL Connect generates and verifies the code | API (email:code) |
| Custom email | Your app's users | HTML or Markdown, unsubscribe link added automatically | API (email:send) |
| Broadcasts | Your app's users + manually added addresses | Markdown, can be scheduled | App details → Email, or API (broadcast:write) |
See App API for keys and authentication: create a key under App details → API access with the scopes you need, send Authorization: Bearer czlc_live_..., base URL https://connect.czl.net/api/v1/app.
Availability and quota
Available by the app owner's plan. All three methods share the account's monthly email quota (all apps in the account combined, calendar month in Beijing time):
| Plan | Monthly email quota | Daily verification codes |
|---|---|---|
| Lite | Not available | Not available |
| Plus | 5,000 | 2,000 |
| Pro | 30,000 | 10,000 |
| Enterprise | Unlimited | 50,000 |
A verification code counts only once it is sent. Custom emails and broadcasts count per recipient entering the send queue; failed deliveries are not refunded.
Sender address
The display name is "App name (via CZL Connect)" and the default address is [email protected]. After setting a sender alias under App details → Email, the address becomes czlconnect_<alias>@notify.czl.net, so recipients can tell apps apart by address.
- Aliases are 3-20 lowercase letters, digits or hyphens, unique across the site
- Setting an alias requires a plan that includes email (Plus or above) and passkey verification
- The alias can only be set once and can't be changed or cleared by you afterwards; contact support if you really need to change it. A replaced alias stays reserved for its original app for 90 days, so other apps can't take it
- Recipients' filters and allowlists for the old address stop working after a change
- Aliases cannot contain words that impersonate official functions or well-known brands (such as
admin,billing,security,apple,paypal) - The sender address does not receive mail; don't ask users to reply
Verification codes
For sending codes to people who aren't your users yet: confirming a guest is a real person before subscribing, verifying an address before sign-up, or double-checking before account deletion. CZL Connect generates, sends and verifies the code; your backend only gets a code id.
Send
POST /api/v1/app/email-codes
Authorization: Bearer czlc_live_...
Content-Type: application/json
{
"email": "[email protected]",
"purpose": "subscribe",
"locale": "en",
"clientIp": "203.0.113.9"
}
| Field | Required | Description |
|---|---|---|
email | Yes | Recipient address, any address |
purpose | No | Determines the subject and wording, defaults to general, see below |
locale | No | Email language zh / en, defaults to zh |
clientIp | Recommended | The end user's IP, used for per-IP rate limiting; only public addresses are accepted, omit to skip per-IP limits |
purpose | Subject |
|---|---|
general | App name verification code |
signup | App name sign-up verification code |
login | App name sign-in verification code |
subscribe | App name subscription confirmation code |
delete_account | App name account deletion verification code (body warns the action cannot be undone) |
The email is sent synchronously and the call returns once the mail server accepts it (usually a second or two), with no queueing:
{ "id": "ec_Xh3...", "expiresIn": 600, "expiresAt": "2026-11-14T10:20:00+08:00" }
Codes are 6 digits and valid for 10 minutes.
Verify
POST /api/v1/app/email-codes/verify
Content-Type: application/json
{ "id": "ec_Xh3...", "code": "482913" }
{ "verified": true, "email": "[email protected]", "purpose": "subscribe" }
A code is voided as soon as it verifies. A wrong code returns code_mismatch with attemptsRemaining; after 5 wrong attempts the code is voided and a new one must be sent. Record the email from this response, not the one submitted by the client.
Rate limits
| Scope | Limit |
|---|---|
| Same app, same recipient | 1 per minute |
| Same recipient (all apps) | 10 per 24 hours |
Same app, same clientIp | 20 per hour |
| Account (all apps) | Daily verification code limit, see the plan table |
We recommend a 60-second countdown on your "resend" button. A few mailbox providers greylist first-time senders, delaying delivery by a few minutes; ask users to check their spam folder on the verification screen.
Custom email
Send notifications to your app's users (order status, account reminders and so on) with your own content.
POST /api/v1/app/emails
Content-Type: application/json
{
"userIds": ["1024", "2048"],
"subject": "Your order has shipped",
"html": "<p>Order <b>#1234</b> has shipped.</p>"
}
| Field | Description |
|---|---|
userIds | User IDs in this app (strings or numbers), 1-100 per call |
subject | Subject, up to 200 characters |
html / markdown | Body, exactly one, up to 200 KB |
HTML is sanitized: inline styles, table layouts and http(s) images and links are kept; scripts, forms, iframe and event attributes are removed. CZL Connect wraps the body (app name, why the user received it, unsubscribe link) and adds a one-click List-Unsubscribe header.
Returns 202 with a result per recipient. Accepted emails enter the send queue immediately and usually go out within seconds:
{
"queued": 1,
"results": [
{ "userId": "1024", "status": "queued" },
{ "userId": "2048", "status": "skipped", "reason": "unsubscribed" }
]
}
reason | Description |
|---|---|
not_app_user | Not a user of this app |
banned | Banned site-wide or disabled in this app |
unsubscribed | Unsubscribed from this app's notifications |
email_unavailable | No verified real email address (such as QQ / WeChat sign-in without an added email) |
rate_limited | This user has already received 20 custom emails today |
Once a user unsubscribes, neither broadcasts nor custom emails are sent to them; verification codes are not affected. Custom emails not yet sent when the service restarts may be lost; for must-deliver content, rely on your own business state.
Broadcasts
Same rules as creating a broadcast under App details → Email: a recipient snapshot is taken at creation, unsubscribed and banned users are excluded again at send time, broadcasts can be scheduled, and tasks that haven't started can be canceled.
POST /api/v1/app/broadcasts
Content-Type: application/json
{
"subject": "November update",
"markdown": "## What's new\n\n- ...",
"audience": "all",
"groups": ["t1", "t2"],
"scheduledAt": "2026-11-20T10:00:00+08:00"
}
| Field | Description |
|---|---|
audience | all all users / single one user (with userId) / exclude exclude some users (with excludeUserIds) / extra_only manually added addresses only |
groups | For all / exclude, filter by in-app group; empty means all |
extraEmails | Extra external addresses (users who haven't signed in with CZL Connect), up to 1000 per task, counted against the quota |
scheduledAt | RFC 3339 send time; empty sends immediately (picked up by a job that runs every minute) |
Other endpoints:
| Endpoint | Description |
|---|---|
GET /broadcasts?page=&pageSize= | List tasks, newest first |
GET /broadcasts/:broadcastId | Task status and counts (sentCount / failedCount / skippedCount) |
POST /broadcasts/:broadcastId/cancel | Cancel a task that hasn't started sending |
Quota
GET /api/v1/app/mail/quota
Requires any of email:code, email:send or broadcast:write. Returns the sender address, daily verification code limit, this app's usage today and this month, and the account's monthly quota.
Error codes
Errors look like {"error": "<code>", "message": "<description>"}:
| HTTP | error | Description |
|---|---|---|
| 400 | invalid_request / invalid_email / invalid_purpose | Invalid parameters |
| 400 | code_mismatch | Wrong code, with attemptsRemaining |
| 400 | code_expired | Code doesn't exist, has expired or was already used |
| 400 | code_attempts_exceeded | Too many wrong attempts, the code was voided |
| 403 | mail_disabled | Not available on the current plan |
| 403 | quota_exceeded | Monthly email quota used up |
| 429 | rate_limited | Recipient or IP rate limit hit |
| 429 | daily_limit_exceeded | The account's daily verification code limit was reached |
| 502 | send_failed | The email could not be sent; retry later |
| 503 | temporarily_unavailable | Service busy; retry later |