Hate API / Documentation
A small guide to
getting started.
Send content and context. Read meaning, concepts, and review signals.
Explore the playgroundUse HateAPI in your code.
Open the guided setup to create your key and confirm your first request.
Start with the AddressHate-hosted model
Try an example in the playground, then create a free account to classify your own content. You do not need an API key to use the playground.
Ready to integrate? Select Use this in my app beside your result. Create a HateAPI key there, copy the runnable code, and confirm your first successful API request without leaving your selected content.
Or sign in and create a key in HateAPI keys to use the example below. Copy the key when it appears; it is shown only once.
Hosted Qwen3.5 4B (AddressHate) uses your free classifications, with no external provider key or provider header. Every account gets 10, shared between the playground and API. Text, context and media share a 4096-token input/output budget.
Set HATE_API_KEY in your server environment. Keep credentials out of browser code and source control. These examples call https://api.hateapi.com/v1/classifications; HATE_API_BASE_URL is optional for another authorized deployment.
JavaScript · Node.js 18+ Save as classify.mjs · node classify.mjs
import { randomUUID } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
// Read settings from a .env file in this folder, if there is one. Real environment variables win.
if (existsSync(".env")) {
for (const line of readFileSync(".env", "utf8").split(/\r?\n/)) {
const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
if (match && process.env[match[1]] === undefined) process.env[match[1]] = match[2].replace(/^(["'])(.*)\1$/, "$2");
}
}
function setting(name, what) {
const value = process.env[name];
if (!value || value.startsWith("paste-your-")) {
console.error(`Set ${name} to ${what}, in .env or your environment.`);
process.exit(1);
}
return value;
}
const key = setting("HATE_API_KEY", "your HateAPI key");
const baseUrl = (process.env.HATE_API_BASE_URL ?? "https://api.hateapi.com").replace(/\/$/, "");
const response = await fetch(`${baseUrl}/v1/classifications`, {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(),
},
body: JSON.stringify({
"content": {
"type": "text",
"text": "Wishing our Jewish friends a peaceful holiday.",
"language": "en"
},
"options": {
"inference_provider": "addresshate",
"model": "AddressHate/AH-Qwen3.5-4B-Antisemitism",
"prompt_variant": "frozen"
}
}),
signal: AbortSignal.timeout(130_000),
});
const text = await response.text();
if (!response.ok) {
console.error(`HateAPI returned HTTP ${response.status}: ${text}`);
process.exit(1);
}
console.log(JSON.stringify(JSON.parse(text), null, 2));Python 3 · no extra packages Save as classify.py · python classify.py
import json
import os
import sys
import urllib.error
import urllib.request
import uuid
# Read settings from a .env file in this folder, if there is one. Real environment variables win.
if os.path.exists(".env"):
with open(".env", encoding="utf-8") as env_file:
for line in env_file:
name, sep, value = line.strip().partition("=")
if sep and name.strip() and not name.strip().startswith("#"):
os.environ.setdefault(name.strip(), value.strip().strip("\"'"))
def setting(name, what):
value = os.environ.get(name, "")
if not value or value.startswith("paste-your-"):
sys.exit(f"Set {name} to {what}, in .env or your environment.")
return value
headers = {
"Authorization": "Bearer " + setting("HATE_API_KEY", "your HateAPI key"),
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
}
base_url = os.environ.get("HATE_API_BASE_URL", "https://api.hateapi.com").rstrip("/")
payload = json.loads("{\"content\":{\"type\":\"text\",\"text\":\"Wishing our Jewish friends a peaceful holiday.\",\"language\":\"en\"},\"options\":{\"inference_provider\":\"addresshate\",\"model\":\"AddressHate/AH-Qwen3.5-4B-Antisemitism\",\"prompt_variant\":\"frozen\"}}")
request = urllib.request.Request(
base_url + "/v1/classifications",
data=json.dumps(payload).encode("utf-8"),
headers=headers,
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=130) as response:
print(json.dumps(json.load(response), indent=2))
except urllib.error.HTTPError as error:
sys.exit(f"HateAPI returned HTTP {error.code}: {error.read().decode('utf-8', 'replace')}")A successful run prints classification JSON. If you started from the playground, select Check my API request to confirm the connection. Usage shows your requests and errors. Reuse the same idempotency key when retrying the same request.
Use another model with your provider
Select a model and provider in the playground. To try it there, save that provider’s key in Provider keys. Your application separately supplies HATE_PROVIDER_API_KEY through X-Provider-API-Key, alongside your HateAPI bearer key. Saving a provider key on the website does not configure your application. Your provider bills model usage.
External provider example · OpenRouter / JavaScript
import { randomUUID } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
// Read settings from a .env file in this folder, if there is one. Real environment variables win.
if (existsSync(".env")) {
for (const line of readFileSync(".env", "utf8").split(/\r?\n/)) {
const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
if (match && process.env[match[1]] === undefined) process.env[match[1]] = match[2].replace(/^(["'])(.*)\1$/, "$2");
}
}
function setting(name, what) {
const value = process.env[name];
if (!value || value.startsWith("paste-your-")) {
console.error(`Set ${name} to ${what}, in .env or your environment.`);
process.exit(1);
}
return value;
}
const key = setting("HATE_API_KEY", "your HateAPI key");
const providerKey = setting("HATE_PROVIDER_API_KEY", "your provider's API key");
const baseUrl = (process.env.HATE_API_BASE_URL ?? "https://api.hateapi.com").replace(/\/$/, "");
const response = await fetch(`${baseUrl}/v1/classifications`, {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(),
"X-Provider-API-Key": providerKey,
},
body: JSON.stringify({
"content": {
"type": "text",
"text": "Wishing our Jewish friends a peaceful holiday.",
"language": "en"
},
"options": {
"inference_provider": "openrouter_decisions",
"model": "openai/gpt-6-luna-decisions",
"prompt_variant": "brief"
}
}),
signal: AbortSignal.timeout(130_000),
});
const text = await response.text();
if (!response.ok) {
console.error(`HateAPI returned HTTP ${response.status}: ${text}`);
process.exit(1);
}
console.log(JSON.stringify(JSON.parse(text), null, 2));The playground’s Code tab preserves the selected model, provider, text, context and actual attachment bytes. Request previews label media placeholders; placeholders cannot be classified. Use a compatible model and preserve media roles and video mode.
You can also download reviewed prompts and open models to run independently under their own licenses.
Build a request in the playgroundMore than a label.
Recorded dataset targets
Gold annotations contain three arrays: ideation_codes, leaf_codes, and ideology_labels. Result renders their meanings in English; Response preserves the arrays. Empty arrays remain empty, without invented confidence or rationale.
Live classification
A successful classification displays the real API result alongside the recorded sample targets. Concepts use an included/excluded view; any probabilities remain available in the response. Decision models answer an independent antisemitism question, a seven-way ideation choice, and 40 conceptual questions. Probabilities are not measured accuracy. These models do not predict ideology labels.
The taxonomy has 40 concepts in six groups. Dataset-card wording and frozen classifier definitions differ for some ideation codes, including generic I1. Recorded and live meanings retain their respective definitions.
Review and policy context
The API exposes review signals, including disagreement between decision heads. The site separately shows policy connections across ten frameworks, from the policy crosswalk. A connection does not establish a violation; target, stance, context, and policy conditions still matter.
Read the policy mappingReal examples. Visible context.
25 verified train, validation, and test examples from 11 platforms, plus a box for your own post. They cover hateful claims, counterspeech, co-textual meaning, confirmation, coded language and benign content, with and without image or video attachments. Focal text and captured context stay separate. The video claim and its counterspeech reply can be explored together.
Signed out, you get two live runs of the examples with no key: they use AddressHate’s own OpenRouter key on a benchmarked model. After that, Classify asks you to sign in, while preserving the selected example. Your own or edited text always needs sign-in. Editing text or context clears previous results. Errors appear in the result panel. The interface separates provider-reported latency from elapsed request time and shows cost only when the API returns it.
To keep classifying, create an account or sign in with Google. Save an external provider key only when the selected route requires one. Create a HateAPI key later, when you integrate with your application. Hosted AddressHate routes use your account allowance without an external provider key. Keys are saved to your account, encrypted with Cloud KMS envelope encryption; after saving, only the last four characters are ever shown, and the key is sent only to that provider when you classify. Response shows only actual API responses. Results compare the model prediction with the original human annotation.
View the source datasetRead the trade-offs in context.
Binary F1 measures antisemitism classification. Concept micro F1 aggregates performance across conceptual labels. Neither is the probability that an individual prediction is correct. Costs are USD per 1,000 posts, not a published Hate API price.
The six decision configurations were checked against committed OpenRouter Decisions evaluation reports, with OpenAI, TypeSafe and Cloudflare provider identities. Classification metrics use 951 test posts; cost and latency use 1,841 deduplicated validation/test requests. Cross-family comparisons remain approximate, and unverified figures are identified.
Brief live smoke requests passed for Luna, Jev and Omni. They confirm request compatibility, not model quality or hosted deployment. The playground sends attached image and video bytes through compatible live routes; video mode is explicit, with native video or sampled frames depending on the provider. The Qwen3.8 27B action opens its Hugging Face repository; it does not claim a hosted endpoint.
The research behind the interface.
Uses resources from AddressHate, Inc. Dataset and model licenses govern reuse; third-party materials retain their applicable rights.