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 + SSEQuick Start
Three steps to your first humanized output. Text must be at least 20 words or the API returns 400 INVALID_TEXT.
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.
Send a request
POST to /v1/humanize with Authorization, Content-Type, optional Idempotency-Key, and a JSON body.
Handle the response
On 200, read humanized_text. On error, match the code field in the problem+json body — never parse detail.
Request
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
{
"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.
Authorization: Bearer htai_live_...Request headers
| Header | Required | Description |
|---|---|---|
| Authorization | Yes* | Bearer htai_live_…. *Not required on GET /v1/health. |
| Content-Type | Yes on POST/PUT | application/json |
| Idempotency-Key | Optional | humanize + detect only. 1–128 chars of A–Z a–z 0–9 _ - : . Becomes the response id when set. |
| Accept | Optional | Defaults 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.
| Scope | Grants |
|---|---|
| humanize | POST /v1/humanize. Using a custom tone here only needs this scope — not tones. |
| detect | POST /v1/detect |
| tones | POST/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_ALLOWEDwhen 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/humanizeand/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_wordsnever changes the bill. Rewrite length can vary; there is no guaranteed length band. - Failed generation refunds the reservation (
refunded: trueon 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_ofmust 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 acceptedbest_ofdoes not change the ultra multiplier; the plan gate above still applies.mode: "aggressive"defaults to 3× whenbest_ofis omitted; an explicit value overrides it.
| Plan | plan.key | Words / mo | Max / request | RPM | Concurrent | Keys | Custom tones | best_of max |
|---|---|---|---|---|---|---|---|---|
| Growth | api_growth | 1,000,000 | 5,000 | 60 | 10 | 5 | 10 | 3 |
| Scale | api_scale | 3,000,000 | 10,000 | 200 | 30 | 15 | 50 | 5 |
Both plans include all humanize modes, streaming, and detection. Read live remaining balance from GET /v1/usageor any success response's words_remaining.
Endpoints
| Method | Path | Auth | Scope | Description |
|---|---|---|---|---|
| POST | /v1/humanize | Bearer | humanize | Humanize text (JSON or SSE) |
| POST | /v1/detect | Bearer | detect | AI detection scoring |
| GET | /v1/usage | Bearer | — | Plan, balance, key info |
| HEAD | /v1/usage | Bearer | — | Same auth as GET; empty body |
| GET | /v1/health | None | — | Health check (200/503) |
| HEAD | /v1/health | None | — | Same as GET; empty body |
| POST | /v1/tones | Bearer | tones | Create custom tone |
| GET | /v1/tones | Bearer | tones | List custom tones |
| GET | /v1/tones/:id | Bearer | tones | Get one tone |
| PUT | /v1/tones/:id | Bearer | tones | Update tone fields |
| DELETE | /v1/tones/:id | Bearer | tones | Delete tone |
https://api.humanizethisai.com/v1/humanizehumanize scopePrimary 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
{
"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
}| Field | Type | Required | Description |
|---|---|---|---|
| text | string | Yes | AI-generated text to humanize. Minimum 20 words. Maximum = your plan's max_words_per_request (5,000 Growth / 10,000 Scale). |
| mode | string | No | Default: standard. One of: standard, academic, enhanced, ultra, aggressive |
| tone | string | No | Default: 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. |
| readability | string | No | Writing level. One of: high-school, college, graduate, doctorate. Default: college. Unrecognized values fall back to college. |
| freeze_words | string[] | No | Words/phrases preserved unchanged. Max 100 entries, 100 characters each — overflow returns 400 INVALID_FREEZE_WORDS. HTML is stripped; blank entries are dropped. |
| best_of | integer | No | 1–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. |
| language | string | No | Output language (ISO 639-1): en, es, fr, de, it, pt, ru, zh, ja, ko, tr, ar. Default: en. Unrecognized → en. |
| stream | boolean | No | Enable 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)
{
"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
}| Field | Type | Required | Description |
|---|---|---|---|
| object | string | — | Always "humanization" |
| id | string | — | Canonical request id. Equals X-Request-Id. Equals your Idempotency-Key when one was sent. |
| humanized_text | string | — | Rewritten output |
| input_words | integer | — | Words counted in the request text |
| output_words | integer | — | Words in humanized_text |
| mode | string | — | Mode that ran |
| tone | string | — | Echo of the tone you sent (built-in name, custom name, or UUID). |
| language | string | — | Resolved output language |
| best_of_used | integer | — | Effective fan-out after mode floors |
| is_custom_tone | boolean | — | true when a custom tone was applied |
| tone_id | string | — | Custom tone UUID. Present only when is_custom_tone is true. |
| tone_name | string | — | Custom tone name. Present only when is_custom_tone is true. |
| words_charged | integer | — | input_words × multiplier |
| words_remaining | integer | — | Balance after this charge |
| credits_low | boolean | — | JSON success: included only when true after reservation. SSE done: always present as a boolean (true or false). |
| replayed | boolean | — | Present only on idempotent replays (also X-Idempotent-Replay: true) |
Streaming Response (SSE events)
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.
https://api.humanizethisai.com/v1/detectdetect scope · Growth & ScaleAI detection scoring. Returns AI/human probabilities, a verdict, and signal details. Charged on input_words (multiplier 1).
Request Body
{
"text": "string (required) — text to score, min 20 words"
}| Field | Type | Required | Description |
|---|---|---|---|
| text | string | Yes | Text to analyze. Minimum 20 words. Same max-per-request cap as humanize. |
Response (200 OK)
{
"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
}| Field | Type | Required | Description |
|---|---|---|---|
| object | string | — | Always "detection" |
| id | string | — | Canonical request id (= X-Request-Id / Idempotency-Key) |
| ai_score | integer | — | 0–100 estimated AI probability |
| human_score | integer | — | 0–100 estimated human probability |
| confidence | string | — | Model confidence band (e.g. high, medium, low) |
| verdict | string | — | Machine-stable label (e.g. likely_ai) |
| label | string | — | Human-readable verdict |
| signals | array | — | [{ signal, severity, detail }] supporting evidence |
| sentences | array | — | Optional per-sentence breakdown when available |
| summary | string | — | Short natural-language summary |
| input_words | integer | — | Words counted in the request |
| words_charged | integer | — | Usually equals input_words |
| words_remaining | integer | — | Balance after this charge |
https://api.humanizethisai.com/v1/usageCurrent 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)
{
"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"]
}
}| Field | Type | Required | Description |
|---|---|---|---|
| plan.key | string | — | Wire plan id: "api_growth" or "api_scale". |
| plan.slug | string | — | Friendly short id without the api_ prefix (growth / scale). |
| plan.name | string | — | Display name (Growth / Scale) |
| plan.max_words_per_request | integer | — | Hard cap enforced on humanize/detect |
| usage.words_remaining | integer | — | Monthly remaining + top-up remaining |
| usage.topup_words_remaining | integer | — | Purchased top-up balance still available |
| key.scopes | string[] | — | Scopes on this API key |
https://api.humanizethisai.com/v1/healthno authPublic 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
{
"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 presentCustom Tonestones scopeCreate 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.
| Method | Path | Description |
|---|---|---|
| POST | /v1/tones | Create a custom tone |
| GET | /v1/tones | List your custom tones |
| GET | /v1/tones/:id | Fetch one tone by UUID |
| PUT | /v1/tones/:id | Update a tone (any subset of fields) |
| DELETE | /v1/tones/:id | Delete a tone |
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes on create | 1–50 characters. Unique per account (case-insensitive). Built-in tone names are reserved → 400 TONE_NAME_RESERVED. Duplicates → 409 TONE_NAME_CONFLICT. |
| instructions | string | Yes on create | 1–500 characters. How the model should rewrite the text. |
| description | string | No | Up to 500 characters. Optional human note. |
| example | string | No | Up to 500 characters. Optional sample of the target voice. |
Create — Request
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)
{
"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)
{
"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)
{
"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
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)
{
"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)
{
"object": "tone",
"id": "0b9c8d7e-6f5a-4b3c-2d1e-0f9a8b7c6d5e",
"deleted": true,
"words_charged": 0,
"words_remaining": 957850
}Use on humanize
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.
X-Idempotent-Replay: true and replayed: true. Replays are always JSON, even if the original used stream: true.409 IDEMPOTENCY_KEY_CONFLICT. Mint a new key for every distinct job.1–128 characters of A-Z a-z 0-9 _ - : . returns 400 INVALID_IDEMPOTENCY_KEY. It is never silently replaced.# 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
- Read
code(and HTTP status). 429 RATE_LIMITED→ sleepRetry-Afterseconds, then retry the same request (same Idempotency-Key is safe).429 CONCURRENT_LIMIT→ wait for an in-flight request to finish, then retry.402 INSUFFICIENT_WORDS→ top up or upgrade; do not retry until balance coverswords_required. Empty balance is never 429.409 IDEMPOTENCY_KEY_CONFLICT/RETRY_REQUIRED→ mint a new Idempotency-Key.5xx/ network timeout → retry with the same Idempotency-Key.- Client disconnect / cancel:
/v1/detect→499 CLIENT_CANCELLED(not charged / refunded)./v1/humanizecancels are returned as500 AI_GENERATION_FAILEDwith a refund when a reservation was taken — do not expect 499 on humanize. - Other AI failures:
/v1/humanizetimeouts and provider failures →500 AI_GENERATION_FAILED;/v1/detectnon-cancel AI failures →500 DETECTION_FAILED. 4xxvalidation errors → fix the payload; do not blind-retry.
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
}| Code | HTTP | Description |
|---|---|---|
| MISSING_API_KEY | 401 | No Authorization header provided |
| INVALID_API_KEY_FORMAT | 401 | Key doesn't start with htai_live_ |
| INVALID_KEY_FORMAT | 401 | Alias of INVALID_API_KEY_FORMAT — malformed key shape (hash length / prefix) from key validation RPC |
| INVALID_KEY | 401 | Key not found or has been revoked |
| KEY_EXPIRED | 401 | Key has expired (rotated) |
| UNKNOWN_ERROR | 401 | Rare auth-path failure when key validation did not return a more specific code. Retry once; if it persists, rotate the key or contact support. |
| UNAUTHORIZED | 403 | Caller is not authorized for this operation |
| NO_ACTIVE_SUBSCRIPTION | 403 | No active API plan on the account (gateway auth path) |
| NO_SUBSCRIPTION | 403 | No developer-surface subscription row for this user (billing reservation path; distinct from NO_ACTIVE_SUBSCRIPTION) |
| PLAN_NOT_FOUND | 403 | Subscription plan_key has no matching plan_configs row |
| BANNED | 403 | Account is suspended |
| RISK_BLOCKED | 403 | Account restricted due to unusual activity |
| TURNSTILE_REQUIRED | 403 | Additional verification required before the request can proceed |
| SCOPE_DENIED | 403 | API key missing a required permission (humanize, detect, or tones). Enable it under Permissions or create a new key. |
| MODE_NOT_AVAILABLE | 403 | Requested mode not included in current plan |
| ENDPOINT_NOT_AVAILABLE | 403 | Endpoint not included in current plan |
| STREAMING_NOT_AVAILABLE | 403 | SSE streaming not included in current plan |
| DETECTION_NOT_AVAILABLE | 403 | AI detection not included in current plan |
| IP_NOT_ALLOWED | 403 | Request IP not in key's allowlist |
| RATE_LIMITED | 429 | Rate limit exceeded — respect Retry-After and retry |
| CONCURRENT_LIMIT | 429 | Too many in-flight humanize/detect requests for your plan concurrent cap. Wait for one to finish, then retry. |
| MISSING_TEXT | 400 | No text field in request body |
| INVALID_TEXT | 400 | Text failed validation: under 20 words, empty after normalize, disallowed characters, or blocked injection/code patterns |
| INVALID_JSON | 400 | Request body is not valid JSON |
| TEXT_TOO_LONG | 400 | Text exceeds plan's maximum words per request (see GET /v1/usage → plan.max_words_per_request) |
| INVALID_MODE | 400 | Unknown humanization mode |
| INVALID_TONE | 400 | Tone is malformed. Use a built-in name, the exact custom tone name, or the tone UUID. |
| INVALID_BEST_OF | 400 | best_of is not an integer between 1 and 5 |
| INVALID_ID | 400 | Tone id path parameter must be a UUID |
| NO_FIELDS | 400 | Tone PUT body has no updatable fields (name and/or instructions required) |
| INVALID_WORD_COUNT | 400 | Word count argument to billing reservation is missing or ≤ 0 |
| INVALID_MULTIPLIER | 400 | Cost multiplier argument to billing reservation is missing or < 1 |
| BEST_OF_NOT_AVAILABLE | 403 | best_of above 3 requires the Scale plan |
| LEGACY_PROMPT_REMOVED | 400 | Legacy "prompt" field is no longer supported — use "text" |
| INVALID_NAME | 400 | Custom tone name missing or not 1–50 characters after sanitization |
| INVALID_INSTRUCTIONS | 400 | Custom tone instructions missing or not 1–500 characters after sanitization |
| INJECTION_DETECTED | 400 | Custom tone field contains blocked injection/code patterns |
| INVALID_IDEMPOTENCY_KEY | 400 | Idempotency-Key header is malformed (must be 1–128 chars of A-Z a-z 0-9 _ - : .) |
| IDEMPOTENCY_KEY_CONFLICT | 409 | Idempotency-Key was already used under a different API key, user, or endpoint. Use a unique key per job. |
| RETRY_REQUIRED | 409 | This Idempotency-Key was already consumed but no stored result is available (expired or original attempt refunded). Mint a new key and retry. |
| INVALID_FREEZE_WORDS | 400 | freeze_words is not an array, has more than 100 entries, or an entry exceeds 100 characters. |
| TONE_NAME_CONFLICT | 409 | A custom tone with that name already exists for this account (case-insensitive). |
| TONE_NAME_RESERVED | 400 | Custom tone name collides with a built-in tone (casual, formal, professional, academic, creative). |
| INSUFFICIENT_WORDS | 402 | Not enough word balance — carries words_required and words_remaining extension fields |
| RESERVATION_FAILED | 500 | Word reservation could not complete — retry. Not a payment or balance error. |
| TONE_NOT_FOUND | 404 | No custom tone matches that name or UUID. List tones with GET /v1/tones or copy from the Custom Tones page. |
| TONE_LIMIT_REACHED | 403 | Custom tone quota for the current plan has been reached. |
| NOT_FOUND | 404 | Unknown path |
| METHOD_NOT_ALLOWED | 405 | Path exists; HTTP method not supported (see Allow header) |
| INVALID_CONTENT_TYPE | 415 | Content-Type must be application/json on write requests |
| PAYLOAD_TOO_LARGE | 413 | Request body exceeds the 128 KiB JSON body limit |
| VALIDATION_FAILED | 400 | Request failed validation (generic developer validation path) |
| CLIENT_CANCELLED | 499 | Detect-only on developer API: client disconnected before /v1/detect completed — not charged / refunded. /v1/humanize cancels collapse to AI_GENERATION_FAILED. |
| AI_TIMEOUT | 504 | Not emitted on developer /v1/humanize (collapsed to AI_GENERATION_FAILED 500). Detect timeouts collapse to DETECTION_FAILED. Present in internal classification / consumer paths. |
| AI_UNAVAILABLE | 503 | All AI providers unavailable — not charged / refunded |
| ABORTED | 504 | Request aborted mid-flight (AbortError) before a typed AI failure was classified |
| INTERNAL_ERROR | 500 | Unexpected server error |
| AI_GENERATION_FAILED | 500 | Humanize AI processing failed on /v1/humanize (timeouts, cancels, and generic AI failures collapse here) — not charged / refunded |
| DETECTION_FAILED | 500 | Detection 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.
| Plan | Primary RPM | Concurrent | Keys | Secondary RPM | Tone writes / min |
|---|---|---|---|---|---|
| Growth | 60 | 10 | 5 | 60 (usage + tones shared) | 10 (POST/PUT/DELETE) |
| Scale | 200 | 30 | 15 | 60 (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# 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.
# 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."
}'