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 URLhttps://zhsafe.tkl.ai
EndpointsPOST /v1/text/check, POST /v1/text/check/batch, POST /v1/feedback
EncodingUTF-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" }
}
FieldTypeDescription
textstring, requiredUTF-8 text to check. Up to 10,000 characters per request.
scenestringWhere the text appears. Thresholds and engine routing depend on it. One of the scenes below. Default post.
strategystringfast or consensus. See strategies. Default fast.
cachebooleanAllow a cached result for identical text in the same scene. Default true.
explainbooleanInclude an English explanation for each flagged category. Default false.
metadataobjectYour 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

SceneUse it for
chatIn-game and in-app chat messages, direct messages.
nicknameUsernames, display names, guild and team names, profile fields.
postPosts, articles and other long-form user content. The default.
commentComments, replies and reviews.
adAd 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_inputPrompts sent to a language model.
llm_outputResponses generated by a language model, before you show them to users.

Strategies

StrategyWhat happensChoose it when
fastOne engine, picked for the scene, checks the text.Latency matters most, as in live chat.
consensusSeveral 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
}
FieldDescription
idID of this check. Use it for feedback and when contacting support.
verdictpass, review or block. See verdicts.
categoriesFlagged categories, highest score first. Can be empty when the verdict is pass.
categories[].categoryOne of the categories.
categories[].scoreConfidence from 0 to 1 that the text belongs to this category.
categories[].agreementOf 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[].spansThe 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[].explanationOne or two English sentences on why the category was flagged. Present when explain is true.
strategyThe strategy that produced this result.
cachedtrue when the result came from the cache.
latency_msTime 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" }
  ]
}
FieldTypeDescription
itemsarray, required1 to 100 items, each { text, scene?, metadata? } with the same meaning as in Check text.
strategystringfast or consensus. Default fast.
cachebooleanAllow cached results. Default true.
explainbooleanInclude 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."
}
FieldTypeDescription
idstring, requiredThe id of the check you're correcting.
expectedstring, requiredThe verdict you expected: pass, review or block.
notestringContext 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

VerdictMeaningTypical handling
passNothing flagged.Publish.
reviewNot 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.
blockFlagged 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.

CategoryCovers
politicsContent about state leaders, political events and systems, separatism, or historical narratives that are regulated in mainland China.
terrorismTerrorism and violent extremism.
violenceViolence, gore, self-harm and threats.
pornSexual, vulgar or sexually suggestive content.
gamblingGambling and betting.
drugsIllegal drugs.
illegalOther illegal activity: weapons, fraud, illicit online services, trade in prohibited goods.
abuseInsults, personal attacks and discrimination.
minorsContent harmful to minors.
adsAdvertising and traffic diversion: contact details, off-platform invitations, and wording restricted by China's Advertising Law.
spamFlooding, repetitive or meaningless content.
religionReligious content covered by specific rules in mainland China.
otherFlagged 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"
  }
}
HTTPcodeMeaning and what to do
400invalid_requestA field is missing or invalid. The message says which. Fix the request; don't retry as-is.
401unauthorizedThe API key is missing, malformed or revoked.
402quota_exceededYour quota is used up. Contact us to raise it.
404not_foundUnknown path, or a feedback id that doesn't belong to your key.
429rate_limitedToo many requests. Retry with exponential backoff.
503providers_unavailableNo engine could answer. Rare. Retry with backoff, or treat the text as review until it can be checked.
500internal_errorSomething 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"}]}'