On this page
Getting started
Overview
The version is part of the path: /api/v1. Every endpoint accepts a POST with a JSON body and responds in JSON, including errors.
Base URL https://textlume.com
- POST /api/v1/detectCheck one text
- POST /api/v1/detect/batchCheck several texts
- POST /api/v1/humanizeHumanize a text
- POST /api/v1/paraphraseParaphrase a text
- POST /api/v1/summarizeSummarize a text
- POST /api/v1/grammarCheck grammar
Available on the site but not through the API: the word counter, the character counter, the plagiarism checker and the humanizer's "Take more time" option.
Generated from the OpenAPI document, so it shows exactly the routes and fields the API accepts. OpenAPI document
Your first request
The minimum text length is 50 words.
Check "calibrated" first. If it is false, there is no fitted curve for that language yet, and the figure must not be treated as a result.
Values are shown as their types. A real response contains the reading for your text.
curl -X POST https://textlume.com/api/v1/detect \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks."}'const res = await fetch('https://textlume.com/api/v1/detect', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
"text": "On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks."
}),
})
const data = await res.json()
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`)import os
import requests
res = requests.post(
"https://textlume.com/api/v1/detect",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={
"text": "On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks.",
},
timeout=90,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}"){
"figure": "number from 0 to 100",
"low": "number from 0 to 100",
"high": "number from 0 to 100",
"uncertainty": "number",
"verdict": "human | uncertain | ai",
"confidence": "low | medium | high",
"sentences": [
{
"text": "string",
"score": "number from 0 to 1",
"verdict": "human | uncertain | ai"
}
],
"words": "integer",
"language": "string",
"model": "string",
"calibrated": "boolean",
"bandHumanFlagRate": "number | null",
"usage": {
"recorded": "boolean",
"words": "integer",
"source": "plan | balance | plan_and_balance",
"fromPlan": "integer",
"fromBalance": "integer",
"allowanceLeft": "integer | null",
"balance": "integer"
}
}Authentication
Pass your key as a bearer token in the Authorization header. Requests with the key in the URL are rejected.
Each key has scopes: detect, rewrite, summarize and grammar. If a key lacks the scope a route needs, the route returns 403 out_of_scope.
Authorization: Bearer YOUR_API_KEYEndpoints
POST /api/v1/detect
Check one text. Returns the detector reading for a single text: a figure from 0 to 100, the range around it, a reading for each sentence and whether the language is calibrated.
Required scope: detect
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| text | string | yes | The text to check or rewrite. |
| language | string | no | A language code such as "en" or "de". If omitted, the language is identified from the text. |
| contentType | string | no | The kind of text, such as "essay" or "email". Set it when the reading should use the curve for that kind of text. |
Response
| Field | Type | Description |
|---|---|---|
| figure | number | The reading on a scale from 0 to 100, showing how likely it is that the text was machine-written. Cards and reports on the site show the midpoint between low and high, which can differ from this value. |
| low | number | The bottom of the range around the figure. |
| high | number | The top of the range around the figure. |
| uncertainty | number | Half the width of the range, in points. |
| verdict | string | The band that contains the figure. It describes the figure only and makes no statement about who wrote the text. Allowed values: human, uncertain, ai. |
| confidence | string | How far the reading can be trusted. Allowed values: low, medium, high. |
| sentences | array of SentenceScore | A reading for each sentence. |
| words | integer | The number of words read. |
| language | string | The language the text was read in. |
| model | string | The model that produced this reading. |
| calibrated | boolean | Whether a fitted curve exists for this language. If false, do not use the figure as a result. |
| bandHumanFlagRate | number | null | In our measurements, the share of human-written texts in this band that were read as machine-written. Null when not available. |
| usage | Usage | The words this call counted. |
Errors specific to this route
| Status | Code | Meaning |
|---|---|---|
| 400 | no_text | The request body does not contain "text". |
| 422 | too_short | The text is shorter than the minimum length. |
| 413 | text_too_long | The text is longer than the engine can read in a single call. Split it into smaller parts. No words were counted. |
| 422 | not_calibrated | Readings are not available for this language yet. |
| 503 | engine_unavailable | The engine did not respond. No words were counted. |
| 502 | engine_unavailable | The engine did not respond. No words were counted. |
| 503 | engine_starting | The engine was still starting and was not ready in time. No words were counted. Try again in about a minute. |
| 503 | engine_busy | The engine was busy and could not start the reading in time. Pro users on the site are served ahead of API calls. No words were counted. Try again shortly. |
| 504 | engine_timeout | The engine could not finish the text in time. No words were counted. Try again, or send a shorter text. |
| 429 | concurrency_limited | This key already has the maximum number of calls of this type in flight. Wait until one finishes, or for the time given in Retry-After. |
curl -X POST https://textlume.com/api/v1/detect \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks."}'const res = await fetch('https://textlume.com/api/v1/detect', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
"text": "On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks."
}),
})
const data = await res.json()
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`)import os
import requests
res = requests.post(
"https://textlume.com/api/v1/detect",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={
"text": "On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks.",
},
timeout=90,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}"){
"figure": "number from 0 to 100",
"low": "number from 0 to 100",
"high": "number from 0 to 100",
"uncertainty": "number",
"verdict": "human | uncertain | ai",
"confidence": "low | medium | high",
"sentences": [
{
"text": "string",
"score": "number from 0 to 1",
"verdict": "human | uncertain | ai"
}
],
"words": "integer",
"language": "string",
"model": "string",
"calibrated": "boolean",
"bandHumanFlagRate": "number | null",
"usage": {
"recorded": "boolean",
"words": "integer",
"source": "plan | balance | plan_and_balance",
"fromPlan": "integer",
"fromBalance": "integer",
"allowanceLeft": "integer | null",
"balance": "integer"
}
}POST /api/v1/detect/batch
Check several texts. Send as many texts as the batch limit allows in a single call. Before it runs, the call must have enough words available for every text it could read (50 words or more per text). Words reserved for a text that returns no reading are refunded afterwards. A failure on one text does not fail the rest of the batch.
Required scope: detect
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| items | array of object | yes | The texts to check. Each item has its own "text" and may include an "id", a "language" and a "contentType". |
| items[].id | string | no | An id of your choice, returned with the item's result. If omitted, the item's position in the list is used. |
| items[].text | string | yes | The text to check or rewrite. |
| items[].language | string | no | A language code such as "en" or "de". If omitted, the language is identified from the text. |
| items[].contentType | string | no | The kind of text, such as "essay" or "email". Set it when the reading should use the curve for that kind of text. |
Response
| Field | Type | Description |
|---|---|---|
| results | array of BatchResult | One result for each item, in the order the items were sent. |
| usage | Usage | The words this call counted. |
Errors specific to this route
| Status | Code | Meaning |
|---|---|---|
| 400 | batch_empty | The request body has no "items", or the list is empty. |
| 413 | batch_too_large | The batch contains more items than the limit allows. |
| 429 | concurrency_limited | This key already has the maximum number of calls of this type in flight. Wait until one finishes, or for the time given in Retry-After. |
curl -X POST https://textlume.com/api/v1/detect/batch \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"items":[{"id":"1","text":"On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks."},{"id":"2","text":"On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks."}]}'const res = await fetch('https://textlume.com/api/v1/detect/batch', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
"items": [
{
"id": "1",
"text": "On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks."
},
{
"id": "2",
"text": "On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks."
}
]
}),
})
const data = await res.json()
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`)import os
import requests
res = requests.post(
"https://textlume.com/api/v1/detect/batch",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={
"items": [
{
"id": "1",
"text": "On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks.",
},
{
"id": "2",
"text": "On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks.",
},
],
},
timeout=90,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}"){
"results": [
{
"id": "string",
"detection": {
"figure": "number from 0 to 100",
"low": "number from 0 to 100",
"high": "number from 0 to 100",
"uncertainty": "number",
"verdict": "human | uncertain | ai",
"confidence": "low | medium | high",
"sentences": [
{
"text": "string",
"score": "number from 0 to 1",
"verdict": "human | uncertain | ai"
}
],
"words": "integer",
"language": "string",
"model": "string",
"calibrated": "boolean",
"bandHumanFlagRate": "number | null"
},
"error": {
"code": "string",
"message": "string"
}
}
],
"usage": {
"recorded": "boolean",
"words": "integer",
"source": "plan | balance | plan_and_balance",
"fromPlan": "integer",
"fromBalance": "integer",
"allowanceLeft": "integer | null",
"balance": "integer"
}
}POST /api/v1/humanize
Humanize a text. Runs the humanizer with its fact checks. Numbers, names and quotations are preserved, and if a rewrite changes a fact, it is repaired or flagged. The "custom" mode (Pro) writes in your own style, learned from a sample of your writing.
The API runs the humanizer's standard rewrite. The "Take more time" option exists only on the site.
Required scope: rewrite
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| text | string | yes | The text to check or rewrite. |
| mode | string | no | The rewrite mode. Allowed values: natural, formal, simple, academic, creative, shorten, expand, custom. Defaults to natural. |
| style_sample | string | no | Used with the "custom" mode: a sample of your own writing between 150 and 2,000 words, processed on the server. If omitted, the sample saved to your account is used. |
| tab | string | no | The humanizer tab sets the tone the rewrite is written in. Standard keeps the tone of the original text. Allowed values: standard, academic, professional, casual. Defaults to standard. |
| strength | string | no | Regular keeps the content and tone. Aggressive may reorder sentences and drop extra detail while keeping every main point, so some parts may read a little unusual. Allowed values: regular, aggressive. Defaults to regular. |
| ownTouch | string | no | A detail, example or opinion in your own words, up to 400 characters, that gets worked into the rewrite. |
| language | string | no | A language code such as "en" or "de". If omitted, the language is identified from the text. |
Response
| Field | Type | Description |
|---|---|---|
| text | string | The rewritten text. |
| before | Detection | The detector reading of the text you sent, or null if there is none. |
| after | Detection | The detector reading of the rewritten text, or null. |
| passes | integer | The number of rewrites produced before this one was selected. |
| facts | object | The fact check findings, including the reason when a rewrite is not clean. |
| late | boolean | True when the rewrite was returned at its time limit before its checks had finished. |
| usage | Usage | The words this call counted. |
Errors specific to this route
| Status | Code | Meaning |
|---|---|---|
| 400 | no_text | The request body does not contain "text". |
| 422 | too_short | The text is shorter than the minimum length. |
| 400 | unknown_mode | This route does not offer the requested mode. |
| 400 | unknown_option | This route doesn't offer that tab or strength. |
| 402 | plan_required | The mode, tab, strength or personal touch you chose needs a plan this account does not have. The "custom" mode requires Pro. No words were counted. |
| 400 | no_sample | The "custom" mode needs a writing sample, either sent with the request or saved to the account. |
| 400 | sample_too_short | The writing sample is shorter than the minimum length. |
| 400 | sample_too_long | The writing sample is longer than the maximum length. |
| 429 | concurrency_limited | This key already has the maximum number of calls of this type in flight. Wait until one finishes, or for the time given in Retry-After. |
| 422 | no_rewrite | The engine responded, but none of its drafts passed the checks (empty output, a refusal or a copy of the input). No words were counted. |
| 503 | timeout | The rewrite ran too long and was stopped. No words were counted. |
| 503 | engine_unavailable | The engine did not respond. No words were counted. |
| 502 | engine_unavailable | The engine did not respond. No words were counted. |
curl -X POST https://textlume.com/api/v1/humanize \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks."}'const res = await fetch('https://textlume.com/api/v1/humanize', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
"text": "On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks."
}),
})
const data = await res.json()
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`)import os
import requests
res = requests.post(
"https://textlume.com/api/v1/humanize",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={
"text": "On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks.",
},
timeout=90,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}"){
"text": "string",
"before": {
"figure": "number from 0 to 100",
"low": "number from 0 to 100",
"high": "number from 0 to 100",
"uncertainty": "number",
"verdict": "human | uncertain | ai",
"confidence": "low | medium | high",
"sentences": [
{
"text": "string",
"score": "number from 0 to 1",
"verdict": "human | uncertain | ai"
}
],
"words": "integer",
"language": "string",
"model": "string",
"calibrated": "boolean",
"bandHumanFlagRate": "number | null"
},
"after": {
"figure": "number from 0 to 100",
"low": "number from 0 to 100",
"high": "number from 0 to 100",
"uncertainty": "number",
"verdict": "human | uncertain | ai",
"confidence": "low | medium | high",
"sentences": [
{
"text": "string",
"score": "number from 0 to 1",
"verdict": "human | uncertain | ai"
}
],
"words": "integer",
"language": "string",
"model": "string",
"calibrated": "boolean",
"bandHumanFlagRate": "number | null"
},
"passes": "integer",
"facts": "object",
"late": "boolean",
"usage": {
"recorded": "boolean",
"words": "integer",
"source": "plan | balance | plan_and_balance",
"fromPlan": "integer",
"fromBalance": "integer",
"allowanceLeft": "integer | null",
"balance": "integer"
}
}POST /api/v1/paraphrase
Paraphrase a text. Runs the paraphraser, with the same modes as the paraphrasing tool on the site.
Required scope: rewrite
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| text | string | yes | The text to check or rewrite. |
| mode | string | no | The rewrite mode. Allowed values: standard, fluency, formal, academic, simple, creative, expand, shorten. Defaults to standard. |
| strength | string | no | How far the rewrite may depart from the original. Allowed values: readable, balanced, strong. Defaults to balanced. |
| readability | string | no | The reading level to write for. Allowed values: casual, professional, academic, creative. Defaults to professional. |
| purpose | string | no | The purpose of the text. Allowed values: general, essay, article, email, marketing, report, story. Defaults to general. |
| synonyms | string | no | How freely words may be swapped for synonyms. Allowed values: few, some, many. Defaults to some. |
| freezeWords | array of string | no | Words and phrases that must be returned exactly as written. Up to 25 entries, each no longer than 60 characters. Their words count toward the call's word total. |
| customInstruction | string | no | Your own short instruction for the rewrite, no longer than 500 characters. Its words count toward the call's word total. |
| language | string | no | A language code such as "en" or "de". If omitted, the language is identified from the text. |
| closedLoop | boolean | no | Set to false to turn off the closed loop. Defaults to true. |
Response
| Field | Type | Description |
|---|---|---|
| text | string | The rewritten text. |
| before | Detection | The detector reading of the text you sent, or null if there is none. |
| after | Detection | The detector reading of the rewritten text, or null. |
| passes | integer | The number of rewrites produced before this one was selected. |
| facts | object | The fact check findings, including the reason when a rewrite is not clean. |
| late | boolean | True when the rewrite was returned at its time limit before its checks had finished. |
| usage | Usage | The words this call counted. |
Errors specific to this route
| Status | Code | Meaning |
|---|---|---|
| 400 | no_text | The request body does not contain "text". |
| 422 | too_short | The text is shorter than the minimum length. |
| 400 | unknown_mode | This route does not offer the requested mode. |
| 400 | option_invalid | freezeWords must be a list of words or phrases. Nothing was counted. |
| 400 | option_too_long | freezeWords or customInstruction is over its length limit. Nothing was counted. |
| 429 | concurrency_limited | This key already has the maximum number of calls of this type in flight. Wait until one finishes, or for the time given in Retry-After. |
| 422 | no_rewrite | The engine responded, but none of its drafts passed the checks (empty output, a refusal or a copy of the input). No words were counted. |
| 503 | timeout | The rewrite ran too long and was stopped. No words were counted. |
| 503 | engine_unavailable | The engine did not respond. No words were counted. |
| 502 | engine_unavailable | The engine did not respond. No words were counted. |
curl -X POST https://textlume.com/api/v1/paraphrase \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks."}'const res = await fetch('https://textlume.com/api/v1/paraphrase', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
"text": "On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks."
}),
})
const data = await res.json()
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`)import os
import requests
res = requests.post(
"https://textlume.com/api/v1/paraphrase",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={
"text": "On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks.",
},
timeout=90,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}"){
"text": "string",
"before": {
"figure": "number from 0 to 100",
"low": "number from 0 to 100",
"high": "number from 0 to 100",
"uncertainty": "number",
"verdict": "human | uncertain | ai",
"confidence": "low | medium | high",
"sentences": [
{
"text": "string",
"score": "number from 0 to 1",
"verdict": "human | uncertain | ai"
}
],
"words": "integer",
"language": "string",
"model": "string",
"calibrated": "boolean",
"bandHumanFlagRate": "number | null"
},
"after": {
"figure": "number from 0 to 100",
"low": "number from 0 to 100",
"high": "number from 0 to 100",
"uncertainty": "number",
"verdict": "human | uncertain | ai",
"confidence": "low | medium | high",
"sentences": [
{
"text": "string",
"score": "number from 0 to 1",
"verdict": "human | uncertain | ai"
}
],
"words": "integer",
"language": "string",
"model": "string",
"calibrated": "boolean",
"bandHumanFlagRate": "number | null"
},
"passes": "integer",
"facts": "object",
"late": "boolean",
"usage": {
"recorded": "boolean",
"words": "integer",
"source": "plan | balance | plan_and_balance",
"fromPlan": "integer",
"fromBalance": "integer",
"allowanceLeft": "integer | null",
"balance": "integer"
}
}POST /api/v1/summarize
Summarize a text. Runs the summarizer and returns key sentences, a paragraph or bullet points.
Required scope: summarize
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| text | string | yes | The text to check or rewrite. |
| mode | string | no | The format of the summary. Allowed values: key-sentences, paragraph, bullets. Defaults to key-sentences. |
| length | string | no | The length of the summary. Allowed values: short, medium, long. Defaults to medium. |
Response
| Field | Type | Description |
|---|---|---|
| text | string | The summary text. |
| usage | Usage | The words this call counted. |
Errors specific to this route
| Status | Code | Meaning |
|---|---|---|
| 400 | no_text | The request body does not contain "text". |
| 422 | too_short | The text is shorter than the minimum length. |
| 429 | concurrency_limited | This key already has the maximum number of calls of this type in flight. Wait until one finishes, or for the time given in Retry-After. |
| 503 | timeout | The rewrite ran too long and was stopped. No words were counted. |
| 503 | engine_unavailable | The engine did not respond. No words were counted. |
| 502 | engine_unavailable | The engine did not respond. No words were counted. |
curl -X POST https://textlume.com/api/v1/summarize \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks."}'const res = await fetch('https://textlume.com/api/v1/summarize', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
"text": "On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks."
}),
})
const data = await res.json()
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`)import os
import requests
res = requests.post(
"https://textlume.com/api/v1/summarize",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={
"text": "On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks.",
},
timeout=90,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}"){
"text": "string",
"usage": {
"recorded": "boolean",
"words": "integer",
"source": "plan | balance | plan_and_balance",
"fromPlan": "integer",
"fromBalance": "integer",
"allowanceLeft": "integer | null",
"balance": "integer"
}
}POST /api/v1/grammar
Check grammar. Finds grammar, spelling, punctuation and style issues and returns each one with its position in the text and suggested replacements.
Required scope: grammar
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| text | string | yes | The text to check or rewrite. |
| language | string | no | The language code for the check, such as "en-US" or "de-DE". Defaults to en-US. |
Response
| Field | Type | Description |
|---|---|---|
| issues | array of GrammarIssue | The issues found, in the order they occur in the text. |
| usage | Usage | The words this call counted. |
Errors specific to this route
| Status | Code | Meaning |
|---|---|---|
| 400 | no_text | The request body does not contain "text". |
| 503 | engine_unavailable | The engine did not respond. No words were counted. |
| 502 | engine_unavailable | The engine did not respond. No words were counted. |
| 503 | engine_starting | The engine was still starting and was not ready in time. No words were counted. Try again in about a minute. |
| 504 | engine_timeout | The engine could not finish the text in time. No words were counted. Try again, or send a shorter text. |
curl -X POST https://textlume.com/api/v1/grammar \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks."}'const res = await fetch('https://textlume.com/api/v1/grammar', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
"text": "On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks."
}),
})
const data = await res.json()
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`)import os
import requests
res = requests.post(
"https://textlume.com/api/v1/grammar",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={
"text": "On Tuesday evening the neighborhood association met to discuss the proposed bike lane on Elm Street. Several shop owners worried about losing parking spaces, while parents said the lane would make the route to school safer. The group asked the city to share traffic counts before the next meeting and agreed to collect comments from residents over the coming weeks.",
},
timeout=90,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}"){
"issues": [
{
"offset": "integer",
"length": "integer",
"message": "string",
"shortMessage": "string",
"category": "grammar | spelling | punctuation | style | typography",
"replacements": [
"string"
],
"context": "string"
}
],
"usage": {
"recorded": "boolean",
"words": "integer",
"source": "plan | balance | plan_and_balance",
"fromPlan": "integer",
"fromBalance": "integer",
"allowanceLeft": "integer | null",
"balance": "integer"
}
}Reference
Errors
Errors come with an HTTP status and a body in the shape below. Handle errors by their code. The message explains what failed and what to do about it.
Errors any route can return
| Status | Code | Meaning |
|---|---|---|
| 401 | key_in_url | The key was included in the URL. Send it in the Authorization header instead. |
| 401 | no_key | The request did not include a key. |
| 401 | bad_key | The key is invalid or has been revoked. |
| 403 | out_of_scope | The key lacks the scope this route requires. |
| 429 | rate_limited | This key has sent too many requests. Wait for the time given in Retry-After. |
| 403 | plan_lapsed | The account's current plan does not include API access. |
| 413 | too_large | The request body is too large. |
| 400 | bad_body | The request body is not a JSON object. |
| 413 | too_many_words | This call has more words than a single call accepts. Split it up. Nothing was counted. |
| 402 | quota_exhausted | The plan's API words for this billing period are used up, and there is no top-up balance left. No words were counted. |
| 402 | quota_short | The call needs more words than the plan's remaining API words and the top-up balance can cover together. No words were counted. |
| 402 | monthly_ceiling | The account has hit its API ceiling for this billing period. The ceiling applies regardless of the top-up balance. No words were counted. |
| 503 | store_unavailable | The key, the account's word count or its saved writing sample could not be read at the moment, or this deployment has no account store. Nothing was run and no words were counted. |
Each call reserves its words before it runs and returns them if it fails. If the word count can't be saved at that moment, the words stay charged. A failed call says so and shows "recorded": false in its usage, a batch shows "recorded": false in its usage, and we never estimate the words.
{
"error": {
"code": "string",
"message": "string",
"docs": "string",
"available": "integer"
},
"usage": {
"recorded": "boolean",
"words": "integer",
"source": "plan | balance | plan_and_balance",
"fromPlan": "integer",
"fromBalance": "integer",
"allowanceLeft": "integer | null",
"balance": "integer"
}
}Limits
These are the exact values the API enforces.
- A key can send up to 60 requests in each window of 60 seconds.
- Each key can have up to 4 detect calls (one batch counts as one call) and up to 2 humanize, paraphrase or summarize calls in flight at the same time. Any call beyond that gets 429 concurrency_limited with a Retry-After header.
- When the engines are under load, Pro users on the site are served ahead of API calls, so an API call may have to wait.
- Maximum texts per batch: 50.
- Each call accepts up to 20,000 words, whether you send one text or a whole batch.
- On every plan, the detector reads a single text of up to 90,000 characters, which is roughly 10,000 words. Longer texts get a 413 text_too_long response, even when the call is within its word limit.
- An account can hold up to 5 active keys at once, so you can rotate a key without downtime.
Response objects
Every object the endpoints return, with each of its fields.
Detection
| Field | Type | Description |
|---|---|---|
| figure | number | The reading on a scale from 0 to 100, showing how likely it is that the text was machine-written. Cards and reports on the site show the midpoint between low and high, which can differ from this value. |
| low | number | The bottom of the range around the figure. |
| high | number | The top of the range around the figure. |
| uncertainty | number | Half the width of the range, in points. |
| verdict | string | The band that contains the figure. It describes the figure only and makes no statement about who wrote the text. Allowed values: human, uncertain, ai. |
| confidence | string | How far the reading can be trusted. Allowed values: low, medium, high. |
| sentences | array of SentenceScore | A reading for each sentence. |
| words | integer | The number of words read. |
| language | string | The language the text was read in. |
| model | string | The model that produced this reading. |
| calibrated | boolean | Whether a fitted curve exists for this language. If false, do not use the figure as a result. |
| bandHumanFlagRate | number | null | In our measurements, the share of human-written texts in this band that were read as machine-written. Null when not available. |
SentenceScore
| Field | Type | Description |
|---|---|---|
| text | string | The sentence. |
| score | number | A value from 0 to 1 for how likely it is that the sentence was machine-written. |
| verdict | string | The band that contains the sentence score. Allowed values: human, uncertain, ai. |
Usage
| Field | Type | Description |
|---|---|---|
| recorded | boolean | True when the word count was saved. False only when a batch could not refund the words of texts it did not read. In that case only "words" is included. |
| words | integer | The number of words this call counted. |
| source | string | Where the words were taken from: the plan's API words, the top-up balance or both. Allowed values: plan, balance, plan_and_balance. |
| fromPlan | integer | Words taken from the plan's API words. Included when recorded is true. |
| fromBalance | integer | Words taken from the top-up balance. Included when recorded is true. |
| allowanceLeft | integer | null | The plan's remaining words for this tool in the current billing period, after this call. Null when the plan has no word pool for the tool. Included when recorded is true. |
| balance | integer | The top-up balance remaining after this call. Included when recorded is true. |
RewriteResult
| Field | Type | Description |
|---|---|---|
| text | string | The rewritten text. |
| before | Detection | The detector reading of the text you sent, or null if there is none. |
| after | Detection | The detector reading of the rewritten text, or null. |
| passes | integer | The number of rewrites produced before this one was selected. |
| facts | object | The fact check findings, including the reason when a rewrite is not clean. |
| late | boolean | True when the rewrite was returned at its time limit before its checks had finished. |
GrammarIssue
| Field | Type | Description |
|---|---|---|
| offset | integer | The start of the issue in the text you sent, in UTF-16 code units (the same as a JavaScript string index). |
| length | integer | The number of UTF-16 code units of your text that the issue spans. |
| message | string | A description of the problem. |
| shortMessage | string | A short version of the message. |
| category | string | The type of issue. Allowed values: grammar, spelling, punctuation, style, typography. |
| replacements | array of string | Suggested replacements, with the best one first. |
| context | string | The text surrounding the issue. |
BatchResult
| Field | Type | Description |
|---|---|---|
| id | string | The id of the item. |
| detection | Detection | The reading, if the item returned one. |
| error | object | The reason the item returned no reading. |
Error
| Field | Type | Description |
|---|---|---|
| error | object | |
| usage | Usage | The words this call counted. |
MCP server
Agents can use the same tools through the Model Context Protocol. The server talks only to this API, so an agent receives the same reading as the site.
- detect_textChecks one text.
- detect_batchChecks several texts.
- humanize_textHumanizes a text and runs the fact checks.
- paraphrase_textParaphrases a text.
- summarize_textSummarizes a text.
- check_grammarChecks grammar and spelling.
Claude Code Connects to the hosted server
Store your API key in an environment variable named TEXTLUME_API_KEY on the machine where the client runs:
Run this command in a terminal:
Or add it to .mcp.json instead:
.mcp.json{ "mcpServers": { "textlume": { "type": "http", "url": "https://textlume.com/api/mcp", "headers": { "Authorization": "Bearer ${TEXTLUME_API_KEY}" } } } }To test the connection, ask your assistant: "Use Textlume to check this paragraph and give me the range and the confidence rating."
You will find your API key on your dashboard, under API. Keep keys out of shared files and repositories.
Claude Desktop Uses the server file
Download the server file and remember where you saved it. Node.js is required.
Download the server fileIn the app, go to Settings > Developer > Edit Config.
Add the following to claude_desktop_config.json:
claude_desktop_config.json{ "mcpServers": { "textlume": { "command": "node", "args": [ "/path/to/server.mjs" ], "env": { "TEXTLUME_API_KEY": "YOUR_API_KEY" } } } }Quit Claude Desktop and open it again so it picks up the server.
To test the connection, ask your assistant: "Use Textlume to check this paragraph and give me the range and the confidence rating."
You will find your API key on your dashboard, under API. Keep keys out of shared files and repositories.
Claude (claude.ai) Coming soon
Connecting claude.ai by signing in to your Textlume account is coming soon. For now, the same tools already work in Claude Desktop through the server file, and in Claude Code. Their tabs have the setup.
Only if your organization can set request headers on custom connectors: add the address together with this header.
ChatGPT Coming soon
Connecting ChatGPT by signing in to your Textlume account is coming soon. For now, the same tools already work in Codex and Claude Code. Their tabs have the setup.
Codex Connects to the hosted server
Store your API key in an environment variable named TEXTLUME_API_KEY on the machine where the client runs:
Run this command in a terminal:
Or add it to ~/.codex/config.toml instead:
~/.codex/config.toml[mcp_servers.textlume] url = "https://textlume.com/api/mcp" bearer_token_env_var = "TEXTLUME_API_KEY"In the app, go to MCP servers > Add server > Streamable HTTP.
To test the connection, ask your assistant: "Use Textlume to check this paragraph and give me the range and the confidence rating."
You will find your API key on your dashboard, under API. Keep keys out of shared files and repositories.
Cursor Connects to the hosted server
Store your API key in an environment variable named TEXTLUME_API_KEY on the machine where the client runs:
Add the following to ~/.cursor/mcp.json:
~/.cursor/mcp.json{ "mcpServers": { "textlume": { "url": "https://textlume.com/api/mcp", "headers": { "Authorization": "Bearer ${env:TEXTLUME_API_KEY}" } } } }To test the connection, ask your assistant: "Use Textlume to check this paragraph and give me the range and the confidence rating."
You will find your API key on your dashboard, under API. Keep keys out of shared files and repositories.
VS Code (Copilot) Connects to the hosted server
Add the following to .vscode/mcp.json:
.vscode/mcp.json{ "inputs": [ { "type": "promptString", "id": "textlume-key", "description": "Textlume API key", "password": true } ], "servers": { "textlume": { "type": "http", "url": "https://textlume.com/api/mcp", "headers": { "Authorization": "Bearer ${input:textlume-key}" } } } }The first time the server starts, VS Code asks for your key and stores it.
To test the connection, ask your assistant: "Use Textlume to check this paragraph and give me the range and the confidence rating."
You will find your API key on your dashboard, under API. Keep keys out of shared files and repositories.
Windsurf Connects to the hosted server
Store your API key in an environment variable named TEXTLUME_API_KEY on the machine where the client runs:
In the app, go to Cascade > ... > Open MCP config file.
Add the following to mcp_config.json:
mcp_config.json{ "mcpServers": { "textlume": { "serverUrl": "https://textlume.com/api/mcp", "headers": { "Authorization": "Bearer ${env:TEXTLUME_API_KEY}" } } } }Quit Windsurf and open it again so it picks up the server.
To test the connection, ask your assistant: "Use Textlume to check this paragraph and give me the range and the confidence rating."
You will find your API key on your dashboard, under API. Keep keys out of shared files and repositories.
Gemini CLI Connects to the hosted server
Store your API key in an environment variable named TEXTLUME_API_KEY on the machine where the client runs:
Run this command in a terminal:
Or add it to ~/.gemini/settings.json instead:
~/.gemini/settings.json{ "mcpServers": { "textlume": { "httpUrl": "https://textlume.com/api/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } }To test the connection, ask your assistant: "Use Textlume to check this paragraph and give me the range and the confidence rating."
You will find your API key on your dashboard, under API. Keep keys out of shared files and repositories.
Other clients Hosted server, bridge or server file
A client that connects to remote MCP servers over HTTP only needs these two values:
A client that can only launch local servers can still reach the address through the open-source mcp-remote bridge, which requires Node.js:
Or run the server file directly:
Download the server file
You will find your API key on your dashboard, under API. Keep keys out of shared files and repositories.
Each reading an agent receives includes the figure, its uncertainty and the calibration state. That lets the agent explain what the reading means and when it should not be used.
API key
Sign in first, then create a key on your dashboard.
This page does not create keys.
Overview of the API and the MCP server