Confusables logo

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

01

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

02
POST/v1/autocorrect

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.

Requesttext, a string. Required.
Returnstext, 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: 2

Python

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
}
An unchanged response is the normal case. When no correction is selected, 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

03
POST/v1/check

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, endOffsets of the word in the string you sent, counted in UTF-16 code units. The note below covers callers outside JavaScript.
originalThe word as it was written.
suggestionThe exact replacement applied in corrected, including apostrophe style.
confidence1.0 structural, 0.95 strong cue, 0.90 heuristic. Nothing below the threshold is applied.
reasonWhich rule fired, in words.
rulesAppliedA count per rule for the whole request.
processingTimeMsServer-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
}
Offsets are into the string you sent, so you can highlight in place without looking for the word again. The same input returns the same corrections with the same engine version and configuration; timing can vary.
The unit is UTF-16 code units. JavaScript 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

04

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
Recognized quotations are protected. Detection has limits: a plural possessive apostrophe inside a single-quoted passage can be mistaken for its closing quote, leaving later words unprotected.

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.

Many recognized ambiguities are skipped. 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

05

What comes back other than a 200

400The text field is missing, null or not a JSON string, or the body is not valid JSON.
413The decoded text or the request body exceeds its limit.
429Over the rate limit. Carries Retry-After in seconds.
5xxThe service is having a problem. Treat it the way you would treat a timeout.
Text must be a JSON string. Numbers, booleans, arrays and objects receive HTTP 400. An empty string, {"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

06

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 input50,000 UTF-16 code units, roughly 8,000 words of English
Maximum request body304,096 bytes by default, checked before JSON decoding
Rate limit60 requests per minute per caller
AuthenticationOptional. 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.

For anything with real traffic behind it, the container inside your own perimeter is the right shape, and it is available to discuss for evaluation: write to us. Then the limits, the authentication and the uptime are yours, and your users' text never leaves your network.

If it is unavailable

07

Fail open, and let the original text through

Confusables has no correction queue or automatic retry and does not persist submitted text by default. Cached word lists and in-memory rate-limit windows are operational state. The calling application decides what happens on a timeout. In a submit path there is one sensible answer: pass the text through exactly as the person wrote it.

let body = original;
try {
  const res = await fetch(url, { ...opts, signal: AbortSignal.timeout(500) });
  if (res.ok) {
    body = (await res.json()).text;
  }
} catch {
  // Correcting is an improvement, not a gate. A comment that fails to
  // post is a worse outcome than a comment with "its" in it.
}
publish(body);
A timeout bounds the integration's wait. Engine timings exclude network and hosting delays and are not a response-time guarantee.

Running it yourself

08

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 self-hosted image is available to discuss for evaluation. A note to [email protected] saying what you want to find out is enough to start.

The FAQ covers what it corrects, how it decides, and what it deliberately refuses to touch.

Read the FAQ

Using the public service

09

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].