Unprofane
DocsAPI reference

API reference

Generated from the same schemas the API validates with. Base URL https://api.unprofane.com. JSON in, JSON out, problem details on failure.

Authentication

Send Authorization: Bearer <key>. Keys are up_live_… (billed), up_test_… (for building: one daily cap per account, never billed on a paid plan) and up_pk_… (publishable: for browsers, /v1/check only, limited to the project's allowed origins). A secret key sent from a browser is refused. The Perspective endpoint also accepts the key as ?key=.

Limits

  • Text up to 10,000 characters; 64 in the username and display_name contexts.
  • Up to 100 items per batch.
  • Bursts per key per minute by plan: free 60, hobby 120, starter 600, growth 3,000, scale 12,000, research 600, enterprise 12,000. The RateLimit-Policy header on every response says which applies; a refusal is a 429 with Retry-After.

Response headers

  • Request-Id: quote it when writing to support.
  • Unprofane-Engine: the engine version that answered.
  • Unprofane-Route: rules, model or cache.
  • Server-Timing: where the milliseconds went.

POST /v1/check

Check one text.

curl -X POST https://api.unprofane.com/v1/check \
  -H "Authorization: Bearer up_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"xXsh1tXx","context":"username","policy":"teen"}'

Request: CheckRequest

FieldTypeDefaultMeaning
text *stringThe text to check. At most 10,000 characters; 64 in the username and display_name contexts.
context"general" | "username" | "display_name" | "comment" | "chat" | "gaming" | "educational" | "creative" | "review" | "custom_text""general"What the text is: general, username, display_name, comment, chat, gaming, educational, creative, review or custom_text. Usernames are split into words and checked reversed; a comment is read as running text; in gaming a taunt about play is not acted on, under every policy but kids; in educational a word quoted or named is discussed, and in creative a quotation is dialogue.
policy"kids" | "teen" | "standard" | "strict" | string (^pol_[A-Za-z0-9]{8,32}$) | object"standard"A preset (kids, teen, standard, strict), a saved policy id (pol_…) from the dashboard, or an object mapping each category to allow, review or block.
mode"fast" | "balanced" | "thorough""balanced"fast never asks the model; balanced asks it about every text the rules do not block outright; thorough asks it about every text.
language"auto" | "en" | "es" | "pt" | "fr" | "de" | "it" | "nl" | "pl" | "tr" | "ru" | "ar" | "he" | "hi" | "id" | "ja""auto"auto detects. Set a code to skip detection when you already know the language of the text.
mask"asterisk" | "first_letter" | "grawlix" | "remove""asterisk"How the censored string is written: asterisk (s***), first_letter (s***), grawlix (#@!%) or remove.
enginestring (^\d{4}-\d{2}-\d{2}$)A dated engine version to pin this request to, such as 2026-09-29. Defaults to the project pin, then the latest stable.
listsobjectAllow and block terms for this request only, added to the project's lists: up to 50 of each, 64 characters each, read through the same disguises as the text. An allowed phrase clears only the words inside it. Not accepted with a publishable key.

Response: CheckResponse

FieldTypeMeaning
idstring (^chk_[A-Za-z0-9]{16,32}$)The check id (chk_…), for a feedback call. Checks are not stored; this is a correlation handle.
allowedbooleanTrue when the action is allow.
action"allow" | "review" | "block"allow, review or block, from the policy applied to the categories.
scorenumberThe highest category score, 0 to 1.
severity"none" | "mild" | "moderate" | "strong" | "extreme"none, mild, moderate, strong or extreme: the strongest matched term, or the model’s answer.
categoriesobjectA score from 0 to 1 per policy category: mild_profanity, profanity, sexual, insult, slur, threat, rude, sexual_minors and self_harm (the last three the model's alone) and reserved.
obfuscatedbooleanTrue when any match was reached through a named evasion transform.
evasion"homoglyph" | "zero_width" | "separator" | "leetspeak" | "repeated_chars" | "diacritics" | "mixed_script" | "fullwidth" | "combining_marks" | "phonetic" | "reversed" | "concatenation" | "case_mixing" | "self_censored"[]Every evasion tag that fired anywhere in the text, deduplicated.
matchesobject[]Each matched term with its UTF-16 span in the original, the text as typed, the normalized form, the term id, its category, severity and the evasion tags on that span.
censoredstringThe original text with every match masked as `mask` asks.
reasonstringWhy the action is what it is, in one sentence written from the matches and the policy, for a moderator or a log: the words read, their disguise, and what the model read where it was asked. Decide on `action`; the wording may change.
languageobjectThe detected language code and the detector’s confidence.
engineobjectWhich version answered, which route (rules, model or cache), and whether the answer is degraded because the model was unavailable.
timing_msnumberTime inside the engine, in milliseconds.

POST /v1/check/batch

Check up to 100 texts.

curl -X POST https://api.unprofane.com/v1/check/batch \
  -H "Authorization: Bearer up_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"id":"a","text":"Have a lovely day"},{"id":"b","text":"f.u.c.k off"}],"context":"comment","policy":"standard"}'

Request: BatchRequest

FieldTypeDefaultMeaning
items *object[]Up to 100 items, each with an id you choose (returned in order) and a text; an item may override context and policy.
context"general" | "username" | "display_name" | "comment" | "chat" | "gaming" | "educational" | "creative" | "review" | "custom_text""general"The context for every item that does not set its own.
policy"kids" | "teen" | "standard" | "strict" | string (^pol_[A-Za-z0-9]{8,32}$) | object"standard"The policy for every item that does not set its own.
mode"fast" | "balanced" | "thorough""balanced"The mode for the whole batch. Four model calls run at a time.
mask"asterisk" | "first_letter" | "grawlix" | "remove""asterisk"The mask for the whole batch.
enginestring (^\d{4}-\d{2}-\d{2}$)A dated engine version for the whole batch.
listsobjectAllow and block terms for every item, added to the project's lists.

Response: BatchResponse

FieldTypeMeaning
resultsobject | object[]One entry per item, in order: { id, result } on success or { id, error } when that item alone failed. One bad item never fails the batch.
countedintegerHow many items counted against the allowance.

POST /v1/feedback

Send a correction.

curl -X POST https://api.unprofane.com/v1/feedback \
  -H "Authorization: Bearer up_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"check_id":"chk_0123456789abcdefGHIJ","verdict":"false_positive","expected_action":"allow"}'

Request: FeedbackRequest

FieldTypeDefaultMeaning
check_id *string (^chk_[A-Za-z0-9]{16,32}$)The id from the response you are correcting.
verdict *"false_positive" | "false_negative" | "correct"false_positive, false_negative or correct.
expected_action"allow" | "review" | "block"What the action should have been, when you know.
notestringUp to 500 characters for a person to read.
textstringThe text itself, only if you want us to have it. This is the one field that is stored; it leaves with your account.

Response: FeedbackResponse

FieldTypeMeaning
idstringThe feedback id (fb_…).
receivedbooleanAlways true on a 202.

GET /v1/usage

Read this period’s usage.

curl -X GET https://api.unprofane.com/v1/usage \
  -H "Authorization: Bearer up_live_YOUR_KEY"

Response: UsageResponse

FieldTypeMeaning
periodobjectThe current billing period’s start and end, ISO 8601.
planstringThe account’s plan.
checksintegerBillable checks so far this period, from closed hours; the current hour is not yet in.
includedinteger | nullThe plan’s allowance, or null for a custom plan.
overageintegerChecks past the allowance so far.
routesobjectThe share of checks answered by the rules, the model and the cache.
degradednumberThe share of checks answered by the rules because the model was unavailable.

GET /v1/projects/prj_YOUR_PROJECT_ID/lists/allow

Read a project list.

curl -X GET https://api.unprofane.com/v1/projects/prj_YOUR_PROJECT_ID/lists/allow \
  -H "Authorization: Bearer up_live_YOUR_KEY"

Response: List

FieldTypeMeaning
kind"allow" | "block" | "reserved"allow, block or reserved.
termsstring[]The terms as they were sent, sorted.
versionintegerBumped on every change; keys pick the new list up within a minute.

PUT /v1/projects/prj_YOUR_PROJECT_ID/lists/allow

Replace a project list.

curl -X PUT https://api.unprofane.com/v1/projects/prj_YOUR_PROJECT_ID/lists/allow \
  -H "Authorization: Bearer up_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"terms":["Scunthorpe","Dickinson","cockatoo"]}'

Request: ListPut

FieldTypeDefaultMeaning
terms *string[]Up to 5,000 terms of up to 64 characters. The list is replaced, not merged.

Response: List

FieldTypeMeaning
kind"allow" | "block" | "reserved"allow, block or reserved.
termsstring[]The terms as they were sent, sorted.
versionintegerBumped on every change; keys pick the new list up within a minute.

DELETE /v1/projects/{id}/lists/{kind}

Empty a list. Answers 204. Same auth as the list reads.

POST /v1alpha1/comments:analyze

Byte-compatible with Google's Perspective API: the same request body, the same response, the same error envelope, the key accepted as ?key=. comments:suggestscore is answered too. The migration hub has the guides; the attribute names and languages accepted are the production set from Google's discovery document.

GET /health

No auth. Returns { ok, engine, lexicon }.

Problem details

Every failure is application/problem+json (RFC 9457) with a stable type. The types:

TypeStatusTitle
invalid-request400The request body is not valid.
unauthorized401No valid API key.
key-revoked401This API key was revoked.
forbidden-origin403This origin is not allowed for this publishable key.
forbidden-secret-cors403Secret keys cannot be used from a browser.
not-found404No such resource.
method-not-allowed405Method not allowed.
unsupported-media-type415Send application/json.
payload-too-large413The request body is too large.
subscription-required402A plan is required.
quota-exceeded429This plan’s monthly checks are used up.
rate-limited429Too many requests.
test-key-daily-cap429Test keys are capped per day.
engine-unknown400No such engine version.
misconfigured503The server is temporarily misconfigured.