1. Get a key
Sign in with your email. The link creates your account and shows a test key once: up_test_…. Test keys are for building and never billed: 100 checks a day across the account, with a plan or without one. When you ship, choose a plan (from $9 a month) and make a live key in the dashboard; live and publishable keys answer only on a plan.
2. Send a text
Replace the key, run it. The username xXsh1tXx is checked against the teen policy.
curl -X POST https://api.unprofane.com/v1/check \
-H "Authorization: Bearer up_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"xXsh1tXx","context":"username","policy":"teen"}'const res = await fetch('https://api.unprofane.com/v1/check', {
method: 'POST',
headers: { Authorization: 'Bearer up_test_YOUR_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({
"text": "xXsh1tXx",
"context": "username",
"policy": "teen"
}),
});
const result = await res.json();import httpx
r = httpx.request(
"POST",
"https://api.unprofane.com/v1/check",
headers={"Authorization": "Bearer up_test_YOUR_KEY"},
json={
"text": "xXsh1tXx",
"context": "username",
"policy": "teen",
},
timeout=5.0,
)
result = r.json()req, _ := http.NewRequest("POST", "https://api.unprofane.com/v1/check",
strings.NewReader(`{"text":"xXsh1tXx","context":"username","policy":"teen"}`))
req.Header.Set("Authorization", "Bearer up_test_YOUR_KEY")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)$res = file_get_contents('https://api.unprofane.com/v1/check', false, stream_context_create(['http' => [
'method' => 'POST',
'header' => "Authorization: Bearer up_test_YOUR_KEY\r\nContent-Type: application/json\r\n",
'content' => '{"text":"xXsh1tXx","context":"username","policy":"teen"}',
]]));
$result = json_decode($res, true);require 'net/http'; require 'json'
uri = URI('https://api.unprofane.com/v1/check')
req = Net::HTTP::Post.new(uri, 'Authorization' => 'Bearer up_test_YOUR_KEY', 'Content-Type' => 'application/json')
req.body = '{"text":"xXsh1tXx","context":"username","policy":"teen"}'
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
result = JSON.parse(res.body)using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "up_test_YOUR_KEY");
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.unprofane.com/v1/check");
req.Content = new StringContent("{\"text\":\"xXsh1tXx\",\"context\":\"username\",\"policy\":\"teen\"}", Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
var result = await res.Content.ReadFromJsonAsync<JsonElement>();var http := HTTPRequest.new()
add_child(http)
http.request_completed.connect(func(_r, _code, _h, body):
var result = JSON.parse_string(body.get_string_from_utf8())
print(result))
http.request("https://api.unprofane.com/v1/check",
PackedStringArray(["Authorization: Bearer up_test_YOUR_KEY", "Content-Type: application/json"]),
HTTPClient.METHOD_POST, "{\"text\":\"xXsh1tXx\",\"context\":\"username\",\"policy\":\"teen\"}")3. Read the answer
{
"id": "chk_…",
"allowed": false,
"action": "block",
"score": 0.9,
"severity": "strong",
"categories": { "mild_profanity": 0, "profanity": 0.9, "sexual": 0, "insult": 0, "slur": 0, "threat": 0, "rude": 0, "sexual_minors": 0, "self_harm": 0, "reserved": 0 },
"obfuscated": true,
"evasion": ["leetspeak"],
"matches": [
{ "start": 2, "end": 6, "text": "sh1t", "normalized": "shit", "term_id": "en:shit",
"category": "profanity", "severity": "strong", "evasion": ["leetspeak"] }
],
"censored": "xX****Xx",
"language": { "detected": "en", "confidence": 0.6 },
"engine": { "version": "2026-09-29", "route": "rules", "degraded": false },
"timing_ms": 1
}actionis what to do: allow, review or block.allowedis the same answer as a boolean.matchesis the evidence: the span in the original, what was typed, what it normalised to, and the evasion tags that got it there. Heresh1twas read asshitthroughleetspeak.censoredis ready to display.engine.routesays who answered: the rules at the edge, the model, or the cache.degradedis true when the model was wanted and unavailable, and the rules answered alone.
4. Choose a context and a policy
context tells the engine what the text is; usernames are split into words and read backwards, comments are read as prose. policy is how strict to be: kids, teen, standard or strict, or your own map of category to action. Both can be set as project defaults in the dashboard so a request can leave them out.
5. Ship it
Keep the secret key on your server. For a username field checked from the browser, make a publishable key with your site's origin allowed. Copy-paste recipes for Supabase, Clerk, Firebase, Auth0, Better Auth, Unity and Godot are on the username page.