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 URL | https://api.metishalo.com |
|---|---|
| Format | JSON request and response bodies (UTF-8). File scans use multipart/form-data. |
| Authentication | X-Api-Key header, one key per account |
| Rate limit | 60 requests per minute per key (HTTP 429 when exceeded) |
| Interactive reference | https://api.metishalo.com/swagger |
| Use in your products | Permitted 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.
| Scan | Endpoint | Cost per call | Requires service |
|---|---|---|---|
| Text | POST /api/scan/text | 1 credit | Text & Form |
| Web form | POST /api/scan/text + X-Scan-Context: form | 1 credit | Text & Form |
| File | POST /api/scan/file | 1 credit | File |
| Database | POST /api/scan/db | 3 credits | Database |
| Excel add-in | POST /api/scan/text + X-Scan-Context: excel | 1 credit per 1,000 scans | Microsoft Excel |
| Word add-in | POST /api/scan/text + X-Scan-Context: word | 1 credit per 1,000 scans | Microsoft Word |
| PowerPoint add-in | POST /api/scan/text + X-Scan-Context: powerpoint | 1 credit per 1,000 scans | Microsoft PowerPoint |
| FormHoney | POST /api/scan/text + X-Scan-Context: formhoney | 1 credit per 1,000 scans | FormHoney |
| Reports | GET /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.
| Field | Type | Description |
|---|---|---|
file | file | The file to scan (.txt, .csv; up to 50 MB) |
clean | bool | Produce a cleaned copy (downloadable as CSV via the report) |
delimiter | string | Optional. 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
| Status | Body error | Meaning |
|---|---|---|
| 400 | — | Missing or invalid input (e.g. no text) |
| 401 | invalid_api_key | Header missing, key unknown, or account disabled |
| 402 | insufficient_credits | Not enough credits for this scan; body includes credits, cost and topUpUrl |
| 403 | service_not_enabled | The service is not enabled on your account |
| 403 | origin_not_allowed | Browser request from a site not in your allowed origins |
| 403 | ip_not_allowed | Request 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 |
| 503 | validation_unavailable | The 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/24or2001:db8:abcd::/48. Requests from any other address are refused with 403ip_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 403origin_not_allowed. Server-to-server calls carry noOriginheader 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.