On this page
Getting started
Overview
The API is versioned in its path, /api/v1. Every endpoint takes a POST with a JSON body and answers with JSON, errors included.
Base URL https://blurwrite.com
- POST /api/v1/detectRead one text
- POST /api/v1/detect/batchRead 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
On the site only, not in the API: the word counter, the character counter, the plagiarism checker and the humanizer's "Take more time" run.
Rendered from the OpenAPI document, so it lists exactly the routes and fields the API accepts. The OpenAPI document
Your first call
A text needs at least 50 words.
Read "calibrated" before anything else: when it is false, the language has no fitted curve yet, so the figure must not be used as a result.
Each value is shown as its type. A real response carries the reading for your text.
curl -X POST https://blurwrite.com/api/v1/detect \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building."}'const res = await fetch('https://blurwrite.com/api/v1/detect', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
"text": "The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building."
}),
})
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://blurwrite.com/api/v1/detect",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={
"text": "The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building.",
},
timeout=90,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}"){
"figure": "number, 0 to 100",
"low": "number, 0 to 100",
"high": "number, 0 to 100",
"uncertainty": "number",
"verdict": "human | uncertain | ai",
"confidence": "low | medium | high",
"sentences": [
{
"text": "string",
"score": "number, 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
Send your key in the Authorization header as a bearer token. A key in the URL is refused.
A key carries scopes: detect, rewrite, summarize and grammar. A route answers 403 out_of_scope when the key does not carry its scope.
Authorization: Bearer YOUR_API_KEYEndpoints
POST /api/v1/detect
Read one text. The detector reading for one text: a figure from 0 to 100, the range around it, a reading per sentence and whether the language is calibrated.
Key scope: detect
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| text | string | yes | The text to read or rewrite. |
| language | string | no | The language code, such as "en" or "de". Left out, it is detected from the text. |
| contentType | string | no | What kind of text it is, such as "essay" or "email", when the reading should use that kind of text's curve. |
Response
| Field | Type | Description |
|---|---|---|
| figure | number | The reading from 0 to 100: how likely the text is machine-written. The site shows the middle of low and high on its cards and reports, which is not always this number. |
| low | number | The lower end of the range around the figure. |
| high | number | The upper end of the range around the figure. |
| uncertainty | number | Half the width of the range, in points. |
| verdict | string | The band the figure falls in; a band, never a finding about who wrote the text. One of: human, uncertain, ai. |
| confidence | string | How much the reading can be relied on. One of: low, medium, high. |
| sentences | array of SentenceScore | A reading per sentence. |
| words | integer | The words read. |
| language | string | The language the text was read as. |
| model | string | The model that produced the reading. |
| calibrated | boolean | Whether the language has a fitted curve. When false, the figure must not be used as a result. |
| bandHumanFlagRate | number | null | How often human text in this band was read as machine in our measurements, or null. |
| usage | Usage | What this call counted. |
Errors this route adds
| Status | Code | Meaning |
|---|---|---|
| 400 | no_text | The body has no "text". |
| 422 | too_short | The text is under the minimum length. |
| 413 | text_too_long | The text is longer than the engine reads in one call. Split it. Nothing was counted. |
| 422 | not_calibrated | The language cannot be read yet. |
| 503 | engine_unavailable | The engine did not answer. Nothing was counted. |
| 502 | engine_unavailable | The engine did not answer. Nothing was counted. |
| 503 | engine_starting | The engine is starting up and was not ready in time. Nothing was counted. Try again in a minute. |
| 503 | engine_busy | The engine was busy and the reading could not start in time; the site's Pro readers are served before API calls. Nothing was counted. Try again in a moment. |
| 504 | engine_timeout | The engine did not finish the text in time. Nothing was counted. Try again, or send a shorter text. |
| 429 | concurrency_limited | This key already has as many calls of this kind running as it may. Wait for one to finish, or for the time in Retry-After. |
curl -X POST https://blurwrite.com/api/v1/detect \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building."}'const res = await fetch('https://blurwrite.com/api/v1/detect', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
"text": "The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building."
}),
})
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://blurwrite.com/api/v1/detect",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={
"text": "The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building.",
},
timeout=90,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}"){
"figure": "number, 0 to 100",
"low": "number, 0 to 100",
"high": "number, 0 to 100",
"uncertainty": "number",
"verdict": "human | uncertain | ai",
"confidence": "low | medium | high",
"sentences": [
{
"text": "string",
"score": "number, 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
Read several texts. Up to the batch limit of texts in one call. The call needs room up front for the words of every text it can read (50 words or more); the words of any text that returns no reading are given back afterwards. One text failing never fails the batch.
Key scope: detect
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| items | array of object | yes | The texts, each with its own "text" and an optional "id", "language" and "contentType". |
| items[].id | string | no | Your own id for the item, returned with its result. Left out, the item's position is used. |
| items[].text | string | yes | The text to read or rewrite. |
| items[].language | string | no | The language code, such as "en" or "de". Left out, it is detected from the text. |
| items[].contentType | string | no | What kind of text it is, such as "essay" or "email", when the reading should use that kind of text's curve. |
Response
| Field | Type | Description |
|---|---|---|
| results | array of BatchResult | One result per item, in the order sent. |
| usage | Usage | What this call counted. |
Errors this route adds
| Status | Code | Meaning |
|---|---|---|
| 400 | batch_empty | The body has no "items", or the list is empty. |
| 413 | batch_too_large | The batch has more items than its limit. |
| 429 | concurrency_limited | This key already has as many calls of this kind running as it may. Wait for one to finish, or for the time in Retry-After. |
curl -X POST https://blurwrite.com/api/v1/detect/batch \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"items":[{"id":"1","text":"The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building."},{"id":"2","text":"The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building."}]}'const res = await fetch('https://blurwrite.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": "The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building."
},
{
"id": "2",
"text": "The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building."
}
]
}),
})
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://blurwrite.com/api/v1/detect/batch",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={
"items": [
{
"id": "1",
"text": "The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building.",
},
{
"id": "2",
"text": "The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building.",
},
],
},
timeout=90,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}"){
"results": [
{
"id": "string",
"detection": {
"figure": "number, 0 to 100",
"low": "number, 0 to 100",
"high": "number, 0 to 100",
"uncertainty": "number",
"verdict": "human | uncertain | ai",
"confidence": "low | medium | high",
"sentences": [
{
"text": "string",
"score": "number, 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. The humanizer with its fact checks: numbers, names and quotations are kept, and a rewrite that changed a fact is repaired or reported. The mode "custom" (Pro) writes in your own style, read from a sample of your writing.
Through the API the humanizer runs its standard rewrite. "Take more time" is on the site only.
Key scope: rewrite
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| text | string | yes | The text to read or rewrite. |
| mode | string | no | The rewriting mode. One of: natural, formal, simple, academic, creative, shorten, expand, custom. Default: natural. |
| style_sample | string | no | With the mode "custom": a sample of your own writing, 150 to 2,000 words, read on the server. Left out, the sample saved to your account is used. |
| tab | string | no | The humanizer tab: the register the rewrite is written in. Standard keeps the register the text came in. One of: standard, academic, professional, casual. Default: standard. |
| strength | string | no | Regular keeps the content and the register. Aggressive may reorder the text and leave elaboration out, keeps every main point, and parts may read unusually. One of: regular, aggressive. Default: regular. |
| ownTouch | string | no | A detail, example or opinion in your own words, up to 400 characters, worked into the rewrite. |
| language | string | no | The language code, such as "en" or "de". Left out, it is detected from the text. |
Response
| Field | Type | Description |
|---|---|---|
| text | string | The rewritten text. |
| before | Detection | The detector reading of your text, or null when there is none. |
| after | Detection | The detector reading of the rewrite, or null. |
| passes | integer | How many rewrites were written before this one was chosen. |
| facts | object | What the fact checks found, and why a rewrite is not clean when it is not. |
| late | boolean | True when the rewrite came back at its time limit with its checks unfinished. |
| usage | Usage | What this call counted. |
Errors this route adds
| Status | Code | Meaning |
|---|---|---|
| 400 | no_text | The body has no "text". |
| 422 | too_short | The text is under the minimum length. |
| 400 | unknown_mode | The mode is not one this route offers. |
| 400 | unknown_option | The tab or the strength is not one this route offers. |
| 402 | plan_required | The mode, the tab, the strength or the own touch needs a plan the account does not have; the mode "custom" needs Pro. Nothing was counted. |
| 400 | no_sample | The mode "custom" needs a writing sample, sent or saved to the account. |
| 400 | sample_too_short | The writing sample is under the minimum length. |
| 400 | sample_too_long | The writing sample is over the maximum length. |
| 429 | concurrency_limited | This key already has as many calls of this kind running as it may. Wait for one to finish, or for the time in Retry-After. |
| 422 | no_rewrite | The engine answered, but no draft passed the checks (empty, a refusal or a copy of the text). Nothing was counted. |
| 503 | timeout | The rewrite took too long and was stopped. Nothing was counted. |
| 503 | engine_unavailable | The engine did not answer. Nothing was counted. |
| 502 | engine_unavailable | The engine did not answer. Nothing was counted. |
curl -X POST https://blurwrite.com/api/v1/humanize \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building."}'const res = await fetch('https://blurwrite.com/api/v1/humanize', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
"text": "The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building."
}),
})
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://blurwrite.com/api/v1/humanize",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={
"text": "The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building.",
},
timeout=90,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}"){
"text": "string",
"before": {
"figure": "number, 0 to 100",
"low": "number, 0 to 100",
"high": "number, 0 to 100",
"uncertainty": "number",
"verdict": "human | uncertain | ai",
"confidence": "low | medium | high",
"sentences": [
{
"text": "string",
"score": "number, 0 to 1",
"verdict": "human | uncertain | ai"
}
],
"words": "integer",
"language": "string",
"model": "string",
"calibrated": "boolean",
"bandHumanFlagRate": "number | null"
},
"after": {
"figure": "number, 0 to 100",
"low": "number, 0 to 100",
"high": "number, 0 to 100",
"uncertainty": "number",
"verdict": "human | uncertain | ai",
"confidence": "low | medium | high",
"sentences": [
{
"text": "string",
"score": "number, 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. The paraphraser, with the same mode list as the paraphrasing tool.
Key scope: rewrite
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| text | string | yes | The text to read or rewrite. |
| mode | string | no | The rewriting mode. One of: standard, fluency, formal, academic, simple, creative, expand, shorten. Default: standard. |
| strength | string | no | How far the rewrite may move from the original. One of: readable, balanced, strong. Default: balanced. |
| readability | string | no | The register to write in. One of: casual, professional, academic, creative. Default: professional. |
| purpose | string | no | What the text is for. One of: general, essay, article, email, marketing, report, story. Default: general. |
| synonyms | string | no | How freely words may be replaced. One of: few, some, many. Default: some. |
| freezeWords | array of string | no | Words and phrases that must come back exactly as written: at most 25, each up to 60 characters. Their words count toward the call's words. |
| customInstruction | string | no | A short instruction of your own for the rewrite, up to 500 characters. Its words count toward the call's words. |
| language | string | no | The language code, such as "en" or "de". Left out, it is detected from the text. |
| closedLoop | boolean | no | Set false to skip the closed loop. Default: true. |
Response
| Field | Type | Description |
|---|---|---|
| text | string | The rewritten text. |
| before | Detection | The detector reading of your text, or null when there is none. |
| after | Detection | The detector reading of the rewrite, or null. |
| passes | integer | How many rewrites were written before this one was chosen. |
| facts | object | What the fact checks found, and why a rewrite is not clean when it is not. |
| late | boolean | True when the rewrite came back at its time limit with its checks unfinished. |
| usage | Usage | What this call counted. |
Errors this route adds
| Status | Code | Meaning |
|---|---|---|
| 400 | no_text | The body has no "text". |
| 422 | too_short | The text is under the minimum length. |
| 400 | unknown_mode | The mode is not one this route offers. |
| 400 | option_invalid | freezeWords is not a list of words or phrases. Nothing was counted. |
| 400 | option_too_long | freezeWords or customInstruction is over its limit. Nothing was counted. |
| 429 | concurrency_limited | This key already has as many calls of this kind running as it may. Wait for one to finish, or for the time in Retry-After. |
| 422 | no_rewrite | The engine answered, but no draft passed the checks (empty, a refusal or a copy of the text). Nothing was counted. |
| 503 | timeout | The rewrite took too long and was stopped. Nothing was counted. |
| 503 | engine_unavailable | The engine did not answer. Nothing was counted. |
| 502 | engine_unavailable | The engine did not answer. Nothing was counted. |
curl -X POST https://blurwrite.com/api/v1/paraphrase \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building."}'const res = await fetch('https://blurwrite.com/api/v1/paraphrase', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
"text": "The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building."
}),
})
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://blurwrite.com/api/v1/paraphrase",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={
"text": "The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building.",
},
timeout=90,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}"){
"text": "string",
"before": {
"figure": "number, 0 to 100",
"low": "number, 0 to 100",
"high": "number, 0 to 100",
"uncertainty": "number",
"verdict": "human | uncertain | ai",
"confidence": "low | medium | high",
"sentences": [
{
"text": "string",
"score": "number, 0 to 1",
"verdict": "human | uncertain | ai"
}
],
"words": "integer",
"language": "string",
"model": "string",
"calibrated": "boolean",
"bandHumanFlagRate": "number | null"
},
"after": {
"figure": "number, 0 to 100",
"low": "number, 0 to 100",
"high": "number, 0 to 100",
"uncertainty": "number",
"verdict": "human | uncertain | ai",
"confidence": "low | medium | high",
"sentences": [
{
"text": "string",
"score": "number, 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. The summarizer, as key sentences, a paragraph or bullets.
Key scope: summarize
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| text | string | yes | The text to read or rewrite. |
| mode | string | no | The form of the summary. One of: key-sentences, paragraph, bullets. Default: key-sentences. |
| length | string | no | How long the summary is. One of: short, medium, long. Default: medium. |
Response
| Field | Type | Description |
|---|---|---|
| text | string | The summary. |
| usage | Usage | What this call counted. |
Errors this route adds
| Status | Code | Meaning |
|---|---|---|
| 400 | no_text | The body has no "text". |
| 422 | too_short | The text is under the minimum length. |
| 429 | concurrency_limited | This key already has as many calls of this kind running as it may. Wait for one to finish, or for the time in Retry-After. |
| 503 | timeout | The rewrite took too long and was stopped. Nothing was counted. |
| 503 | engine_unavailable | The engine did not answer. Nothing was counted. |
| 502 | engine_unavailable | The engine did not answer. Nothing was counted. |
curl -X POST https://blurwrite.com/api/v1/summarize \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building."}'const res = await fetch('https://blurwrite.com/api/v1/summarize', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
"text": "The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building."
}),
})
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://blurwrite.com/api/v1/summarize",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={
"text": "The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building.",
},
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. Grammar, spelling, punctuation and style issues, each with its place in the text and suggested replacements.
Key scope: grammar
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| text | string | yes | The text to read or rewrite. |
| language | string | no | The language code for the check, such as "en-US" or "de-DE". Default: en-US. |
Response
| Field | Type | Description |
|---|---|---|
| issues | array of GrammarIssue | The issues found, in the order they appear. |
| usage | Usage | What this call counted. |
Errors this route adds
| Status | Code | Meaning |
|---|---|---|
| 400 | no_text | The body has no "text". |
| 503 | engine_unavailable | The engine did not answer. Nothing was counted. |
| 502 | engine_unavailable | The engine did not answer. Nothing was counted. |
| 503 | engine_starting | The engine is starting up and was not ready in time. Nothing was counted. Try again in a minute. |
| 504 | engine_timeout | The engine did not finish the text in time. Nothing was counted. Try again, or send a shorter text. |
curl -X POST https://blurwrite.com/api/v1/grammar \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building."}'const res = await fetch('https://blurwrite.com/api/v1/grammar', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
"text": "The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building."
}),
})
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://blurwrite.com/api/v1/grammar",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={
"text": "The committee met on Tuesday to review the plan for the new library. Members agreed that the reading room should stay open later on weekdays, and they asked the staff to prepare a budget for extra hours. The next meeting will look at the costs, the likely number of visitors and the effect on the rest of the building.",
},
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
An error answers with its HTTP status and a body in this shape. Act on the code; the message says what went wrong and what to do next.
Errors every route can answer
| Status | Code | Meaning |
|---|---|---|
| 401 | key_in_url | The key was sent in the URL. Send it in the Authorization header. |
| 401 | no_key | No key was sent. |
| 401 | bad_key | The key is not valid or was revoked. |
| 403 | out_of_scope | The key does not carry the scope this route needs. |
| 429 | rate_limited | Too many requests for this key. Wait for the time in Retry-After. |
| 403 | plan_lapsed | The account's plan no longer includes the API. |
| 413 | too_large | The request is too large. |
| 400 | bad_body | The body is not a JSON object. |
| 413 | too_many_words | The call has more words than one call takes. Split it. Nothing was counted. |
| 402 | quota_exhausted | The plan's API words for this billing period are used up and the top-up balance is empty. Nothing was counted. |
| 402 | quota_short | The call needs more words than the plan's API words left and the balance together. Nothing was counted. |
| 402 | monthly_ceiling | The account has reached its API ceiling for this billing period, which holds whatever the balance. Nothing was counted. |
| 503 | store_unavailable | The key, the account's word count or its saved writing sample could not be read just now, or this deployment has no account store. Nothing was run or counted. |
A call reserves its words before it runs and gives them back if it fails. If the word count cannot be written at that moment, the words stay charged: a failed call then says so and carries "recorded": false in its usage, a batch answers with "recorded": false in its usage, and the words are not guessed.
{
"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
Every figure here is the one the API enforces.
- Each key makes at most 60 requests every 60 seconds.
- Each key has at most 4 detect calls (a batch counts as one) and 2 humanize, paraphrase or summarize calls running at once; one more is answered 429 concurrency_limited with a Retry-After.
- When the engines are busy, readings from the site's Pro accounts are served before API calls, so an API call may wait its turn.
- A batch holds at most 50 texts.
- One call takes at most 20,000 words, a single text or a whole batch.
- The detector reads one text of up to 90,000 characters, about 10,000 words, on every plan. A longer text is answered 413 text_too_long, even inside the call's word limit.
- An account holds up to 5 active keys, so a key can be rotated without downtime.
Response objects
The objects the endpoints return, field by field.
Detection
| Field | Type | Description |
|---|---|---|
| figure | number | The reading from 0 to 100: how likely the text is machine-written. The site shows the middle of low and high on its cards and reports, which is not always this number. |
| low | number | The lower end of the range around the figure. |
| high | number | The upper end of the range around the figure. |
| uncertainty | number | Half the width of the range, in points. |
| verdict | string | The band the figure falls in; a band, never a finding about who wrote the text. One of: human, uncertain, ai. |
| confidence | string | How much the reading can be relied on. One of: low, medium, high. |
| sentences | array of SentenceScore | A reading per sentence. |
| words | integer | The words read. |
| language | string | The language the text was read as. |
| model | string | The model that produced the reading. |
| calibrated | boolean | Whether the language has a fitted curve. When false, the figure must not be used as a result. |
| bandHumanFlagRate | number | null | How often human text in this band was read as machine in our measurements, or null. |
SentenceScore
| Field | Type | Description |
|---|---|---|
| text | string | The sentence. |
| score | number | From 0 to 1, how likely the sentence is machine-written. |
| verdict | string | The band the sentence falls in. One of: human, uncertain, ai. |
Usage
| Field | Type | Description |
|---|---|---|
| recorded | boolean | True when the count was written. False only when a batch could not give back its unread words; then only "words" is present. |
| words | integer | The words this call counted. |
| source | string | Where they came from: the plan's API words, the top-up balance, or both. One of: plan, balance, plan_and_balance. |
| fromPlan | integer | The words taken from the plan's API words. Present when recorded is true. |
| fromBalance | integer | The words taken from the top-up balance. Present when recorded is true. |
| allowanceLeft | integer | null | The plan's words left for this tool after this call, in the current billing period. Null for a tool with no pool on this plan. Present when recorded is true. |
| balance | integer | The top-up balance left after this call. Present when recorded is true. |
RewriteResult
| Field | Type | Description |
|---|---|---|
| text | string | The rewritten text. |
| before | Detection | The detector reading of your text, or null when there is none. |
| after | Detection | The detector reading of the rewrite, or null. |
| passes | integer | How many rewrites were written before this one was chosen. |
| facts | object | What the fact checks found, and why a rewrite is not clean when it is not. |
| late | boolean | True when the rewrite came back at its time limit with its checks unfinished. |
GrammarIssue
| Field | Type | Description |
|---|---|---|
| offset | integer | Where the issue starts in the text you sent, in UTF-16 code units (a JavaScript string index). |
| length | integer | How many UTF-16 code units of the text you sent it covers. |
| message | string | What is wrong. |
| shortMessage | string | The same, in a few words. |
| category | string | The kind of issue. One of: grammar, spelling, punctuation, style, typography. |
| replacements | array of string | Suggested replacements, best first. |
| context | string | The text around the issue. |
BatchResult
| Field | Type | Description |
|---|---|---|
| id | string | The item's id. |
| detection | Detection | The reading, when the item returned one. |
| error | object | Why the item returned no reading. |
Error
| Field | Type | Description |
|---|---|---|
| error | object | |
| usage | Usage | What this call counted. |
The MCP server
An agent can call the same tools through the Model Context Protocol. The server is a client of this API and nothing else, so an agent gets exactly the reading the site gives.
- detect_textReads one text.
- detect_batchReads several texts.
- humanize_textHumanizes a text with its fact checks.
- paraphrase_textParaphrases a text.
- summarize_textSummarizes a text.
- check_grammarChecks grammar and spelling.
Claude Code Connects online
Keep your API key in an environment variable called BLURWRITE_API_KEY, where the client runs:
Run this in a terminal:
Or put this in .mcp.json instead:
.mcp.json{ "mcpServers": { "blurwrite": { "type": "http", "url": "https://blurwrite.com/api/mcp", "headers": { "Authorization": "Bearer ${BLURWRITE_API_KEY}" } } } }Check that it works: ask your assistant "Check this paragraph with Blurwrite and tell me the range and the confidence rating."
Your API key is on your dashboard under API. Never put a key in a shared file or a repository.
Claude Desktop Runs the server file
Download the server file and note where you saved it. It needs Node.js.
Download the server fileIn the app: Settings > Developer > Edit Config.
Add this to claude_desktop_config.json:
claude_desktop_config.json{ "mcpServers": { "blurwrite": { "command": "node", "args": [ "/path/to/server.mjs" ], "env": { "BLURWRITE_API_KEY": "YOUR_API_KEY" } } } }Quit and restart Claude Desktop so it loads the server.
Check that it works: ask your assistant "Check this paragraph with Blurwrite and tell me the range and the confidence rating."
Your API key is on your dashboard under API. Never put a key in a shared file or a repository.
Claude (claude.ai) Coming soon
Connecting claude.ai through a sign-in to your Blurwrite account is coming soon. Until then, the same tools work today in Claude Desktop with the server file, or in Claude Code, with the setup in their tabs.
Only if your organization has the request headers option for custom connectors: add the address with this header.
ChatGPT Coming soon
Connecting ChatGPT through a sign-in to your Blurwrite account is coming soon. Until then, the same tools work today in Codex and Claude Code, with the setup in their tabs.
Codex Connects online
Keep your API key in an environment variable called BLURWRITE_API_KEY, where the client runs:
Run this in a terminal:
Or put this in ~/.codex/config.toml instead:
~/.codex/config.toml[mcp_servers.blurwrite] url = "https://blurwrite.com/api/mcp" bearer_token_env_var = "BLURWRITE_API_KEY"In the app: MCP servers > Add server > Streamable HTTP.
Check that it works: ask your assistant "Check this paragraph with Blurwrite and tell me the range and the confidence rating."
Your API key is on your dashboard under API. Never put a key in a shared file or a repository.
Cursor Connects online
Keep your API key in an environment variable called BLURWRITE_API_KEY, where the client runs:
Add this to ~/.cursor/mcp.json:
~/.cursor/mcp.json{ "mcpServers": { "blurwrite": { "url": "https://blurwrite.com/api/mcp", "headers": { "Authorization": "Bearer ${env:BLURWRITE_API_KEY}" } } } }Check that it works: ask your assistant "Check this paragraph with Blurwrite and tell me the range and the confidence rating."
Your API key is on your dashboard under API. Never put a key in a shared file or a repository.
VS Code (Copilot) Connects online
Add this to .vscode/mcp.json:
.vscode/mcp.json{ "inputs": [ { "type": "promptString", "id": "blurwrite-key", "description": "Blurwrite API key", "password": true } ], "servers": { "blurwrite": { "type": "http", "url": "https://blurwrite.com/api/mcp", "headers": { "Authorization": "Bearer ${input:blurwrite-key}" } } } }When the server starts, VS Code asks for your key once and keeps it.
Check that it works: ask your assistant "Check this paragraph with Blurwrite and tell me the range and the confidence rating."
Your API key is on your dashboard under API. Never put a key in a shared file or a repository.
Windsurf Connects online
Keep your API key in an environment variable called BLURWRITE_API_KEY, where the client runs:
In the app: Cascade > ... > Open MCP config file.
Add this to mcp_config.json:
mcp_config.json{ "mcpServers": { "blurwrite": { "serverUrl": "https://blurwrite.com/api/mcp", "headers": { "Authorization": "Bearer ${env:BLURWRITE_API_KEY}" } } } }Quit and restart Windsurf so it loads the server.
Check that it works: ask your assistant "Check this paragraph with Blurwrite and tell me the range and the confidence rating."
Your API key is on your dashboard under API. Never put a key in a shared file or a repository.
Gemini CLI Connects online
Keep your API key in an environment variable called BLURWRITE_API_KEY, where the client runs:
Run this in a terminal:
Or put this in ~/.gemini/settings.json instead:
~/.gemini/settings.json{ "mcpServers": { "blurwrite": { "httpUrl": "https://blurwrite.com/api/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } }Check that it works: ask your assistant "Check this paragraph with Blurwrite and tell me the range and the confidence rating."
Your API key is on your dashboard under API. Never put a key in a shared file or a repository.
Other clients Online, a bridge or the file
A client that connects to remote MCP servers over HTTP needs only these two values:
A client that only starts local servers can reach the address through the open-source mcp-remote bridge (it needs Node.js):
Or run our server file yourself:
Download the server file
Your API key is on your dashboard under API. Never put a key in a shared file or a repository.
Every reading an agent gets carries the figure, the uncertainty around it and the calibration state, so the agent can say what the reading means and when it must not be used.
Your key
Sign in, then create a key on your dashboard.
Keys are never created on this page.
What the API and the MCP server do