Documentation
Humanizer API docs
A JSON REST API for humanizing and detecting AI text. Authenticate with a key, send text, get text back.
Quickstart
Make your first request in under five minutes. All you need is an API key and any HTTP client.
Create an account and a key
Sign up with email or Google, open API keys in the dashboard and create a key. The full secret is shown once — copy it then.
Keep the key on your server
export HUMANIZER_API_KEY="ht_live_…"Send your first request
curl https://api.humanizertool.io/v1/humanize \ -H "Authorization: Bearer $HUMANIZER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Artificial intelligence has fundamentally transformed the way organizations approach content creation, enabling unprecedented efficiency.", "style": "business" }'Read the response
200 OK{ "id": "hum_oJoMouByloZk", "object": "humanize", "output": "AI has changed how teams write. Work that took days now takes an afternoon.", "words_in": 16, "words_out": 15, "style": "business", "usage": { "words_used": 1240, "words_limit": 100000, "words_remaining": 98760, "period_end": "2026-11-30T08:25:17.000Z" }, "request_id": "req_8e2d4f6a1b3c5d7e9f0a2b4c" }outputholds the rewritten text.usagetells you how many words are left this period, so you never have to guess.
Authentication
Every request is authenticated with a secret API key sent in a header.
Authorization: Bearer ht_live_…
# or
x-api-key: ht_live_…Keys look like ht_live_ or ht_test_ followed by 43 random characters. We store only a hash, so a lost key can't be recovered — revoke it and create a new one. Each key can be limited to the humanize and/or detect scopes and can expire after 30, 60 or 90 days.
Live and test keys
| Mode | Behaviour | Metered |
|---|---|---|
ht_live_ | Real humanize and detect results. Counts toward your plan's live-key limit. | Yes |
ht_test_ | Deterministic stub responses with the same shape. Limited to 60 requests per minute; never counts toward the key limit. | Never |
Keys are secrets.
Call the API from your backend, not from browsers or mobile apps. Never commit keys to git — use environment variables or a secrets manager.
To check a key, call GET https://api.humanizertool.io/v1/me: it returns the account, plan and key details (see Usage & account).
Playground
Try every option in the browser before you write code.
The playground in your dashboard runs humanize and detect with the same options as the API and shows the request you can copy. Playground calls use your account's words and are logged as playground:humanize / playground:detect.
Humanize
Rewrites AI-generated text so it reads naturally.
Body
| Field | Type | Description |
|---|---|---|
| text | string, required | The text to rewrite. Up to your plan's words-per-request limit. |
| style | string (enum) | One of neutral (default), academic, business, creative, technical, friendly, informal. |
| length | string (enum) | concise, normal (default) or expanded. |
| similarity | string (enum) | How close to stay to the original: low, moderate (default) or high. |
| language | string | auto (default) or an ISO 639-1 code such as en, de, es. |
curl https://api.humanizertool.io/v1/humanize \
-H "Authorization: Bearer $HUMANIZER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Artificial intelligence has fundamentally transformed the way organizations approach content creation, enabling unprecedented efficiency.",
"style": "business"
}'Response
| Field | Type | Description |
|---|---|---|
| id | string | Unique id, prefixed hum_. |
| object | string | Always humanize. |
| output | string | The humanized text. |
| words_in / words_out | integer | Word counts of the input and the output. You are charged words_in. |
| style | string (enum) | The style that was applied. |
| usage | object | words_used, words_limit, words_remaining (integers) and period_end (date-time, null on the Free trial) after this request. |
| request_id | string | Same as the X-Request-Id header. |
{
"id": "hum_oJoMouByloZk",
"object": "humanize",
"output": "AI has changed how teams write. Work that took days now takes an afternoon.",
"words_in": 16,
"words_out": 15,
"style": "business",
"usage": {
"words_used": 1240,
"words_limit": 100000,
"words_remaining": 98760,
"period_end": "2026-11-30T08:25:17.000Z"
},
"request_id": "req_8e2d4f6a1b3c5d7e9f0a2b4c"
}Detect
Scores how likely a text is to be AI-generated and flags the sentences that look machine-written.
Send { "text": "…" } with at least 300 characters; shorter texts return 422 text_too_short. Detect draws from the same word pool as humanize and takes longer, so keep a generous client timeout.
curl https://api.humanizertool.io/v1/detect \
-H "Authorization: Bearer $HUMANIZER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Artificial intelligence has fundamentally transformed the way modern organizations approach content creation. By automating repetitive writing tasks, these systems enable teams to produce high-quality material at unprecedented speed. Furthermore, they ensure consistency across channels while significantly reducing operational costs for businesses of every size."
}'| Field | Type | Description |
|---|---|---|
| id | string | Unique id, prefixed det_. |
| object | string | Always detect. |
| score | number | 0–100: likelihood the text is AI-generated. |
| verdict | string (enum) | human, mixed or ai. |
| human_score | number | 100 − score. |
| ai_word_count / total_word_count | integer | Words in flagged sentences vs. all words. |
| sentences | array | Flagged sentences only: [{ text, flagged: true }]. |
| usage, request_id | object, string | Same as humanize. |
{
"id": "det_a1B2c3D4e5F6",
"object": "detect",
"score": 93,
"verdict": "ai",
"human_score": 7,
"ai_word_count": 44,
"total_word_count": 44,
"sentences": [
{ "text": "Artificial intelligence has fundamentally transformed the way modern organizations approach content creation.", "flagged": true },
{ "text": "By automating repetitive writing tasks, these systems enable teams to produce high-quality material at unprecedented speed.", "flagged": true },
{ "text": "Furthermore, they ensure consistency across channels while significantly reducing operational costs for businesses of every size.", "flagged": true }
],
"usage": {
"words_used": 1284,
"words_limit": 100000,
"words_remaining": 98716,
"period_end": "2026-11-30T08:25:17.000Z"
},
"request_id": "req_3c5e7a9b1d3f5a7c9e1b3d5f"
}Usage & account
Read-only endpoints for checking a key and its quota. They are not metered.
{
"object": "account",
"email": "dev@example.com",
"plan": { "id": "starter", "name": "Starter" },
"key": {
"id": "302a20f0-67ce-4507-a7ae-e272f633941b",
"name": "Production",
"prefix": "ht_live_Ab3x",
"mode": "live",
"scopes": ["humanize", "detect"],
"created_at": "2026-10-30T09:12:44.000Z"
},
"limits": { "words_per_month": 100000, "trial_words": null, "max_words_per_request": 1500, "requests_per_minute": 30 },
"period": { "start": "2026-10-30T08:25:17.000Z", "end": "2026-11-30T08:25:17.000Z" },
"request_id": "req_e5bbc9b25e899a4dd055209e"
}{
"object": "usage",
"plan": { "id": "starter", "name": "Starter" },
"period": { "start": "2026-10-30T08:25:17.000Z", "end": "2026-11-30T08:25:17.000Z" },
"words": { "used": 1284, "limit": 100000, "remaining": 98716 },
"requests": { "period": 12 },
"by_endpoint": {
"humanize": { "requests": 9, "words": 1240 },
"detect": { "requests": 1, "words": 44 }
},
"request_id": "req_c813a184dba46eed6b2dff78"
}key.id is a UUID. On the Free trial the word pool is one-time, so period.start and period.end are null (and so is usage.period_end in humanize and detect responses); limits.trial_words is set and limits.words_per_month is null.
| Endpoint | Auth | Description |
|---|---|---|
GET /health | None | { "ok": true, "version": "1", "service": "humanizer-api", "commit": "4cb8285" } — use it for uptime checks. commit identifies the deployed build. |
GET /openapi.json | None | The OpenAPI 3.1 document rendered in the reference below. |
Errors
Every non-2xx response uses the same JSON envelope, so one error handler covers the whole API.
{
"error": {
"type": "billing_error",
"code": "quota_exceeded",
"message": "Your word quota for this billing period is exhausted.",
"request_id": "req_e004dfec06a3b07a59d03767",
"details": {
"words_used": 100000,
"words_limit": 100000,
"period_end": "2026-11-30T08:25:17.000Z",
"upgrade_url": "https://api.humanizertool.io/billing"
}
}
}Branch on error.code (stable) rather than message (human-readable, may change). On the Free trial the 402 message reads “Your free trial words are used up. Upgrade to keep going.” and details.period_end is null. Requests that fail with 4xx or 503 never consume words.
| Status | Type | Code | Meaning |
|---|---|---|---|
| 400 | invalid_request_error | invalid_request | The body failed validation. details.issues lists each problem (path and message). |
| 401 | authentication_error | missing_api_key, invalid_api_key, key_revoked, key_expired | No key, wrong key, or the key is no longer active. |
| 402 | billing_error | quota_exceeded | Not enough words left. Nothing was consumed. details has words_used, words_limit, period_end (null on the Free trial) and upgrade_url. |
| 403 | permission_error | scope_not_allowed, plan_required | The key lacks the scope for this endpoint (details.scope). plan_required is reserved for plan-only features; no current endpoint returns it. |
| 404 | invalid_request_error | not_found | Unknown path, for example a typo in the URL. |
| 409 | invalid_request_error | key_limit_reached, already_subscribed | Dashboard actions only: live-key limit reached (details.max_keys) or already on that plan. |
| 413 | invalid_request_error | text_too_long | Text exceeds your plan's words per request (details.max_words, details.words). Nothing consumed. |
| 422 | invalid_request_error | text_too_short | Detect needs at least details.min_chars (300) characters. |
| 429 | rate_limit_error | rate_limited | Too many requests this minute. Wait Retry-After seconds (also in details.retry_after; details.limit is your per-minute limit). Nothing consumed. |
| 500 | api_error | internal_error | Unexpected error on our side. Safe to retry once. |
| 503 | api_error | upstream_unavailable, api_disabled, db_unavailable | Temporarily unavailable; words were refunded. Retry with backoff. db_unavailable (a brief database issue) sends Retry-After: 5 and is safe to retry. |
| 503 | api_error | cancel_failed | Dashboard only: deleting the account couldn't cancel its paid API subscription. Nothing was deleted; try again later. |
Rate limits & headers
Limits are applied per key, per minute, and depend on your plan.
| Header | Description |
|---|---|
X-Request-Id | Unique id of the request (req_…), on every response. Include it when you contact support. |
X-RateLimit-Limit | Requests allowed per minute for this key (humanize and detect only). |
X-RateLimit-Remaining | Requests left in the current minute. |
X-RateLimit-Reset | Unix time (seconds) when the minute window resets. |
X-Words-Remaining | Words left in your current period after this request (successful humanize and detect responses). |
Retry-After | On 429 and on 503 db_unavailable: seconds to wait before retrying. |
Limits by plan
| Plan | Requests / minute | Max words / request | Live keys |
|---|---|---|---|
| Free trial | 5 | 1,000 | 1 |
| Starter | 30 | 1,500 | 2 |
| Growth | 60 | 3,000 | 5 |
| Scale | 120 | 5,000 | 10 |
| Enterprise | 240 | 6,000 | Unlimited |
Test keys have their own budget of 60 requests per minute. When you exceed a limit you get 429 rate_limited with a Retry-After header; nothing is consumed. details repeats the wait and your limit:
{
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "Too many requests, please slow down.",
"request_id": "req_13112eb48b2eb75ea30f53e2",
"details": { "limit": 30, "retry_after": 44 }
}
}Best practices
A few habits that make integrations fast and resilient.
- Use generous timeouts. Long texts can take a while, detect more than humanize; set your client timeout to at least 180 seconds and split long documents. If a request times out on your side, check
GET /usagebefore retrying: the request may still have completed. - Retry 429 and 503 with backoff. Respect
Retry-Afterwhen present, otherwise back off exponentially. Don't retry 4xx errors other than 429. - Keep keys server-side. Proxy requests through your backend; rotate keys by creating a new one before revoking the old one.
- Use test keys in CI. Same response shape, zero words used.
- Split long documents into chunks under your plan's words-per-request limit, ideally on paragraph boundaries.
- Log
request_id. It lets us find a specific request quickly if you contact support.
const API_BASE = "https://api.humanizertool.io/v1";
const API_KEY = process.env.HUMANIZER_API_KEY;
async function humanize(text, attempt = 0) {
const res = await fetch(`${API_BASE}/humanize`, {
method: "POST",
headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ text }),
signal: AbortSignal.timeout(180_000),
});
if ((res.status === 429 || res.status === 503) && attempt < 3) {
const wait = Number(res.headers.get("Retry-After")) || 2 ** attempt;
await new Promise((r) => setTimeout(r, wait * 1000));
return humanize(text, attempt + 1);
}
if (!res.ok) throw new Error((await res.json()).error.message);
return res.json();
}
// const { output } = await humanize("Our new release makes it simple to manage invoices and share reports with your team.");Changelog
v1 — public launch
- POST /v1/humanize and POST /v1/detect with one shared word pool.
- GET /v1/me and GET /v1/usage for account and quota checks.
- Live and test keys, scopes, optional expiry, revocation.
- Rate-limit and quota headers on every metered response.
- OpenAPI 3.1 spec at /v1/openapi.json.
API reference
Generated from /v1/openapi.json. Every endpoint, parameter and schema, with request examples.