Metishalo Profanity API

One HTTPS endpoint family for detecting and cleaning profanity in text, web forms, files and databases. Any language or platform that can make an HTTP request can use it.

Overview

Base URLhttps://api.metishalo.com
FormatJSON request and response bodies (UTF-8). File scans use multipart/form-data.
AuthenticationX-Api-Key header, one key per account
Rate limit60 requests per minute per key (HTTP 429 when exceeded)
Interactive referencehttps://api.metishalo.com/swagger
Use in your productsPermitted inside any application that does not compete with the METIS HALO service, for example website forms, CMS and e-commerce plug-ins, chat, review and support tools, or import pipelines. Reselling profanity detection as a service on top of this API is not permitted.

Authentication

Every request must carry your API key in the X-Api-Key header. Your key is shown under My Account once you have an account (Registration is not yet open; ask to be told when it is). Treat it like a password: anyone holding it can spend your credits.

X-Api-Key: YOUR_API_KEY

The engine checks each key with this website and caches the answer for about a minute, so a key that is regenerated or disabled stops working within that window.

Credits & services

Scans are paid for with credits bought on the Pricing page. A credit is deducted only when a scan completes; failed requests (invalid input, rate limit, engine error) are not charged.

ScanEndpointCost per callRequires service
TextPOST /api/scan/text1 creditText & Form
Web formPOST /api/scan/text + X-Scan-Context: form1 creditText & Form
FilePOST /api/scan/file1 creditFile
DatabasePOST /api/scan/db3 creditsDatabase
Excel add-inPOST /api/scan/text + X-Scan-Context: excel1 credit per 1,000 scansMicrosoft Excel
Word add-inPOST /api/scan/text + X-Scan-Context: word1 credit per 1,000 scansMicrosoft Word
PowerPoint add-inPOST /api/scan/text + X-Scan-Context: powerpoint1 credit per 1,000 scansMicrosoft PowerPoint
FormHoneyPOST /api/scan/text + X-Scan-Context: formhoney1 credit per 1,000 scansFormHoney
ReportsGET /api/reports/…free—

Services are chosen when you register and can be changed under My Account. Calling a service that is not enabled on your account returns HTTP 403 service_not_enabled. Services priced per 1,000 scans count every call and deduct the credit when the counter completes a thousand; the account only needs a positive balance to keep scanning.

POST/api/scan/text

Scans a string and returns the matches, categories and a decision. Set clean to true to also receive a redacted copy.

Request body
{
  "text": "Text to scan",
  "clean": false,                 // return cleanedText with matches masked
  "maxEditDistance": 1,           // optional, 0–3, typo tolerance
  "enableFuzzyMatch": true,       // optional overrides, default true
  "enableReverseMatch": true,
  "enableObfuscationMatch": true,
  "enableMaskedMatch": true,
  "enableLeetMatch": true,
  "enableStretchMatch": true,
  "enableSkeletonMatch": true,
  "enableHomoglyphMatch": true
}
Response (200 OK, or 403 when the decision is Block)
{
  "hasProfanity": true,
  "categories": "SwearWord",
  "categoriesList": ["SwearWord"],
  "decision": "Warn",             // Allow | Warn | Redact | Block
  "matches": ["sh1t"],
  "details": [ { "term": "sh1t", "categories": "SwearWord", "matchType": "Leet", "index": 12 } ],
  "originalText": "…",
  "cleanedText": "…",             // only when clean = true
  "cleanedCount": 1,
  "timeTakenMs": 0.42
}

A Block decision is returned with HTTP 403 and the same JSON body, so a client can treat the status code alone as pass/fail. The scan still completes and is charged.

POST/api/scan/text for web forms and Office

The text endpoint accepts an optional X-Scan-Context header that tells the engine what kind of client is scanning, so the call is logged and priced accordingly. Recognised values: form (web-form validation), excel, word, powerpoint (the Office add-ins send these automatically) and formhoney (sent by the FormHoney platform). Anything else, or no header, is a plain text scan.

X-Api-Key: YOUR_API_KEY
X-Scan-Context: form

Each call costs 1 credit. To validate a whole form for one credit, join the field values with newlines and send them as a single text; use the index values in details to work out which field a match came from. Scanning field-by-field gives simpler highlighting but costs one credit per field.

POST/api/scan/file

Uploads a text or CSV file as multipart/form-data.

FieldTypeDescription
filefileThe file to scan (.txt, .csv; up to 50 MB)
cleanboolProduce a cleaned copy (downloadable as CSV via the report)
delimiterstringOptional. When set (e.g. ,) every field of every row is scanned separately.
{
  "jobId": "3f1c…",
  "report": { "file": "customers.csv", "mode": "delimited", "rowsProcessed": 50, "fieldsProcessed": 200, "removals": 3 },
  "cleanedFileAvailable": true
}

Word and Excel documents are scanned by the Office add-ins, which extract the text and use the text endpoint.

POST/api/scan/db

Scans nominated columns of a database. Costs 3 credits per call and requires the Database service.

{
  "provider": "MYSQL",            // MSSQL | MYSQL | SQLITE
  "connectionString": "Server=…;Database=…;Uid=…;Pwd=…;",
  "tables": ["comments", "reviews"],
  "columns": { "comments": ["body"], "reviews": ["title", "body"] },
  "clean": false
}

The engine connects to the database from api.metishalo.com; the database must accept connections from that host. Use a read-only account unless clean is true.

GET/api/reports/{jobId}.json  ·  GET/api/reports/{jobId}.csv

Fetches the stored report for a file scan, or the cleaned output as CSV when clean was requested. Reports are free to fetch.

Errors

StatusBody errorMeaning
400—Missing or invalid input (e.g. no text)
401invalid_api_keyHeader missing, key unknown, or account disabled
402insufficient_creditsNot enough credits for this scan; body includes credits, cost and topUpUrl
403service_not_enabledThe service is not enabled on your account
403origin_not_allowedBrowser request from a site not in your allowed origins
403ip_not_allowedRequest from an IP address outside your allowed IP ranges
403(scan result)Scan completed with a Block decision
429—Rate limit exceeded; retry after a short pause
503validation_unavailableThe engine could not reach the account service to check a key it has not seen recently; retry
{ "error": "insufficient_credits", "message": "This scan costs 3 credits; you have 1.", "credits": 1, "cost": 3, "topUpUrl": "https://www.metishalo.com/index.php?page=pricing" }

Locking a key to your company

Because a key may end up in a web page, a spreadsheet or a config file, every account can restrict where its key works. Both settings are under My Account › Services & Key Restrictions, and administrators can set them for any account. Changes reach the engine within about a minute.

  • Allowed IP addresses / ranges (IPv4 and IPv6). List your servers or office networks as single addresses or CIDR ranges, for example 203.0.113.10, 198.51.100.0/24 or 2001:db8:abcd::/48. Requests from any other address are refused with 403 ip_not_allowed. This is the right control for server-side integrations and the Office add-ins.
  • Allowed website origins. For browser-side calls, list the sites permitted to use the key (for example https://www.example.com). Requests from any other origin are refused with 403 origin_not_allowed. Server-to-server calls carry no Origin header and are unaffected by this setting.

The two can be combined: for a web form, allow your website's origin and, if your visitors come from a known network, their IP range. Scans run on this website while logged in are never restricted, because the user has already authenticated with a password. Transport is always TLS-encrypted, so the restrictions are about where a key may be used, not about eavesdropping.

Code examples

curl
curl -X POST https://api.metishalo.com/api/scan/text \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -d '{"text":"hello world","clean":true}'
JavaScript (browser or Node)
async function scanText(text) {
  const res = await fetch('https://api.metishalo.com/api/scan/text', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-Api-Key': 'YOUR_API_KEY', 'X-Scan-Context': 'form' },
    body: JSON.stringify({ text, clean: false })
  });
  const data = await res.json();
  if (res.status === 402) throw new Error('Out of credits: ' + data.message);
  if (!res.ok && data.decision !== 'Block') throw new Error(data.error || res.statusText);
  return data;   // data.hasProfanity, data.matches, data.decision
}
PHP
$ch = curl_init('https://api.metishalo.com/api/scan/text');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-Api-Key: YOUR_API_KEY'],
  CURLOPT_POSTFIELDS => json_encode(['text' => $text, 'clean' => true]),
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status === 402) { /* out of credits */ }
if (!empty($result['hasProfanity'])) { /* reject or use $result['cleanedText'] */ }
C# (.NET)
using var http = new HttpClient { BaseAddress = new Uri("https://api.metishalo.com") };
http.DefaultRequestHeaders.Add("X-Api-Key", "YOUR_API_KEY");
var response = await http.PostAsJsonAsync("/api/scan/text", new { text = "hello world", clean = true });
if (response.StatusCode == HttpStatusCode.PaymentRequired) { /* out of credits */ }
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
bool hasProfanity = result.GetProperty("hasProfanity").GetBoolean();
PowerShell
Invoke-RestMethod -Method Post -Uri "https://api.metishalo.com/api/scan/text" `
  -ContentType "application/json" `
  -Headers @{ "X-Api-Key" = "YOUR_API_KEY" } `
  -Body '{"text":"hello world","clean":false}'
Python
import requests
r = requests.post("https://api.metishalo.com/api/scan/text",
                  headers={"X-Api-Key": "YOUR_API_KEY"},
                  json={"text": "hello world", "clean": True})
if r.status_code == 402:
    raise SystemExit("Out of credits")
print(r.json()["hasProfanity"], r.json().get("cleanedText"))
VBA (Excel / Word)
Dim http As Object: Set http = CreateObject("MSXML2.XMLHTTP")
http.Open "POST", "https://api.metishalo.com/api/scan/text", False
http.setRequestHeader "Content-Type", "application/json"
http.setRequestHeader "X-Api-Key", "YOUR_API_KEY"
http.send "{""text"":""" & Replace(cellText, """", "\""") & """,""clean"":false}"
If http.Status = 402 Then MsgBox "Out of credits" Else Debug.Print http.responseText
File scan (curl)
curl -X POST https://api.metishalo.com/api/scan/file \
  -H "X-Api-Key: YOUR_API_KEY" \
  -F "file=@customers.csv" -F "clean=true" -F "delimiter=,"

A complete browser example, a Bootstrap customer form that validates every field before submitting, is available at examples/demo-web-form.html (view its source and replace the demo key with your own).

Office add-ins & desktop scanner

The Metis Halo add-ins for Excel, Word and PowerPoint and the desktop scanner call this same API. Open the add-in's Settings, paste your API key from My Account into API Key, and leave the API base as https://api.metishalo.com. Each scan they run is deducted from your credits exactly like a direct API call.