Confusables API
Two endpoints, JSON over HTTP, one field in the request. One returns the corrected text, the other returns what it would change and why. There is no SDK because there is not much for one to do.
Quick start
Send text, get text back
Nothing to install and no key to request. Paste this into a terminal.
curl -X POST https://api.confusables.io/v1/autocorrect \
-H "Content-Type: application/json" \
-d '{"text": "Its been a long day and your right."}'
{
"text": "It's been a long day and you're right.",
"applied": 2
}
Everything else on this page is detail on those two fields, the second endpoint, and what happens when something goes wrong.
Correct the text
Takes the text a person wrote and returns it with misused words replaced. Everything else is exactly what came in: no rewriting, no reordering, no synonyms, no tidying up.
| Request | text, a string. Required. |
|---|---|
| Returns | text, the corrected string, and applied, how many words changed. |
curl
curl -X POST https://api.confusables.io/v1/autocorrect \
-H "Content-Type: application/json" \
-d '{"text": "Its been a long day and your right."}'JavaScript
const res = await fetch("https://api.confusables.io/v1/autocorrect", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: "Its been a long day and your right." })
});
const { text, applied } = await res.json();
// text: "It's been a long day and you're right."
// applied: 2Python
import requests
response = requests.post(
"https://api.confusables.io/v1/autocorrect",
json={"text": "Its been a long day and your right."},
timeout=2,
)
response.raise_for_status()
corrected = response.json()["text"]Response:
{
"text": "It's been a long day and you're right.",
"applied": 2
}
applied is 0 and text is exactly what you sent. The engine favors preservation, but it can still change correct or ambiguous wording incorrectly.Inspect without changing
The same input, but it reports rather than rewrites. It suits showing a suggestion in your own interface, letting a person accept or reject each one, or auditing a correction you disagree with.
start, end | Offsets of the word in the string you sent, counted in UTF-16 code units. The note below covers callers outside JavaScript. |
|---|---|
original | The word as it was written. |
suggestion | The exact replacement applied in corrected, including apostrophe style. |
confidence | 1.0 structural, 0.95 strong cue, 0.90 heuristic. Nothing below the threshold is applied. |
reason | Which rule fired, in words. |
rulesApplied | A count per rule for the whole request. |
processingTimeMs | Server-side time for this request. |
curl
curl -X POST https://api.confusables.io/v1/check \
-H "Content-Type: application/json" \
-d '{"text": "Its been a long day and your right."}'JavaScript
const res = await fetch("https://api.confusables.io/v1/check", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: "Its been a long day and your right." })
});
const { issues } = await res.json();
for (const issue of issues) {
console.log(issue.start, issue.original, "->", issue.suggestion);
}Python
import requests
response = requests.post(
"https://api.confusables.io/v1/check",
json={"text": "Its been a long day and your right."},
timeout=2,
)
for issue in response.json()["issues"]:
print(issue["start"], issue["original"], "->", issue["suggestion"])Response:
{
"original": "Its been a long day and your right.",
"corrected": "It's been a long day and you're right.",
"rulesApplied": {
"Likely confusion between its/it's": 1,
"Likely confusion between your/you're": 1
},
"issues": [
{
"start": 0,
"end": 3,
"original": "Its",
"suggestion": "It's",
"confidence": 0.95,
"reason": "Likely confusion between its/it's"
},
{
"start": 24,
"end": 28,
"original": "your",
"suggestion": "you're",
"confidence": 0.95,
"reason": "Likely confusion between your/you're"
}
],
"processingTimeMs": 2
}
text.slice(start, end) uses this unit directly. Java and C# use the same unit with their own substring APIs. Python normally indexes Unicode code points, while Go and Rust string offsets use UTF-8 bytes; other languages also have their own indexing conventions.
# Python: convert once, slice in the same unit the API used
units = text.encode("utf-16-le")
word = units[issue["start"] * 2 : issue["end"] * 2].decode("utf-16-le")
Non-ASCII text can require conversion even without emoji: in é Your, Your starts at UTF-16 offset 2 but UTF-8 byte offset 3. Characters outside the Basic Multilingual Plane, including many emoji, occupy two UTF-16 units.What it will not touch
Recognized protected regions are skipped
The engine replaces one word with another and can do nothing else. On top of that, several kinds of text are left exactly as written even when they contain the very word being corrected somewhere else in the same sentence. Every line below is what the API returns today.
in Its been a long day at its-been.com
out It's been a long day at its-been.com the sentence corrects,
the domain does not
in Read more at https://example.com/its-been-great?your=1
out unchanged URLs
in Email me at [email protected]
out unchanged addresses
in #itsbeen trending with @your_friend
out unchanged hashtags and handles
in She said "lets go" and left.
out unchanged quoted speech
in if (x) { your_var = 1; }
out unchanged identifiers and code
in The colour was bright.
out unchanged British spellings
Protected text does not supply correction cues outside its span. Mixed-case identifiers are the exception: they remain unchanged but can supply context, so cOuLd of done it becomes cOuLd have done it. Text with more than 30 consecutive non-spacing combining marks is returned unchanged.
their family, they're family and your losing patience all come back unchanged, because each reads correctly under some interpretation. Other ambiguous wording can still receive a wrong suggestion; these examples are not a guarantee for every sentence.Errors
What comes back other than a 200
| 400 | The text field is missing, null or not a JSON string, or the body is not valid JSON. |
|---|---|
| 413 | The decoded text or the request body exceeds its limit. |
| 429 | Over the rate limit. Carries Retry-After in seconds. |
| 5xx | The service is having a problem. Treat it the way you would treat a timeout. |
{"text": ""}, is valid and returns unchanged. Applying the returned issue suggestions at their original UTF-16 offsets reproduces corrected, including apostrophe styling.For allowed browser origins, error responses carry CORS headers and Retry-After is exposed to JavaScript. Preflight requests do not require a key.
A 429 says how long to wait, in the header and again in the body, so a client backing off does not have to guess:
HTTP/1.1 429
Retry-After: 42
{"error": "Too many requests. Limit is 60 requests per minute.",
"retryAfterSeconds": 42}
Limits and the demo host
What api.confusables.io is
It is the host behind the demo on the home page, left open so anyone can try the engine without asking us for anything first. It is not sized to sit in front of your traffic and it carries no availability promise.
| Maximum input | 50,000 UTF-16 code units, roughly 8,000 words of English |
|---|---|
| Maximum request body | 304,096 bytes by default, checked before JSON decoding |
| Rate limit | 60 requests per minute per caller |
| Authentication | Optional. A key raises the limit and meters it to you |
The cap counts UTF-16 code units rather than bytes, so scripts whose characters cost several bytes each, Cyrillic and Greek and Arabic and Hebrew and the common CJK ranges among them, get the full allowance. Characters outside the Basic Multilingual Plane cost two units rather than one, so a body made entirely of emoji reaches the cap at 25,000 of them. The text and byte limits are configurable when self-hosted. The default body limit allows six bytes per UTF-16 unit plus 4,096 bytes for JSON overhead.
Sending a key
You do not need one, and the endpoint stays open without one. What a key changes is who you are to the rate limiter: without it a caller is an IP address, so an integration behind a shared egress shares a single 60/minute allowance with everything else on that address. With one, your traffic is metered as you, with an allowance sized for a real integration.
curl -X POST https://api.confusables.io/v1/autocorrect \
-H "Content-Type: application/json" \
-H "X-API-Key: $CONFUSABLES_KEY" \
-d '{"text": "Its been a long day and your not wrong."}'
Authorization: Bearer $CONFUSABLES_KEY works identically if that fits your client better. A key that is not recognized is 401 rather than a silent fall back to anonymous, so a typo surfaces immediately instead of as unexplained 429s a month later.
Keys are arranged by email at [email protected] rather than issued automatically. There is no signup form and no dashboard yet, which is honest about the stage rather than a feature.
Running it yourself
A container, and nothing else
It is a JVM service on Java 21 that ships as a standard container image and runs in 512 MB on a single core. No GPU, no database, no external service and no outbound network calls at runtime, so it works with egress blocked entirely.
- The same two endpoints, on your own host and your own limits.
- Every rule can be switched on or off through configuration.
- Three of the twelve ship switched off. The demo runs all twelve.
- Nothing your users write leaves your network.
The FAQ covers what it corrects, how it decides, and what it deliberately refuses to touch.
Read the FAQUsing the public service
What it is, and what it does not promise
The public endpoint is for demonstration and evaluation. It has no service level agreement and no availability commitment, it is rate limited as described above, and it can change or be withdrawn. That is a boundary rather than a warning: it runs, and building something that depends on it staying up is a decision it cannot support. For real traffic, the container in your own network is the answer rather than this endpoint, and it is available to discuss for evaluation: talk to us.
The corrections are not guaranteed
The engine can make a wrong correction, miss a real error, or leave text unchanged. Its output is a suggestion produced by software and you remain responsible for what you publish. How often it is wrong is measured and published rather than glossed: see Accuracy and evidence.
How wrong depends heavily on the kind of text. A September 11, 2026 evaluation changed 273 of 50,000 sentences of informal comments. Of 100 reviewed changes, 60 were judged correct and 40 damaging. A September 19, 2026 pilot on an earlier build using 1,000 sentences changed seven sentences: four useful corrections and three damaging changes. Both evaluations used earlier builds and do not measure the latest production version. The pilot was developer-reviewed and is too small for a general accuracy estimate. We do not currently recommend it for unattended correction of informal conversational text. If that is the text you would be sending it, /v1/check and a person accepting each suggestion is the integration to build, and the figures behind this are in the accuracy section.
Please don't send it sensitive material
Confidential, personal, regulated, credential or security-sensitive material does not belong in a free public endpoint, whatever the engine does with it. Privacy and Data Handling sets out what happens to what you do send. If your text cannot leave your infrastructure, that is what the self-hosted image is for.
Keeping it open depends on a short list
- Don't work around the rate limits, by rotating addresses, forging caller identity headers, or any other route.
- Don't deliberately overload or disrupt the service.
- Don't use it for unlawful activity.
Access to the public endpoint can be limited or revoked for abuse. Security testing is a different thing and is welcome: put “security” in the subject line at [email protected].