API Reference

Documentation

Complete developer API reference — authentication, scopes, limits, every endpoint, idempotency, errors, rate limits, and copy-paste examples that actually run.

https://api.humanizethisai.comBase URL · HTTPS only · JSON + SSE

Quick Start

Three steps to your first humanized output. Text must be at least 20 words or the API returns 400 INVALID_TEXT.

1

Get your API key

Subscribe to an API plan, open the Developer dashboard, and create a key (htai_live_…). Copy it once — we only store a hash.

2

Send a request

POST to /v1/humanize with Authorization, Content-Type, optional Idempotency-Key, and a JSON body.

3

Handle the response

On 200, read humanized_text. On error, match the code field in the problem+json body — never parse detail.

Request

bash
curl -X POST https://api.humanizethisai.com/v1/humanize \
  -H "Authorization: Bearer htai_live_your_key_here" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -d '{
    "text": "Artificial intelligence has revolutionized the way we interact with technology. Machine learning models can now generate coherent text that closely resembles human writing across many domains.",
    "mode": "standard",
    "tone": "casual"
  }'

More runnable samples (curl, Python, Node) in Code Examples.

Response

json
{
  "object": "humanization",
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "humanized_text": "AI has completely changed how we deal with tech day to day. Models can now write text that feels a lot like something a person would produce, across plenty of different fields.",
  "input_words": 26,
  "output_words": 36,
  "mode": "standard",
  "tone": "casual",
  "language": "en",
  "best_of_used": 1,
  "is_custom_tone": false,
  "words_charged": 26,
  "words_remaining": 999974
}

Authentication

Every authenticated endpoint requires a Bearer token in the Authorization header. GET /v1/health is the only public route.

HTTP Header
Authorization: Bearer htai_live_...

Request headers

HeaderRequiredDescription
AuthorizationYes*Bearer htai_live_…. *Not required on GET /v1/health.
Content-TypeYes on POST/PUTapplication/json
Idempotency-KeyOptionalhumanize + detect only. 1–128 chars of A–Z a–z 0–9 _ - : . Becomes the response id when set.
AcceptOptionalDefaults to application/json. Streaming is controlled by body stream: true (response is text/event-stream).

Scopes

Each key carries a set of permissions. Missing a required scope returns 403 SCOPE_DENIED. Configure scopes when you create or edit a key in the Developer dashboard.

ScopeGrants
humanizePOST /v1/humanize. Using a custom tone here only needs this scope — not tones.
detectPOST /v1/detect
tonesPOST/GET/PUT/DELETE /v1/tones. Create and manage custom tones.

Key management

  • •Generation: Create keys from the Developer dashboard. Every live key starts with htai_live_.
  • •Security: The full key is shown once at creation. We store a SHA-256 hash — even we cannot recover the original. Treat it like a password.
  • •Rotation: Rotating a key puts the old one in a grace period before revocation so you can roll clients without downtime.
  • •Limits: Growth plans get 5 keys, Scale gets 15. Optional IP allowlists return 403 IP_NOT_ALLOWED when the caller is outside the list.

Limits & Billing

Hard limits enforced on every request. Hitting one returns a stable error code — see Error Codes. JSON request bodies are capped at 128 KiB; over → 413 PAYLOAD_TOO_LARGE.

Word count

  • Minimum: 20 words on /v1/humanize and /v1/detect. Shorter text → 400 INVALID_TEXT.
  • Maximum per request: plan cap (5,000 Growth / 10,000 Scale). Over → 400 TEXT_TOO_LONG.

How you are charged

  • words_charged = input_words × multiplier
  • Charging is input-only — output_words never changes the bill. Rewrite length can vary; there is no guaranteed length band.
  • Failed generation refunds the reservation (refunded: true on stream errors).
  • Tone CRUD never consumes words (words_charged: 0).
  • Balance too low for the charge → 402 INSUFFICIENT_WORDS (not 429).

Multiplier rules

  • Plan gate first: best_of must be an integer 1–5. Values above 3 require Scale → 403 BEST_OF_NOT_AVAILABLE. This check runs for every mode, including ultra.
  • Default multiplier is best_of (default 1) for standard, academic, and enhanced.
  • mode: "ultra" always runs a fixed 3-candidate self-audit pipeline and always bills 3×. A accepted best_of does not change the ultra multiplier; the plan gate above still applies.
  • mode: "aggressive" defaults to 3× when best_of is omitted; an explicit value overrides it.
Planplan.keyWords / moMax / requestRPMConcurrentKeysCustom tonesbest_of max
Growthapi_growth1,000,0005,00060105103
Scaleapi_scale3,000,00010,0002003015505

Both plans include all humanize modes, streaming, and detection. Read live remaining balance from GET /v1/usageor any success response's words_remaining.

Endpoints

MethodPathAuthScopeDescription
POST/v1/humanizeBearerhumanizeHumanize text (JSON or SSE)
POST/v1/detectBearerdetectAI detection scoring
GET/v1/usageBearer—Plan, balance, key info
HEAD/v1/usageBearer—Same auth as GET; empty body
GET/v1/healthNone—Health check (200/503)
HEAD/v1/healthNone—Same as GET; empty body
POST/v1/tonesBearertonesCreate custom tone
GET/v1/tonesBearertonesList custom tones
GET/v1/tones/:idBearertonesGet one tone
PUT/v1/tones/:idBearertonesUpdate tone fields
DELETE/v1/tones/:idBearertonesDelete tone
POSThttps://api.humanizethisai.com/v1/humanizehumanize scope

Primary endpoint. Rewrites AI-generated text. Default response is JSON; set stream: true for SSE. Typical completion is a few seconds; use stream: true for a progressive UI. Latency is reported as the X-Latency-Ms response header and as latency_ms on the SSE done event.

Rewrite length can vary from the input. words_charged is always input_words × multiplier— never based on output_words.

Request Body

json
{
  "text": "string (required) — AI text to humanize, min 20 words",
  "mode": "standard | academic | enhanced | ultra | aggressive",
  "tone": "casual | formal | professional | academic | creative | <custom name|uuid>",
  "readability": "high-school | college | graduate | doctorate",
  "freeze_words": ["BrandName", "TechnicalTerm"],
  "best_of": 1,
  "language": "en",
  "stream": false
}
FieldTypeRequiredDescription
textstringYesAI-generated text to humanize. Minimum 20 words. Maximum = your plan's max_words_per_request (5,000 Growth / 10,000 Scale).
modestringNoDefault: standard. One of: standard, academic, enhanced, ultra, aggressive
tonestringNoDefault: casual. Built-in: casual, formal, professional, academic, creative. Custom: exact name or UUID from GET /v1/tones. Malformed tone → 400 INVALID_TONE; unknown custom name/UUID → 404 TONE_NOT_FOUND.
readabilitystringNoWriting level. One of: high-school, college, graduate, doctorate. Default: college. Unrecognized values fall back to college.
freeze_wordsstring[]NoWords/phrases preserved unchanged. Max 100 entries, 100 characters each — overflow returns 400 INVALID_FREEZE_WORDS. HTML is stripped; blank entries are dropped.
best_ofintegerNo1–5. Default 1. Values above 3 require Scale (403 BEST_OF_NOT_AVAILABLE) on every mode including ultra. For standard/academic/enhanced multiplies charge by N. Ultra always bills 3× via its fixed pipeline; aggressive defaults to 3× when omitted.
languagestringNoOutput language (ISO 639-1): en, es, fr, de, it, pt, ru, zh, ja, ko, tr, ar. Default: en. Unrecognized → en.
streambooleanNoEnable SSE streaming. Default: false. Response Content-Type becomes text/event-stream.

Billing & best_of

Charged as words_charged = input_words × multiplier. See Limits & Billing for ultra / aggressive floors and Scale gating. Output length never affects the charge.

Response (200 OK)

json
{
  "object": "humanization",
  "id": "8f2a1c6e-3b4d-4f0a-9c12-7e5d8a1b2c3d",
  "humanized_text": "Your humanized output...",
  "input_words": 26,
  "output_words": 36,
  "mode": "standard",
  "tone": "casual",
  "language": "en",
  "best_of_used": 1,
  "is_custom_tone": false,
  "words_charged": 26,
  "words_remaining": 999974
}
FieldTypeRequiredDescription
objectstring—Always "humanization"
idstring—Canonical request id. Equals X-Request-Id. Equals your Idempotency-Key when one was sent.
humanized_textstring—Rewritten output
input_wordsinteger—Words counted in the request text
output_wordsinteger—Words in humanized_text
modestring—Mode that ran
tonestring—Echo of the tone you sent (built-in name, custom name, or UUID).
languagestring—Resolved output language
best_of_usedinteger—Effective fan-out after mode floors
is_custom_toneboolean—true when a custom tone was applied
tone_idstring—Custom tone UUID. Present only when is_custom_tone is true.
tone_namestring—Custom tone name. Present only when is_custom_tone is true.
words_chargedinteger—input_words × multiplier
words_remaininginteger—Balance after this charge
credits_lowboolean—JSON success: included only when true after reservation. SSE done: always present as a boolean (true or false).
replayedboolean—Present only on idempotent replays (also X-Idempotent-Replay: true)

Streaming Response (SSE events)

text/event-stream
event: meta
data: {"id":"8f2a1c6e-3b4d-4f0a-9c12-7e5d8a1b2c3d","mode":"standard","tone":"casual","language":"en","input_words":26}

event: chunk
data: {"text":"AI has completely "}

event: chunk
data: {"text":"changed how we "}

event: chunk
data: {"text":"deal with tech day to day."}

event: done
data: {"object":"humanization","id":"8f2a1c6e-3b4d-4f0a-9c12-7e5d8a1b2c3d","input_words":26,"output_words":36,"words_charged":26,"words_remaining":999974,"credits_low":false,"language":"en","best_of_used":1,"is_custom_tone":false,"latency_ms":1342}

# If generation fails mid-stream, a single error event is emitted instead of done:
event: error
data: {"code":"AI_GENERATION_FAILED","message":"Failed to generate humanized text. You were not charged.","refunded":true}

meta — Request metadata: id, mode, tone, language, input_words.

chunk — Partial text: { text }. Repeats until complete.

done — Terminal success with object, id, word counts, words_charged / words_remaining, best_of_used, is_custom_tone, latency_ms, and credits_low as a boolean that is always present on SSE (true or false).

error — Terminal failure: { code, message, refunded }. Emitted instead of done. refunded: true means you were not charged.

Replays of a prior streamed request always return JSON, not SSE — see Idempotency.

POSThttps://api.humanizethisai.com/v1/detectdetect scope · Growth & Scale

AI detection scoring. Returns AI/human probabilities, a verdict, and signal details. Charged on input_words (multiplier 1).

Request Body

json
{
  "text": "string (required) — text to score, min 20 words"
}
FieldTypeRequiredDescription
textstringYesText to analyze. Minimum 20 words. Same max-per-request cap as humanize.

Response (200 OK)

json
{
  "object": "detection",
  "id": "c4d5e6f7-8a9b-4c0d-1e2f-3a4b5c6d7e8f",
  "ai_score": 72,
  "human_score": 28,
  "confidence": "high",
  "verdict": "likely_ai",
  "label": "Likely AI",
  "signals": [
    { "signal": "Low lexical variance", "severity": "high", "detail": "Repetitive phrasing across sentences" }
  ],
  "sentences": [],
  "summary": "Text shows strong markers of AI generation.",
  "input_words": 26,
  "words_charged": 26,
  "words_remaining": 999948
}
FieldTypeRequiredDescription
objectstring—Always "detection"
idstring—Canonical request id (= X-Request-Id / Idempotency-Key)
ai_scoreinteger—0–100 estimated AI probability
human_scoreinteger—0–100 estimated human probability
confidencestring—Model confidence band (e.g. high, medium, low)
verdictstring—Machine-stable label (e.g. likely_ai)
labelstring—Human-readable verdict
signalsarray—[{ signal, severity, detail }] supporting evidence
sentencesarray—Optional per-sentence breakdown when available
summarystring—Short natural-language summary
input_wordsinteger—Words counted in the request
words_chargedinteger—Usually equals input_words
words_remaininginteger—Balance after this charge
GETHEADhttps://api.humanizethisai.com/v1/usage

Current plan, period usage, optional daily stats, and the calling key's id / name / scopes. No words charged. HEAD uses the same auth and returns the same status with an empty body. Use this to drive dashboards and pre-flight checks. If the stats RPC fails, stats is null and degraded: true is set — plan/usage/key still return.

Response (200 OK)

json
{
  "plan": {
    "key": "api_growth",
    "slug": "growth",
    "name": "Growth",
    "words_per_month": 1000000,
    "max_words_per_request": 5000,
    "requests_per_minute": 60,
    "allowed_modes": ["standard", "academic", "enhanced", "ultra", "aggressive"],
    "streaming_enabled": true,
    "detection_enabled": true
  },
  "usage": {
    "words_used": 42150,
    "words_remaining": 957850,
    "words_limit": 1000000,
    "topup_words_remaining": 0,
    "requests_this_period": 1203,
    "period_end": "2026-05-01T00:00:00Z"
  },
  "stats": {
    "summary": {
      "total_requests": 1203,
      "total_words_charged": 42150,
      "total_input_words": 41000,
      "total_output_words": 40200,
      "avg_latency_ms": 1380,
      "error_count": 4,
      "success_rate": 99.67
    },
    "daily": [
      { "date": "2026-04-07", "requests": 47, "words_charged": 1650, "errors": 0 }
    ]
  },
  "key": {
    "id": "0b9c8d7e-6f5a-4b3c-2d1e-0f9a8b7c6d5e",
    "name": "Production Key",
    "scopes": ["humanize", "detect", "tones"]
  }
}
FieldTypeRequiredDescription
plan.keystring—Wire plan id: "api_growth" or "api_scale".
plan.slugstring—Friendly short id without the api_ prefix (growth / scale).
plan.namestring—Display name (Growth / Scale)
plan.max_words_per_requestinteger—Hard cap enforced on humanize/detect
usage.words_remaininginteger—Monthly remaining + top-up remaining
usage.topup_words_remaininginteger—Purchased top-up balance still available
key.scopesstring[]—Scopes on this API key
GETHEADhttps://api.humanizethisai.com/v1/healthno auth

Public health check. No authentication. Per-IP in-memory limit of 60 requests/minute (not your plan RPM). Returns 200 when database and AI are both healthy; otherwise 503 with status: "degraded" or "unhealthy". Over limit → 429 application/problem+json with code: RATE_LIMITED and Retry-After. HEAD returns the same status with an empty body.

Response

json
{
  "status": "healthy",
  "timestamp": "2026-04-07T14:30:00Z",
  "version": "1.0.0"
}
# status is one of: healthy | degraded | unhealthy
# HTTP 200 when all services healthy; HTTP 503 otherwise
# optional "services" object only when an admin secret header is present
Custom Tonestones scope

Create reusable rewrite voices, then pass the tone name or UUID as tone on /v1/humanize. Listing and writing tones requires the tones scope. Using a custom tone on humanize only needs humanize. Write ops (POST/PUT/DELETE) are capped at 10 requests/minute. These calls never consume words — words_charged is always 0. Caps: 10 tones on Growth, 50 on Scale. Path :id must be a UUID → otherwise 400 INVALID_ID. PUT with no updatable fields → 400 NO_FIELDS. On every tones response the top-level id is the request correlation id (same as X-Request-Id). The tone UUID is always tone.id — never use the envelope id as the tone id for GET/PUT/DELETE.

MethodPathDescription
POST/v1/tonesCreate a custom tone
GET/v1/tonesList your custom tones
GET/v1/tones/:idFetch one tone by UUID
PUT/v1/tones/:idUpdate a tone (any subset of fields)
DELETE/v1/tones/:idDelete a tone
FieldTypeRequiredDescription
namestringYes on create1–50 characters. Unique per account (case-insensitive). Built-in tone names are reserved → 400 TONE_NAME_RESERVED. Duplicates → 409 TONE_NAME_CONFLICT.
instructionsstringYes on create1–500 characters. How the model should rewrite the text.
descriptionstringNoUp to 500 characters. Optional human note.
examplestringNoUp to 500 characters. Optional sample of the target voice.

Create — Request

bash
curl -X POST https://api.humanizethisai.com/v1/tones \
  -H "Authorization: Bearer htai_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Newsletter voice",
    "instructions": "Warm, concise, second-person. Short sentences.",
    "description": "Used for our weekly digest",
    "example": "Hey there — here is what you missed this week."
  }'

Create — Response (200 OK)

json
{
  "object": "tone",
  "id": "0b9c8d7e-6f5a-4b3c-2d1e-0f9a8b7c6d5e",
  "tone": {
    "id": "9d7c1b2a-4e5f-4a6b-8c9d-0e1f2a3b4c5d",
    "name": "Newsletter voice",
    "instructions": "Warm, concise, second-person. Short sentences.",
    "description": "Used for our weekly digest",
    "example": "Hey there — here is what you missed this week.",
    "created_at": "2026-04-07T14:30:00Z",
    "updated_at": "2026-04-07T14:30:00Z"
  },
  "words_charged": 0,
  "words_remaining": 957850
}

List — Response (200 OK)

json
{
  "object": "list",
  "id": "0b9c8d7e-6f5a-4b3c-2d1e-0f9a8b7c6d5e",
  "tones": [
    {
      "id": "9d7c1b2a-4e5f-4a6b-8c9d-0e1f2a3b4c5d",
      "name": "Newsletter voice",
      "instructions": "Warm, concise, second-person. Short sentences.",
      "description": "Used for our weekly digest",
      "example": "Hey there — here is what you missed this week.",
      "created_at": "2026-04-07T14:30:00Z",
      "updated_at": "2026-04-07T14:30:00Z"
    }
  ],
  "words_charged": 0,
  "words_remaining": 957850
}

Get one — Response (200 OK)

json
{
  "object": "tone",
  "id": "0b9c8d7e-6f5a-4b3c-2d1e-0f9a8b7c6d5e",
  "tone": {
    "id": "9d7c1b2a-4e5f-4a6b-8c9d-0e1f2a3b4c5d",
    "name": "Newsletter voice",
    "instructions": "Warm, concise, second-person. Short sentences.",
    "description": "Used for our weekly digest",
    "example": "Hey there — here is what you missed this week.",
    "created_at": "2026-04-07T14:30:00Z",
    "updated_at": "2026-04-07T14:30:00Z"
  },
  "words_charged": 0,
  "words_remaining": 957850
}

Update — Request

bash
curl -X PUT https://api.humanizethisai.com/v1/tones/9d7c1b2a-4e5f-4a6b-8c9d-0e1f2a3b4c5d \
  -H "Authorization: Bearer htai_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Updated note for the weekly digest voice"
  }'

Update — Response (200 OK)

json
{
  "object": "tone",
  "id": "0b9c8d7e-6f5a-4b3c-2d1e-0f9a8b7c6d5e",
  "tone": {
    "id": "9d7c1b2a-4e5f-4a6b-8c9d-0e1f2a3b4c5d",
    "name": "Newsletter voice",
    "instructions": "Warm, concise, second-person. Short sentences.",
    "description": "Updated note for the weekly digest voice",
    "example": "Hey there — here is what you missed this week.",
    "created_at": "2026-04-07T14:30:00Z",
    "updated_at": "2026-04-07T15:12:00Z"
  },
  "words_charged": 0,
  "words_remaining": 957850
}

Delete — Response (200 OK)

json
{
  "object": "tone",
  "id": "0b9c8d7e-6f5a-4b3c-2d1e-0f9a8b7c6d5e",
  "deleted": true,
  "words_charged": 0,
  "words_remaining": 957850
}

Use on humanize

bash
curl -X POST https://api.humanizethisai.com/v1/humanize \
  -H "Authorization: Bearer htai_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "text": "Artificial intelligence has revolutionized the way we interact with technology. Machine learning models can now generate coherent text that closely resembles human writing across many domains.",
    "mode": "standard",
    "tone": "Newsletter voice"
  }'
# tone may also be the UUID from GET /v1/tones.
# Success sets is_custom_tone: true and adds tone_id + tone_name. tone echoes what you sent.

Idempotency

/v1/humanize and /v1/detect accept an optional Idempotency-Key request header. Send the same key to safely retry after a timeout or flaky network without being charged twice. The key becomes the response id and the X-Request-Id header.

•First use — the request is processed normally and the result is stored against the key for that API key + endpoint.
•Replay — reusing the same key with the same API key and endpoint returns the stored result without re-running or re-charging. The response carries X-Idempotent-Replay: true and replayed: true. Replays are always JSON, even if the original used stream: true.
•Replay scope — keys are scoped to API key + endpoint. Reusing your own key after a successful charge returns the original reservation/result (idempotent replay). Prefer a new key for every distinct job so retries stay unambiguous. UUIDs are fine.
•Conflict — the same key string used under a different API key, user, or endpoint returns 409 IDEMPOTENCY_KEY_CONFLICT. Mint a new key for every distinct job.
•Malformed — a key outside 1–128 characters of A-Z a-z 0-9 _ - : . returns 400 INVALID_IDEMPOTENCY_KEY. It is never silently replaced.
•Absent — without the header the request is not deduplicated; each call is independent and gets a server-generated id.
bash
# Retry the exact same request safely — reuse the key.
curl -X POST https://api.humanizethisai.com/v1/humanize \
  -H "Authorization: Bearer htai_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-4815-humanize-01" \
  -d '{
    "text": "Artificial intelligence has revolutionized the way we interact with technology. Machine learning models can now generate coherent text that closely resembles human writing across many domains.",
    "mode": "standard",
    "tone": "casual"
  }'

# Same key again → stored result, no second charge:
HTTP/1.1 200 OK
X-Idempotent-Replay: true
X-Request-Id: order-4815-humanize-01
Content-Type: application/json

# Body includes "replayed": true and the original humanized_text.

# New job → new key (UUID recommended):
# Idempotency-Key: $(uuidgen)

Error Codes

Every error is an RFC 7807 application/problem+json document. Match on the code field (stable UPPER_SNAKE) or the full type URL — never parse the human-readable detail. request_id equals the X-Request-Id response header. Unknown routes and wrong methods on /v1/* also return problem+json. Some codes add structured extension members — INSUFFICIENT_WORDS carries words_required and words_remaining. Each code has a stable doc page at /errors.

Client handling recipe

  1. Read code (and HTTP status).
  2. 429 RATE_LIMITED → sleep Retry-After seconds, then retry the same request (same Idempotency-Key is safe). 429 CONCURRENT_LIMIT → wait for an in-flight request to finish, then retry.
  3. 402 INSUFFICIENT_WORDS → top up or upgrade; do not retry until balance covers words_required. Empty balance is never 429.
  4. 409 IDEMPOTENCY_KEY_CONFLICT / RETRY_REQUIRED → mint a new Idempotency-Key.
  5. 5xx / network timeout → retry with the same Idempotency-Key.
  6. Client disconnect / cancel: /v1/detect → 499 CLIENT_CANCELLED (not charged / refunded). /v1/humanize cancels are returned as 500 AI_GENERATION_FAILED with a refund when a reservation was taken — do not expect 499 on humanize.
  7. Other AI failures: /v1/humanize timeouts and provider failures → 500 AI_GENERATION_FAILED; /v1/detect non-cancel AI failures → 500 DETECTION_FAILED.
  8. 4xx validation errors → fix the payload; do not blind-retry.
json
HTTP/1.1 402 Payment Required
Content-Type: application/problem+json

{
  "type": "https://humanizethisai.com/errors/insufficient-words",
  "title": "Insufficient word balance",
  "status": 402,
  "detail": "Insufficient word balance. Need 600, have 50. This request uses mode=\"ultra\", which multiplies word cost by 3. Purchase top-ups or upgrade your plan.",
  "instance": "/v1/humanize",
  "code": "INSUFFICIENT_WORDS",
  "request_id": "8f2a1c6e-3b4d-4f0a-9c12-7e5d8a1b2c3d",
  "words_required": 600,
  "words_remaining": 50
}
CodeHTTPDescription
MISSING_API_KEY401No Authorization header provided
INVALID_API_KEY_FORMAT401Key doesn't start with htai_live_
INVALID_KEY_FORMAT401Alias of INVALID_API_KEY_FORMAT — malformed key shape (hash length / prefix) from key validation RPC
INVALID_KEY401Key not found or has been revoked
KEY_EXPIRED401Key has expired (rotated)
UNKNOWN_ERROR401Rare auth-path failure when key validation did not return a more specific code. Retry once; if it persists, rotate the key or contact support.
UNAUTHORIZED403Caller is not authorized for this operation
NO_ACTIVE_SUBSCRIPTION403No active API plan on the account (gateway auth path)
NO_SUBSCRIPTION403No developer-surface subscription row for this user (billing reservation path; distinct from NO_ACTIVE_SUBSCRIPTION)
PLAN_NOT_FOUND403Subscription plan_key has no matching plan_configs row
BANNED403Account is suspended
RISK_BLOCKED403Account restricted due to unusual activity
TURNSTILE_REQUIRED403Additional verification required before the request can proceed
SCOPE_DENIED403API key missing a required permission (humanize, detect, or tones). Enable it under Permissions or create a new key.
MODE_NOT_AVAILABLE403Requested mode not included in current plan
ENDPOINT_NOT_AVAILABLE403Endpoint not included in current plan
STREAMING_NOT_AVAILABLE403SSE streaming not included in current plan
DETECTION_NOT_AVAILABLE403AI detection not included in current plan
IP_NOT_ALLOWED403Request IP not in key's allowlist
RATE_LIMITED429Rate limit exceeded — respect Retry-After and retry
CONCURRENT_LIMIT429Too many in-flight humanize/detect requests for your plan concurrent cap. Wait for one to finish, then retry.
MISSING_TEXT400No text field in request body
INVALID_TEXT400Text failed validation: under 20 words, empty after normalize, disallowed characters, or blocked injection/code patterns
INVALID_JSON400Request body is not valid JSON
TEXT_TOO_LONG400Text exceeds plan's maximum words per request (see GET /v1/usage → plan.max_words_per_request)
INVALID_MODE400Unknown humanization mode
INVALID_TONE400Tone is malformed. Use a built-in name, the exact custom tone name, or the tone UUID.
INVALID_BEST_OF400best_of is not an integer between 1 and 5
INVALID_ID400Tone id path parameter must be a UUID
NO_FIELDS400Tone PUT body has no updatable fields (name and/or instructions required)
INVALID_WORD_COUNT400Word count argument to billing reservation is missing or ≤ 0
INVALID_MULTIPLIER400Cost multiplier argument to billing reservation is missing or < 1
BEST_OF_NOT_AVAILABLE403best_of above 3 requires the Scale plan
LEGACY_PROMPT_REMOVED400Legacy "prompt" field is no longer supported — use "text"
INVALID_NAME400Custom tone name missing or not 1–50 characters after sanitization
INVALID_INSTRUCTIONS400Custom tone instructions missing or not 1–500 characters after sanitization
INJECTION_DETECTED400Custom tone field contains blocked injection/code patterns
INVALID_IDEMPOTENCY_KEY400Idempotency-Key header is malformed (must be 1–128 chars of A-Z a-z 0-9 _ - : .)
IDEMPOTENCY_KEY_CONFLICT409Idempotency-Key was already used under a different API key, user, or endpoint. Use a unique key per job.
RETRY_REQUIRED409This Idempotency-Key was already consumed but no stored result is available (expired or original attempt refunded). Mint a new key and retry.
INVALID_FREEZE_WORDS400freeze_words is not an array, has more than 100 entries, or an entry exceeds 100 characters.
TONE_NAME_CONFLICT409A custom tone with that name already exists for this account (case-insensitive).
TONE_NAME_RESERVED400Custom tone name collides with a built-in tone (casual, formal, professional, academic, creative).
INSUFFICIENT_WORDS402Not enough word balance — carries words_required and words_remaining extension fields
RESERVATION_FAILED500Word reservation could not complete — retry. Not a payment or balance error.
TONE_NOT_FOUND404No custom tone matches that name or UUID. List tones with GET /v1/tones or copy from the Custom Tones page.
TONE_LIMIT_REACHED403Custom tone quota for the current plan has been reached.
NOT_FOUND404Unknown path
METHOD_NOT_ALLOWED405Path exists; HTTP method not supported (see Allow header)
INVALID_CONTENT_TYPE415Content-Type must be application/json on write requests
PAYLOAD_TOO_LARGE413Request body exceeds the 128 KiB JSON body limit
VALIDATION_FAILED400Request failed validation (generic developer validation path)
CLIENT_CANCELLED499Detect-only on developer API: client disconnected before /v1/detect completed — not charged / refunded. /v1/humanize cancels collapse to AI_GENERATION_FAILED.
AI_TIMEOUT504Not emitted on developer /v1/humanize (collapsed to AI_GENERATION_FAILED 500). Detect timeouts collapse to DETECTION_FAILED. Present in internal classification / consumer paths.
AI_UNAVAILABLE503All AI providers unavailable — not charged / refunded
ABORTED504Request aborted mid-flight (AbortError) before a typed AI failure was classified
INTERNAL_ERROR500Unexpected server error
AI_GENERATION_FAILED500Humanize AI processing failed on /v1/humanize (timeouts, cancels, and generic AI failures collapse here) — not charged / refunded
DETECTION_FAILED500Detection processing failed on /v1/detect (non-cancel AI failures, including timeouts, collapse here) — not charged / refunded

Rate Limits

Primary endpoints (/v1/humanize, /v1/detect) use your plan RPM and concurrent caps. Secondary endpoints (/v1/usage, /v1/tones) share a separate 60 requests/minute per user bucket so they do not consume primary budget. Tone writes add a further 10/min cap on POST/PUT/DELETE.

PlanPrimary RPMConcurrentKeysSecondary RPMTone writes / min
Growth6010560 (usage + tones shared)10 (POST/PUT/DELETE)
Scale200301560 (usage + tones shared)10 (POST/PUT/DELETE)

Hitting the concurrent cap returns 429 CONCURRENT_LIMIT (separate from RPM RATE_LIMITED). Empty word balance is 402 INSUFFICIENT_WORDS, not 429.

Response headers

X-RateLimit-* appear on /v1/humanize and /v1/detect (success and error). Secondary routes (/v1/usage, /v1/tones) include them on 429 only. They are not guaranteed on every endpoint or every status.

X-RateLimit-LimitRPM cap for the bucket that applied (plan primary RPM, or 60 on secondary).
X-RateLimit-RemainingRequests remaining in the current 60-second window for that bucket.
X-RateLimit-ResetSeconds until the current window resets (a delta, not a Unix timestamp). 0 while you are within the limit.
Retry-AfterSeconds to wait before retrying. Set when the RPM bucket is exhausted (RATE_LIMITED). Not guaranteed on CONCURRENT_LIMIT.
X-Request-IdCanonical request id. Equals JSON success id and problem request_id. Equals your Idempotency-Key when you sent one.
X-Idempotent-Replaytrue when this response is a stored replay (also body.replayed: true).
Content-Typeapplication/json | application/problem+json | text/event-stream
HTTP
# Example rate-limited response
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 30
Retry-After: 30

{
  "type": "https://humanizethisai.com/errors/rate-limited",
  "title": "Rate limit exceeded",
  "status": 429,
  "detail": "Rate limit exceeded. Slow down.",
  "instance": "/v1/humanize",
  "code": "RATE_LIMITED",
  "request_id": "8f2a1c6e-3b4d-4f0a-9c12-7e5d8a1b2c3d"
}

Code Examples

Runnable examples with authentication, idempotency keys, and error handling. Every sample uses at least 20 words so it will not fail validation.

cURL
# Humanize (JSON)
curl -X POST https://api.humanizethisai.com/v1/humanize \
  -H "Authorization: Bearer htai_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "text": "Artificial intelligence has revolutionized the way we interact with technology. Machine learning models can now generate coherent text that closely resembles human writing across many domains.",
    "mode": "standard",
    "tone": "casual",
    "freeze_words": ["HumanizeThisAI", "GPTZero"],
    "best_of": 1,
    "language": "en",
    "stream": false
  }'

# Humanize (SSE stream)
curl -N -X POST https://api.humanizethisai.com/v1/humanize \
  -H "Authorization: Bearer htai_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "text": "Artificial intelligence has revolutionized the way we interact with technology. Machine learning models can now generate coherent text that closely resembles human writing across many domains.",
    "mode": "standard",
    "tone": "casual",
    "stream": true
  }'

# Detect
curl -X POST https://api.humanizethisai.com/v1/detect \
  -H "Authorization: Bearer htai_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "text": "Artificial intelligence has revolutionized the way we interact with technology. Machine learning models can now generate coherent text that closely resembles human writing across many domains." }'

# Usage
curl -X GET https://api.humanizethisai.com/v1/usage \
  -H "Authorization: Bearer htai_live_..."

# Create a custom tone
curl -X POST https://api.humanizethisai.com/v1/tones \
  -H "Authorization: Bearer htai_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Newsletter voice",
    "instructions": "Warm, concise, second-person. Short sentences."
  }'