API reference
zhsafe checks Chinese text and returns one verdict, pass, review or block, with the categories that were flagged, the matched text and an English explanation.
The API takes and returns JSON over HTTPS. Send Content-Type: application/json with every request.
| Base URL | https://zhsafe.tkl.ai |
|---|---|
| Endpoints | POST /v1/text/check, POST /v1/text/check/batch, POST /v1/feedback |
| Encoding | UTF-8. Simplified and Traditional Chinese, mixed with Latin text, digits and emoji. |
Examples on this page use https://zhsafe.tkl.ai.
Authentication
Every request carries an API key as a bearer token. Live keys start with zs_live_.
Authorization: Bearer zs_live_your_key
During early access, keys are issued by email. Request early access to get one.
Keep keys on your server. Don't ship them in a mobile app or browser bundle; call zhsafe from your backend and pass the verdict on.
Check text
POST/v1/text/check
Checks one piece of text and returns a verdict.
Request
{
"text": "加我微信 abc123 领优惠",
"scene": "chat",
"strategy": "fast",
"cache": true,
"explain": true,
"metadata": { "user_id": "u_123" }
}
| Field | Type | Description |
|---|---|---|
text | string, required | UTF-8 text to check. Up to 10,000 characters per request. |
scene | string | Where the text appears. Thresholds and engine routing depend on it. One of the scenes below. Default post. |
strategy | string | fast or consensus. See strategies. Default fast. |
cache | boolean | Allow a cached result for identical text in the same scene. Default true. |
explain | boolean | Include an English explanation for each flagged category. Default false. |
metadata | object | Your own fields, such as a user or message ID. Stored as-is so you can find the call in your logs later. Not used for the verdict. |
Scenes
| Scene | Use it for |
|---|---|
chat | In-game and in-app chat messages, direct messages. |
nickname | Usernames, display names, guild and team names, profile fields. |
post | Posts, articles and other long-form user content. The default. |
comment | Comments, replies and reviews. |
ad | Ad copy, product titles and descriptions, listings. Checks contact details that pull users off-platform and claims restricted by China's Advertising Law, such as 最 and 第一. |
llm_input | Prompts sent to a language model. |
llm_output | Responses generated by a language model, before you show them to users. |
Strategies
| Strategy | What happens | Choose it when |
|---|---|---|
fast | One engine, picked for the scene, checks the text. | Latency matters most, as in live chat. |
consensus | Several engines check the text and vote. Disagreement becomes review. | You want fewer false positives and fewer misses, as for posts, listings or model output. Priced higher than fast. |
Response
{
"id": "chk_01J8Z3K6T2QF9W4M7XN5B0D1RE",
"verdict": "block",
"categories": [
{
"category": "ads",
"score": 0.92,
"agreement": 1,
"spans": [{ "start": 0, "end": 11, "text": "加我微信 abc123" }],
"explanation": "Asks users to add an outside WeChat account to claim a discount, which moves them off your platform."
}
],
"strategy": "consensus",
"cached": false,
"latency_ms": 312
}
| Field | Description |
|---|---|
id | ID of this check. Use it for feedback and when contacting support. |
verdict | pass, review or block. See verdicts. |
categories | Flagged categories, highest score first. Can be empty when the verdict is pass. |
categories[].category | One of the categories. |
categories[].score | Confidence from 0 to 1 that the text belongs to this category. |
categories[].agreement | Of the engines that flagged the text, the share that reported this category, from 0 to 1. With fast only one engine runs, so it is always 1. With consensus, 0.67 means two out of three flagging engines put the text in this category. |
categories[].spans | The matched text. start and end are character offsets into your text, end exclusive. They can be missing when an engine reports only the matched text; text is always present. |
categories[].explanation | One or two English sentences on why the category was flagged. Present when explain is true. |
strategy | The strategy that produced this result. |
cached | true when the result came from the cache. |
latency_ms | Time spent on the check, in milliseconds. |
Batch check
POST/v1/text/check/batch
Checks up to 100 texts in one request. Each item takes its own scene and metadata; strategy, cache and explain apply to the whole batch. Each item counts as one check.
{
"strategy": "fast",
"items": [
{ "text": "快乐小熊猫", "scene": "nickname", "metadata": { "user_id": "u_123" } },
{ "text": "全网最低价!第一品牌纯棉T恤", "scene": "ad" }
]
}
| Field | Type | Description |
|---|---|---|
items | array, required | 1 to 100 items, each { text, scene?, metadata? } with the same meaning as in Check text. |
strategy | string | fast or consensus. Default fast. |
cache | boolean | Allow cached results. Default true. |
explain | boolean | Include English explanations. Default false. |
The response holds one result per item, in the same order as items. Each result has the same shape as a single check. If one item fails, its position holds an error object instead, and the other items are unaffected.
{
"results": [
{ "id": "chk_...", "verdict": "pass", "categories": [], "strategy": "fast", "cached": true, "latency_ms": 4 },
{ "id": "chk_...", "verdict": "block", "categories": [{ "category": "ads", "score": 0.88, "agreement": 1, "spans": [{ "text": "最低价" }, { "text": "第一" }] }], "strategy": "fast", "cached": false, "latency_ms": 287 },
{ "error": { "code": "invalid_request", "message": "`text` must be a non-empty string" } }
]
}
Feedback
POST/v1/feedback
Tells us a verdict was wrong. Corrections go into a review queue. Once a correction is confirmed, cached results for the same text are refreshed, so it gets the corrected verdict next time.
{
"id": "chk_01J8Z3K6T2QF9W4M7XN5B0D1RE",
"expected": "pass",
"note": "Game term, not an ad."
}
| Field | Type | Description |
|---|---|---|
id | string, required | The id of the check you're correcting. |
expected | string, required | The verdict you expected: pass, review or block. |
note | string | Context for the reviewer, in English or Chinese. |
A queued correction returns 200 with { "ok": true }. You can only correct checks made with your own key; any other id returns 404 not_found.
Verdicts
| Verdict | Meaning | Typical handling |
|---|---|---|
| pass | Nothing flagged. | Publish. |
| review | Not certain: the engines disagree, or the score sits in the middle band. | Send to human review, or decide per category with your own risk tolerance. |
| block | Flagged with high confidence. | Reject, or hide and notify the user. |
Policies usually differ per category. A game might let mild abuse through with a warning while always blocking politics. Read categories rather than only verdict when you need that control.
Categories
Every engine's labels are mapped onto this one set. Categories are kept separate rather than merged, so you can set a policy for each.
| Category | Covers |
|---|---|
politics | Content about state leaders, political events and systems, separatism, or historical narratives that are regulated in mainland China. |
terrorism | Terrorism and violent extremism. |
violence | Violence, gore, self-harm and threats. |
porn | Sexual, vulgar or sexually suggestive content. |
gambling | Gambling and betting. |
drugs | Illegal drugs. |
illegal | Other illegal activity: weapons, fraud, illicit online services, trade in prohibited goods. |
abuse | Insults, personal attacks and discrimination. |
minors | Content harmful to minors. |
ads | Advertising and traffic diversion: contact details, off-platform invitations, and wording restricted by China's Advertising Law. |
spam | Flooding, repetitive or meaningless content. |
religion | Religious content covered by specific rules in mainland China. |
other | Flagged by an engine but outside the categories above. |
Errors
Errors use standard HTTP status codes and a JSON body with a stable code and a readable message.
{
"error": {
"code": "invalid_request",
"message": "text is required"
}
}
| HTTP | code | Meaning and what to do |
|---|---|---|
| 400 | invalid_request | A field is missing or invalid. The message says which. Fix the request; don't retry as-is. |
| 401 | unauthorized | The API key is missing, malformed or revoked. |
| 402 | quota_exceeded | Your quota is used up. Contact us to raise it. |
| 404 | not_found | Unknown path, or a feedback id that doesn't belong to your key. |
| 429 | rate_limited | Too many requests. Retry with exponential backoff. |
| 503 | providers_unavailable | No engine could answer. Rare. Retry with backoff, or treat the text as review until it can be checked. |
| 500 | internal_error | Something failed on our side. Retry with backoff. |
Code examples
Check a chat message and act on the verdict. Set ZHSAFE_KEY to your API key.
curl https://zhsafe.tkl.ai/v1/text/check \
-H "Authorization: Bearer $ZHSAFE_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "加我微信 abc123 领优惠", "scene": "chat", "explain": true}'
# Batch
curl https://zhsafe.tkl.ai/v1/text/check/batch \
-H "Authorization: Bearer $ZHSAFE_KEY" \
-H "Content-Type: application/json" \
-d '{"items": [{"text": "快乐小熊猫", "scene": "nickname"}, {"text": "周末一起组队吗?", "scene": "chat"}]}'
// Node 18+ or any runtime with fetch. Run this on your server, not in the browser.
async function checkText(text, scene = "chat") {
const res = await fetch("https://zhsafe.tkl.ai/v1/text/check", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.ZHSAFE_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ text, scene, explain: true }),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`zhsafe ${res.status} ${error.code}: ${error.message}`);
}
return res.json();
}
const result = await checkText("加我微信 abc123 领优惠");
if (result.verdict === "block") {
// reject the message
} else if (result.verdict === "review") {
// hold for a moderator
}
for (const c of result.categories) {
console.log(c.category, c.score, c.explanation);
}
import os
import requests
def check_text(text, scene="chat"):
resp = requests.post(
"https://zhsafe.tkl.ai/v1/text/check",
headers={"Authorization": f"Bearer {os.environ['ZHSAFE_KEY']}"},
json={"text": text, "scene": scene, "explain": True},
timeout=10,
)
if not resp.ok:
err = resp.json()["error"]
raise RuntimeError(f"zhsafe {resp.status_code} {err['code']}: {err['message']}")
return resp.json()
result = check_text("加我微信 abc123 领优惠")
if result["verdict"] == "block":
pass # reject the message
elif result["verdict"] == "review":
pass # hold for a moderator
for c in result["categories"]:
print(c["category"], c["score"], c.get("explanation"))