FilterIt

Developer reference · v1

Build moderation into your product

One endpoint for category decisions, with optional paid explanations and input excerpts. Use the decision, coverage, and request ID together when applying your product policy.

Quick start

MethodPathAuthentication
POST/api/v1/analyzex-api-key: your server-held API key
OPTIONS/api/v1/analyzeNone; CORS preflight only

Use Content-Type: application/json and set FILTERIT_BASE_URL to your deployment's origin. The public base URL is https://filterit.io.

cURL

# Configure an active deployment and a server-held API key.
# Local development: FILTERIT_BASE_URL=http://localhost:3000
curl "$FILTERIT_BASE_URL/api/v1/analyze" \
  -H "x-api-key: $FILTERIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Your order arrived today. Thank you!",
    "options": { "mode": "fast", "strictness": "balanced" }
  }'

Server-side JavaScript

// Server-side JavaScript. Never expose this key in browser code.
const baseUrl = process.env.FILTERIT_BASE_URL;
const apiKey = process.env.FILTERIT_API_KEY;
if (!baseUrl || !apiKey) throw new Error("Configure FilterIt on your server.");

const response = await fetch(new URL("/api/v1/analyze", baseUrl), {
  method: "POST",
  headers: { "Content-Type": "application/json", "x-api-key": apiKey },
  body: JSON.stringify({
    text: "Your order arrived today. Thank you!",
    options: { mode: "fast", strictness: "balanced" }
  })
});
const body = await response.json();
if (!response.ok) {
  // Keep the request ID for diagnostics; do not log text or the API key.
  throw new Error(
    "FilterIt status " + response.status + ", request " + (body.requestId ?? "unknown")
  );
}

// A review result is a successful response that needs a policy/human decision.
if (body.data.decision === "allow") publishContent();
else if (body.data.decision === "review") queueForReview(body.requestId);
else rejectContent(body.requestId);

Python

# Python standard library; run on your server.
import json
import os
from urllib.request import Request, urlopen
from urllib.error import HTTPError

request = Request(
    os.environ["FILTERIT_BASE_URL"].rstrip("/") + "/api/v1/analyze",
    data=json.dumps({
        "text": "Your order arrived today. Thank you!",
        "options": {"mode": "fast"}
    }).encode("utf-8"),
    headers={
        "Content-Type": "application/json",
        "x-api-key": os.environ["FILTERIT_API_KEY"]
    },
    method="POST"
)
try:
    with urlopen(request, timeout=15) as response:
        body = json.load(response)
except HTTPError as error:
    body = json.load(error)
    raise RuntimeError(
        f"FilterIt status {error.code}, request {body.get('requestId', 'unknown')}"
    ) from None

# Apply your product policy to body["data"]["decision"].
# Do not retry automatically: requests have no idempotency guarantee.

Modes, quotas, and credits

ModeBehaviorCustomer cost
fast (default)Base classifier evaluates multiple enabled categories. Returns flags, coverage, and a decision. No advanced explanations or findings; no first-issue exit.0 credits. Account quota and shared rate limits apply.
thorough (explicit)Base classification plus required advanced AI. Can return explanations and verified input excerpts. Does not silently downgrade when advanced analysis fails.1 base + 1 advanced AI + length credits. Advanced availability and sufficient credit balance are required.

The default fast quota is 1,000 requests per account per 24-hour window. The default rate limit is 120 requests per minute across the account. Windows start with the first request after the previous window expires. All API keys and playground requests share these limits. Operators may configure different limits, and provider-wide caps can also make the service unavailable.

// Only thorough requests incur a length surcharge.
lengthCredits = Math.max(0, Math.ceil((text.length - 512) / 512));
thoroughCredits = 1 + 1 + lengthCredits;
fastCredits = 0;
Text length (UTF-16 code units)Fast creditsSuccessful thorough credits
1–51202
513–1,02403
3,585–4,000 (maximum)09

Thorough credits are reserved before analysis and refunded when analysis fails. If a worker stops before settlement, the reservation has a 10-minute lease; an expired reservation is reconciled when the account next makes a request. There is no automatic idle-account refund timer. A successful review decision still counts as completed analysis. Check creditsConsumed and creditBreakdown in the response. meta.aiUsed can be true for free TypeSafe calls; it is not a billing flag.

Authentication and API keys

Sign in, open API keys, and deliberately create a key for your integration. A full key is shown once; subsequent views show a masked value. FilterIt stores a hash for authentication. Store the key in your server's secret configuration and send it in the x-api-key header.

Use separate named keys for integrations, monitor their usage, and revoke a key when it is compromised or no longer needed. An account can have at most 20 active keys; revoke an unused key before creating another when the limit is reached. Revocation prevents future requests from authenticating. It does not cancel work that has already started.

Dashboard session cookies do not authenticate the public API. CORS support does not protect an embedded API key: do not put keys in frontend bundles, public mobile clients, query strings, or logs. Call FilterIt from your backend. The signed-in playground uses your account session and shares your usage limits.

Request options

FieldType / defaultBehavior
textRequired string, 1–4,000 UTF-16 code unitsMust include non-whitespace characters. Original text is preserved for offsets. No file, image, URL fetching, batch, or streaming interface.
optionsOptional objectOmitting this object applies the defaults below.
options.modefast | thorough; fastChooses free base classification or paid required advanced analysis.
options.detectProfanityboolean; trueEnables the profanity check. Disabling it excludes that check; it does not mean profanity is absent.
options.filterProfanityboolean; falseThorough mode only, with detectProfanity=true. Masks verified advanced profanity spans. A flag without valid profanity findings cannot produce filteredText. It does not remove all unsafe categories.
options.detectToneboolean; trueReturns tone where supported. A disabled or unassessed check has a neutral label and confidence=0. Tone alone does not determine the moderation decision.
options.customCategoriesstring[]; []At most 8 labels, each 1–32 code units. Labels are scoped to this request. Use meaningful, stable category names.
options.strictnesslenient | balanced | strict; balancedChanges decision thresholds. Strict is more sensitive. This is a policy preference, not a guarantee of fewer false negatives.

Paid thorough request

{
  "text": "You are an idiot.",
  "options": {
    "mode": "thorough",
    "detectProfanity": true,
    "filterProfanity": false,
    "detectTone": true,
    "customCategories": ["personal_insult"],
    "strictness": "balanced"
  }
}

Custom categories become independent semantic questions in the TypeSafe base pipeline. Multiple labels can match; they are not exclusive classes. The local development heuristic cannot establish arbitrary semantic custom categories. Advanced AI can return findings for the labels; it does not rewrite the base category scores. A label alone is not a trained policy or a reliable detector for PII, legal risk, or regulated content. Validate each label and threshold against representative synthetic examples before depending on it.

Category names are retained in usage metadata. Use policy labels, and keep personal information and confidential text out of label names.

Decisions, categories, and uncertainty

DecisionsafeHow to handle it
allowtrueThe configured checks permit the content under the selected policy. This is not a guarantee that all inappropriate content was detected.
reviewfalseCoverage is limited, a signal is ambiguous, or an assessment needs review. Use meta.reviewReasons and your human/product policy.
blockfalseAn enabled moderation check met the blocking policy. Your product determines whether to reject, transform, or review it.

Each category has name, matched, and score. Arrays are ordered by score, not by an exclusive winning category. More than one category can match the same text. matched means the score reached the selected block threshold; a lower score can still cause a review decision. Inspect meta.coverage for assessed category/profanity checks.

StrictnessReview thresholdBlock threshold
strict0.100.70
balanced0.200.80
lenient0.300.90

These are current application policy thresholds, pending validation on labeled examples. A score at or above the block threshold blocks; otherwise any score at or above the review threshold requires review. Limited development coverage also requires review. Advanced findings can escalate an allow result to review, and cannot override a base block.

Built-in categoryIntended signal
toxicityAbusive, degrading, or hostile language
harassmentTargeted insults, bullying, or intimidation
hateHateful language directed at protected groups
violenceThreats, incitement, or inappropriate violent content
sexual_contentSexually explicit or inappropriate content
self_harmSelf-harm encouragement or risk signals
spamUnwanted promotional or deceptive content

The TypeSafe base result uses uncalibrated probability scores; heuristic scores are weaker signals. Neither should be interpreted as measured accuracy or a calibrated probability of real-world harm. Quoted words, reclaimed language, sarcasm, misspellings, obfuscation, multilingual content, and context can change the correct decision. A missing match or finding does not establish that the text is safe.

Response reference

A successful response has a top-level data object, request ID, and billing fields. Errors also include a request ID. Use data.decision for workflow routing and retain the request ID for diagnostics.

Complete offline response example

Verified with the quick-start's synthetic text using the development classifier. Clean-looking text still requires review under limited coverage. The request ID, balance, and latency are example values, not live service measurements or a performance target.

{
  "data": {
    "safe": false,
    "decision": "review",
    "summary": "Content requires review; the classification is uncertain or limited.",
    "findings": [],
    "profanity": {
      "detected": false,
      "score": 0,
      "severity": "none",
      "confidence": 0,
      "matches": []
    },
    "tone": {
      "label": "neutral",
      "confidence": 0.2
    },
    "categories": [
      {
        "name": "toxicity",
        "matched": false,
        "score": 0
      },
      {
        "name": "harassment",
        "matched": false,
        "score": 0
      },
      {
        "name": "hate",
        "matched": false,
        "score": 0
      },
      {
        "name": "violence",
        "matched": false,
        "score": 0
      },
      {
        "name": "sexual_content",
        "matched": false,
        "score": 0
      },
      {
        "name": "self_harm",
        "matched": false,
        "score": 0
      },
      {
        "name": "spam",
        "matched": false,
        "score": 0
      }
    ],
    "meta": {
      "model": "heuristic-dev-v2",
      "provider": "heuristic",
      "advancedProvider": "none",
      "policyVersion": "filterit-policy-2",
      "reviewReasons": [
        "limited_classification"
      ],
      "scoreInterpretation": "heuristic_signal",
      "coverage": [
        "toxicity",
        "harassment",
        "hate",
        "violence",
        "sexual_content",
        "self_harm",
        "spam",
        "profanity"
      ],
      "mode": "fast",
      "aiUsed": false,
      "terminatedEarly": false,
      "cacheHit": false,
      "latencyMs": 1
    }
  },
  "requestId": "example-request-id",
  "creditsConsumed": 0,
  "creditBreakdown": {
    "mode": "fast",
    "textLength": 36,
    "baseCredits": 0,
    "aiCredits": 0,
    "lengthCredits": 0,
    "totalCredits": 0
  },
  "creditsRemaining": 500
}
Top-level fieldTypeMeaning
requestIdstringIdentifier for this attempt; also sent as X-Request-Id.
dataAnalysis objectFields listed below.
creditsConsumedintegerFinal customer charge; zero for fast mode.
creditBreakdownobjectmode, textLength, baseCredits, aiCredits, lengthCredits, totalCredits. Length is measured in UTF-16 code units.
creditsRemainingintegerAccount balance observed after processing; concurrent requests can change it.
Analysis fieldTypeMeaning
safebooleanTrue only when decision is allow.
decisionallow | review | blockResolved moderation workflow decision.
summarystringSummary of the result. Generated explanations can be inaccurate.
profanity.detectedbooleanWhether an enabled profanity check found a signal.
profanity.severitynone | low | medium | highSeverity derived from the classifier signal; not an independently calibrated scale.
profanity.scorenumber 0–1Raw profanity signal. TypeSafe uses its uncalibrated Noul probability. Inspect this score, not confidence, for the base signal.
profanity.confidencenumber 0–10 for TypeSafe because its Noul answer has no independent confidence estimate. 0 here does not mean no profanity. Development heuristics reuse their signal.
profanity.matchesstring[]Verified advanced profanity finding excerpts; empty in fast mode. An empty list does not negate a provider flag.
profanity.filteredTextoptional stringOnly returned when requested in thorough mode and verified profanity spans exist. Only those spans are masked; unsafe content may remain.
tone{label, confidence}label is positive, neutral, negative, or mixed. Disabled/unassessed checks use neutral and confidence=0; tone alone does not block.
categories{name, matched, score}[]Built-in/custom category signals. Check coverage and policy with the scores.
findingsFinding[]Empty in fast mode; optional generated verified excerpts in thorough mode. See findings below.
data.meta fieldTypeMeaning
providertypesafe | heuristicBase classifier used.
advancedProvideropenai | noneAdvanced provider used for thorough analysis.
modelstringBase classifier model/engine identifier. The deployment config selects the advanced model separately.
policyVersionstringFilterIt policy version; behavior may change when this changes.
reviewReasonsstring[]Reasons a result needs review. Do not assume the list is an exhaustive set of all risk causes.
scoreInterpretationuncalibrated_probability | heuristic_signalHow to interpret the base-provider scores; neither is calibrated accuracy.
coveragestring[]Assessed category/profanity names. An absent category check was not established by this result.
modefast | thoroughExecuted mode.
aiUsedbooleanWhether any AI was used. TypeSafe fast mode may have aiUsed=true and cost zero credits.
terminatedEarlybooleanRetained for compatibility. The multi-label base pipeline does not stop on the first flag.
cacheHitbooleanWhether this result came from explicitly enabled classification caching.
latencyMsnumberObserved server-side analysis/cache time, not total network round-trip time or a latency guarantee.

Thorough findings and excerpt offsets

Thorough analysis can return findings[] with a category, exact input excerpt, offsets, and explanation. FilterIt checks that each accepted finding's text equals the corresponding input substring. This confirms the excerpt came from the input; it does not establish that the category or explanation is correct, or that every offense was found.

Finding fieldTypeRule
categorystringCategory attributed to the excerpt.
textstringExact excerpt from the submitted text.
startinteger ≥ 0UTF-16 code-unit index into the original input.
endinteger > startExclusive UTF-16 code-unit index; input.slice(start, end) must equal text.
explanationstringGenerated explanation; validate before displaying or acting on it.
// Illustrative finding shape, not a measured provider result.
// Submitted text: "You are an idiot."
{
  "category": "harassment",
  "text": "idiot",
  "start": 11,
  "end": 16,
  "explanation": "An insult directed at another person."
}

// Offsets use JavaScript UTF-16 code units and an exclusive end.
inputText.slice(finding.start, finding.end) === finding.text

Do not normalize, trim, or otherwise change the input before applying its offsets. Emoji can occupy two UTF-16 code units. Python string indexes and UTF-8 byte indexes differ from these offsets; convert them before highlighting text. Fast TypeSafe decisions do not produce spans or profanity matches. Filtering requires thorough mode with profanity detection enabled; only accepted profanity findings are masked, and other unsafe content can remain.

Limits, headers, and errors

Text is limited to 4,000 UTF-16 code units and the JSON body to 64 KiB. Send one JSON object per request. Authentication, validation, quota, and provider failures return non-2xx statuses. A successful review decision uses HTTP 200.

Error bodies include error (a human-readable message), code (a machine-readable cause), and requestId. Validation failures can include details, while credit failures include requiredCredits and estimatedCost. Do not match error message text to decide whether to retry.

Response headerMeaning
X-Request-IdRequest identifier available on success and error responses.
X-RateLimit-LimitConfigured account-wide requests-per-minute limit.
X-RateLimit-RemainingRemaining requests in the current rate window, when available.
X-RateLimit-ResetRate-window reset as Unix seconds, when available.
Retry-AfterSeconds before retrying a rate-limited/unavailable request, when supplied.
X-FilterIt-CacheHIT or MISS for a successful request.
X-FilterIt-Credits-ConsumedCustomer credits charged for a successful request.
StatusMeaningAction
200Analysis completed, including review decisions.Apply your policy to decision and coverage.
400Malformed JSON or invalid request fields.Correct the request using validation details; do not retry unchanged.
401Missing, invalid, or revoked API key.Verify the server-held key and x-api-key header.
402Insufficient credits for thorough analysis.Inspect requiredCredits/estimatedCost; fund the account or explicitly choose fast.
413JSON body exceeds 64 KiB.Reduce the body; text also has a separate 4,000-code-unit limit.
415Unsupported Content-Type.Send Content-Type: application/json.
429Shared account rate limit or free daily quota reached.Respect Retry-After and reset information; creating another key does not increase the account limit.
503Provider disabled/unavailable, shared quota backend unavailable, or provider cap reached.Keep the request ID and respect any Retry-After. Do not treat the content as allowed.
500Unexpected application failure.Investigate with the request ID; avoid logging the payload or secret.
// Example authentication error shape (request ID is illustrative).
{
  "error": "Missing API key. Send the x-api-key header.",
  "code": "missing_api_key",
  "requestId": "example-request-id"
}

Rate/quota checks can count attempts even if a later step fails. Do not rely on retries being free. There is no supported idempotency key: retrying after an ambiguous timeout can duplicate a completed request and its paid charge. Use bounded retries with backoff only when your application accepts this behavior; authentication/validation errors need a corrected request.

Privacy, retention, and provider coverage

Request history records completed results and authenticated failures using metadata: request ID, HTTP status, sanitized error code, decision/category signals where available, character count, latency, provider identifiers, cache status, and credit usage. They do not retain submitted text, excerpts, model explanations, profanity matches, or filtered text. Only the owning account can view its usage and keys.

Classification caching is disabled by default. An operator can explicitly enable a retention TTL; that cache stores full results, which can include filtered text and excerpts. It is scoped by tenant, provider, model, and policy, and does not cache review decisions. Expiry stops reuse; database expiry alone does not guarantee physical deletion. Legacy records from older builds can contain payload-derived data and need a separate retention review.

TypeSafe supplies base category probabilities rather than exact offending spans or rationales. OpenAI is the currently supported optional advanced provider; disabled advanced analysis returns 503. Local development heuristics offer limited coverage and cannot automatically allow content.

Enabled providers receive the submitted text and selected category questions. Their own processing and retention terms apply; metadata-only FilterIt usage logs do not establish zero provider retention. Before submitting customer data, confirm provider activation, processing arrangements, and suitability for your privacy requirements. No universal accuracy, latency, multilingual coverage, or sensitive-data guarantee is made.

Debugging and integration checks

Open Usage and requests to review your account's request metadata. Use the request ID from the JSON body or response header to correlate a completed request or an authenticated error. Failure records include HTTP status and a sanitized error code, without retaining the payload or provider error body. Missing/invalid-key attempts cannot be associated with an account. If the database is unavailable, a failure record may also be unavailable; keep the client-side request ID, error code, and HTTP status without recording the payload.

SymptomCheck
401 after key rotationConfirm the active server secret, its whitespace/header name, and whether that key was revoked.
Review result for apparently clean textInspect provider, coverage, and reviewReasons. Development heuristics never establish an allow result.
Flag with no matches/findingsFast classification supplies flags, not spans. Empty matches do not negate a provider flag. Filtering requires verified thorough profanity findings.
Thorough returns 503The advanced provider must be activated and available. Failed analysis does not become a silently cheaper result.
No failure record in historyMissing/invalid API keys have no authenticated account; database failures can also prevent logging. Keep the response request ID, status, and sanitized error code.
Limits reached with a new keyKeys and playground share account budgets. Confirm account and deployment-wide provider limits.
Highlight offsets look wrongUse the original input and UTF-16 code units with an exclusive end; avoid normalization and byte/Python-character indexes.
Credit balance changed after a timeoutThe server may have completed the request. Inspect request metadata before resubmitting; idempotency is not supported.

Test your integration with synthetic clean, clearly flagged, quoted, obfuscated, ambiguous, and multiple-category text. Verify all three decision paths, 401/402/429/503 behavior, key revocation, and failure/refund handling. Compare false positives and false negatives against your own policy before relying on automation. Share request IDs and status codes for debugging; avoid sharing API keys or real customer text.