GET/v1/jobs
List job summaries
- Auth
- API key
- Success
- HTTP 200
- Query
- limit, cursor, status, preset, strategy, source, created_after, created_before
3 possible error codes
UNAUTHENTICATED, VALIDATION_ERROR, INTERNAL_ERROR
Developers
Create humanization jobs, stream their progress, read the detector estimate and manage style profiles from your own code. The API is part of the Premium plan.
Base URL
https://turnithuman.com/v1Auth
Bearer API key
Progress
Server-sent events
Format
JSON, OpenAPI 3.1
A job takes your text through three layers: mechanical fixes, a dedicated humanizer pass, and checks for meaning and detector risk. You create a job, follow it until it is done, and read the output with its scores and warnings.
The detector readout is an independent multi-detector estimate with no guarantee attached. The per-detector numbers are most likely the detector provider's own emulations, not those vendors' verdicts. They point the other way from result: result is an AI likelihood from 0 to 100, while each per-detector value is 0, 50 or 100, where 100 means that model would read the text as human, 50 means unsure and 0 means it would flag the text as AI; human is their average, and a value the detector did not return is left out. No TurnItHuman endpoint calls Turnitin, Turnitin offers no public AI-detection API, and no result here is a Turnitin score or a prediction of one.
Send your key as a bearer token: Authorization: Bearer th_live_ followed by 32 characters. Create keys in the app under API keys; the full key is shown once, and the list shows only its prefix. Revocation takes effect immediately.
Keys are for server-to-server use: browsers are blocked by CORS. Premium includes 2 active keys. To rotate, create a new key, deploy it, then revoke the old one. Keys are revoked if the account leaves Premium.
POST /v1/jobs takes the text plus an optional title, preset, strategy, style_id, options and metadata. It returns 202 with the job id. The job object reports status, stage, the output with its scores, the detector estimate, the meaning check, warnings, and when the text and the history row expire.
fidelity is the meaning check of the delivered text. Its status is ok when nothing changed that is worth a look, review when review_items lists something (each item has a kind, the before and after words, and a message such as Number changed: "almost two hundred dollars" became "$190"), and skipped when no humanized text was checked. The humanized text is delivered and charged either way.
strategy is humanize_only (the default) or style_replicate_humanized, which humanizes in the style of a registered style profile given as style_id. strategy_effective tells you what actually ran, for example humanize_only when the style profile could not be used. Code blocks and reference lists are never sent to the humanizer. Protected terms, locked ranges (offsets in the text as sent), inline code, citations, quotes, numbers and links are compared with your original after the humanizer pass, and any that look changed are listed in fidelity.review_items.
To keep formatting, send input_format: "markdown" with the text as Markdown. Headings, bullet and numbered lists (one level of nesting), block quotes, fenced code, links (http, https and mailto), bold and italic are kept, and anything else is read as plain text. Only the prose is humanized: headings, code and short list items stay as you wrote them. The job returns input.markdown and output.markdown beside the plain input.text and output.text, and it counts and bills the words without Markdown syntax or link URLs, so formatting never costs words. When the humanizer rewords the text of a link or the link has to move to other words, fidelity.review_items says so (link_text_changed, link_moved). options.locked_ranges must be empty with markdown. Without input_format the text is plain, as before.
academic_essayEssays and discussion posts. Formal register; citations are checked against your original.academic_reportReports and lab write-ups. Formal tone; figures and citations are checked against your original.article_blogPosts and articles that read like a person wrote them.marketingLanding pages, newsletters and ad copy with a natural voice.businessMemos, client emails and internal docs. Figures are checked against your original.storyScripts and fiction. Defaults to the More Human strength.cover_letterLetters in a formal register. Numbers and dates are checked; add names as protected terms to have them checked too.legalLetters and memos in a formal register. Add terms of art as protected terms to have them checked.generalAny English text. A balanced humanizer pass.GET /v1/jobs/{id}/events is a text/event-stream of job.status, job.progress, job.error and job.done events, with a ping comment every 15 seconds. Reconnect with Last-Event-ID to replay only the events you missed. After job.done or job.error the server closes the stream, and a reconnect past the last event answers 204, so stop reconnecting then. If you receive only pings for 60 seconds, read the job once with GET /v1/jobs/{id}.
If you cannot hold a stream open, poll GET /v1/jobs/{id}?fields=summary with If-None-Match every 5 seconds at most.
POST /v1/detect checks text you already have without humanizing it. It costs 10 percent of the words checked, rounded up to the nearest 10, with a minimum of 10. Scores under 50 read as human, 50 to 60 as possible AI, and over 60 as likely AI. Texts under 200 words give unreliable estimates. A check that is still running replays as pending with the same idempotency key.
A style profile is a sample of 100 to 2,000 words of your own writing. The response includes a voice profile summary. With replicate_upstream, the sample is also registered with the humanizer provider; its upstream status stays pending until registration finishes, and style_replicate_humanized jobs need it to be done. Without registration a profile returns its voice profile summary but does not change jobs.
A job reserves the billable words of its input, rounded up to the nearest 10, when it is created. A failed job returns every reserved word. A job cancelled after the humanizer pass started is charged. Humanize again charges the words of the humanized result. A job that returns your own text, because the humanizer could not process it, releases its words and reports them as input.refunded_words. GET /v1/usage reports your balance and ledger for a period.
Each key may send 60 requests a minute with bursts of up to 30 in 10 seconds, run 5 jobs at once and create 60 jobs an hour. A 429 carries Retry-After in seconds.
Send an Idempotency-Key header (16 to 128 characters; a UUID works) on POST /v1/jobs, /v1/jobs/{id}/rehumanize, /v1/detect and /v1/styles. Retrying with the same key and body returns the original result instead of creating a second job; the same key with a different body returns 422 IDEMPOTENCY_CONFLICT.
The API is versioned in the path (/v1). New fields, endpoints, enum values and warning codes are added without a version change, so ignore fields you do not know. Breaking changes ship as a new version with notice.
The terms of service and content policy apply to every API call. Use the API only on text you have the right to edit, never to disguise authorship of work that must be your own, and never to produce deceptive content.
Replace $KEY with a Premium API key. uuidgen supplies a fresh idempotency key; the ids are examples.
curl -sS -X POST https://turnithuman.com/v1/jobs \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"text": "The institute was officially established in 1989, marking a pivotal moment ...",
"title": "Institute history",
"preset": "academic_essay",
"options": { "strength": "Balanced", "protected_terms": ["Foster Care Act"] },
"metadata": { "client_ref": "essay-42" }
}'
# 202 {"id":"01J8Q0Z2K7M4N6P8R1S3T5V7W9","object":"job","status":"queued", ...}curl -sS -N https://turnithuman.com/v1/jobs/01J8Q0Z2K7M4N6P8R1S3T5V7W9/events \
-H "Authorization: Bearer $KEY" \
-H "Accept: text/event-stream" \
-H "Last-Event-ID: 0"
# id: 1
# event: job.status
# data: {"job_id":"01J8Q0Z2K7M4N6P8R1S3T5V7W9","status":"preprocessing","stage":"s0_normalize","at":"..."}curl -sS "https://turnithuman.com/v1/jobs/01J8Q0Z2K7M4N6P8R1S3T5V7W9?fields=summary" \
-H "Authorization: Bearer $KEY" \
-H 'If-None-Match: W/"humanizing:1758463403105:summary"'
# 200 with the summary job, or 304 when unchanged. Repeat every 5 s until done, failed or cancelled.curl -sS -X POST https://turnithuman.com/v1/detect \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"text": "At least two hundred words of the text you want checked ..."}'curl -sS -X POST https://turnithuman.com/v1/styles \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name": "My essay voice", "sample": "At least one hundred words of your own writing ...", "replicate_upstream": true}'const BASE = "https://turnithuman.com/v1";
const headers = {
Authorization: `Bearer ${process.env.TURNITHUMAN_API_KEY}`,
"Content-Type": "application/json",
};
async function call(path: string, init: RequestInit = {}) {
const res = await fetch(`${BASE}${path}`, { ...init, headers: { ...headers, ...init.headers } });
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message}`);
}
return res.json();
}
const created = await call("/jobs", {
method: "POST",
headers: { "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({ text, preset: "article_blog" }),
});
let job = created;
while (!["done", "failed", "cancelled"].includes(job.status)) {
await new Promise((resolve) => setTimeout(resolve, 5000));
job = await call(`/jobs/${created.id}`);
}
console.log(job.output?.text, job.detector?.final?.result, job.warnings);Errors use one envelope,{ "error": { "code", "message", "details" } }, with an X-Request-Id header.
Branch on the code, never on the message. The message column is the text the app shows.
AUTH_TOKEN_INVALID400
This link is not valid. Request a new one.
Do not retry; change the request
BAD_REQUEST400
We could not read that request. Reload and try again.
Do not retry; change the request
MAGIC_LINK_INVALID400
This sign-in link is not valid. Request a new one.
Do not retry; change the request
UNKNOWN_PIPELINE_VERSION400
That pipeline version is not available.
Do not retry; change the request
API_KEY_REVOKED401
This API key was revoked.
Do not retry; change the request
INVALID_API_KEY401
This API key is not valid.
Do not retry; change the request
INVALID_CREDENTIALS401
That email and password do not match.
Do not retry; change the request
RECENT_AUTH_REQUIRED401
For your security, confirm it is you. We sent a link to your email.
Do not retry; change the request
SESSION_EXPIRED401
Your session ended. Sign in again.
Do not retry; change the request
UNAUTHENTICATED401
Sign in to continue.
Do not retry; change the request
INSUFFICIENT_WORDS402
You need {required} words and have {remaining} left until {date}.
Do not retry; change the request
ACCOUNT_SUSPENDED403
This account is suspended. Contact hello@turnithuman.com.
Do not retry; change the request
CSRF_ORIGIN_MISMATCH403
This request came from an unexpected page. Reload and try again.
Do not retry; change the request
EMAIL_DOMAIN_BLOCKED403
Enter a valid email address.
Do not retry; change the request
FEATURE_DISABLED403
This feature is not available right now.
Do not retry; change the request
FORBIDDEN403
This action is not available with an API key.
Do not retry; change the request
PLAN_REQUIRED403
{feature} is available on {plan} and up.
Do not retry; change the request
TRIAL_ALREADY_USED403
Your free trial has been used. Choose a plan to continue.
Do not retry; change the request
TURNSTILE_FAILED403
Verification failed. Reload the page and try again.
Do not retry; change the request
JOB_NOT_FOUND404
We could not find that job.
Do not retry; change the request
NOT_FOUND404
We could not find that page or item.
Do not retry; change the request
STYLE_NOT_FOUND404
We could not find that style profile.
Do not retry; change the request
ACCOUNT_DELETION_PENDING409
This account is scheduled for deletion. Sign in and choose Cancel deletion to continue.
Do not retry; change the request
ALREADY_REPLAYED409
This event was already replayed.
Do not retry; change the request
ALREADY_SUBSCRIBED409
You already have a plan. Use Manage billing to change it.
Do not retry; change the request
API_KEY_LIMIT_REACHED409
Premium includes 2 keys. Revoke one to create another.
Do not retry; change the request
AUTH_TOKEN_USED409
This link was already used. Request a new one.
Do not retry; change the request
EMAIL_IN_USE409
That email already belongs to another account.
Do not retry; change the request
JOB_NOT_CANCELLABLE409
This job has already finished.
Do not retry; change the request
JOB_NOT_DELETABLE409
Cancel the job before deleting it.
Do not retry; change the request
JOB_NOT_REHUMANIZABLE409
This job cannot be run again. Its text may have expired.
Do not retry; change the request
JOB_TERMINAL409
This job has already finished.
Do not retry; change the request
MAGIC_LINK_USED409
This sign-in link was already used. Request a new one.
Do not retry; change the request
REFUND_NOT_AVAILABLE409
There are no settled words left to refund on this job, or the account has no open cycle.
Do not retry; change the request
REHUMANIZE_LIMIT_REACHED409
Limit of 2 re-runs per job. Start a new job to continue.
Do not retry; change the request
STYLE_LIMIT_REACHED409
Your plan includes {limit} style profiles.
Do not retry; change the request
AUTH_TOKEN_EXPIRED410
This link expired. Request a new one.
Do not retry; change the request
GONE410
This item is no longer available.
Do not retry; change the request
MAGIC_LINK_EXPIRED410
This sign-in link expired. Request a new one.
Do not retry; change the request
TEXTS_EXPIRED410
The text for this job has expired and can no longer be downloaded.
Do not retry; change the request
FILE_TOO_LARGE413
Files are limited to 5 MB.
Do not retry; change the request
PAYLOAD_TOO_LARGE413
The request is too large. Split the text.
Do not retry; change the request
FILE_TYPE_UNSUPPORTED415
Upload a .txt or .docx file.
Do not retry; change the request
UNSUPPORTED_MEDIA_TYPE415
We could not read that request format.
Do not retry; change the request
CONTENT_REFUSED422
We cannot process this text under our content policy. Your words were not used.
Do not retry; change the request
FILE_NO_TEXT422
We could not find readable text in this file.
Do not retry; change the request
FILE_UNREADABLE422
We could not open this file. Save it again as .docx or .txt and try again.
Do not retry; change the request
IDEMPOTENCY_CONFLICT422
This request conflicts with one you already sent. Reload to see the result.
Do not retry; change the request
IDEMPOTENCY_KEY_INVALID422
Something went wrong sending this request. Reload and try again.
Do not retry; change the request
INVALID_EMAIL422
Enter a valid email address.
Do not retry; change the request
STYLE_SAMPLE_TOO_LONG422
Samples are limited to 2,000 words.
Do not retry; change the request
STYLE_SAMPLE_TOO_SHORT422
Paste at least 100 words of your own writing.
Do not retry; change the request
TEXT_TOO_LONG422
This text is {word_count} words. Your plan allows {max_words} per job.
Do not retry; change the request
TEXT_TOO_SHORT422
Add at least 50 words.
Do not retry; change the request
UNSUPPORTED_LANGUAGE422
TurnItHuman works on English text for now.
Do not retry; change the request
VALIDATION_ERROR422
Check the highlighted fields and try again.
Do not retry; change the request
ACCOUNT_LOCKED429
Too many failed sign-in attempts. Wait 15 minutes or reset your password.
Retry after the Retry-After header
CONCURRENCY_LIMIT429
You have {running} jobs running. Wait for one to finish.
Retry after the Retry-After header
RATE_LIMITED429
Too many requests. Try again in {seconds} seconds.
Retry after the Retry-After header
INTERNAL_ERROR500
Something went wrong on our side. Your words were refunded if a job had started. Reference: {requestId}.
Retry with backoff
WORKFLOW_CREATE_FAILED500
We could not start your job.
Retry with backoff
BILLING_UNAVAILABLE502
We could not reach billing. Try again in a minute.
Retry with backoff
UPSTREAM_UNAVAILABLE502
The humanizer service did not respond. Your words were refunded.
Retry with backoff
BILLING_NOT_CONFIGURED503
Billing is not enabled on this environment.
Retry after the Retry-After header
DAILY_CAPACITY_REACHED503
We reached today's capacity for new jobs. They open again by 00:00 UTC. Your text is still here.
Retry after the Retry-After header
DB_UNAVAILABLE503
We could not reach our database. Try again in a minute. Your words were not used.
Retry after the Retry-After header
EMAIL_SEND_FAILED503
We could not send the email right now. Try again in a few minutes.
Retry after the Retry-After header
INTAKE_PAUSED503
New jobs are paused for a few minutes. Your text is still here.
Retry after the Retry-After header
PIPELINE_HALTED503
New jobs are paused while we fix a problem. Your text is still here.
Retry after the Retry-After header
QUEUE_UNAVAILABLE503
We could not start your job. Your words were not used. Try again.
Retry after the Retry-After header
SIGNUPS_PAUSED503
New sign-ups are paused for a short time. Existing accounts work as usual.
Retry after the Retry-After header
UPSTREAM_BUSY503
The checker is busy. Try again in {seconds} seconds.
Retry after the Retry-After header
UPSTREAM_CREDITS_EXHAUSTED503
The humanizer service is temporarily unavailable. Your words were not used.
Retry after the Retry-After header
UPSTREAM_TIMEOUT504
The check took too long. Your words were refunded.
Do not retry; change the request
Every endpoint an API key can call, generated from the OpenAPI document. Account settings, billing and sign-in are app-only and need a signed-in session.
GET/v1/jobs
List job summaries
UNAUTHENTICATED, VALIDATION_ERROR, INTERNAL_ERROR
POST/v1/jobs
Create a humanization job (plain text, or Markdown with input_format markdown)
UNAUTHENTICATED, INSUFFICIENT_WORDS, PLAN_REQUIRED, TRIAL_ALREADY_USED, TURNSTILE_FAILED, FEATURE_DISABLED, STYLE_NOT_FOUND, VALIDATION_ERROR, IDEMPOTENCY_CONFLICT, TEXT_TOO_SHORT, TEXT_TOO_LONG, UNSUPPORTED_LANGUAGE, CONCURRENCY_LIMIT, RATE_LIMITED, INTERNAL_ERROR, UPSTREAM_CREDITS_EXHAUSTED, INTAKE_PAUSED, DAILY_CAPACITY_REACHED
GET/v1/jobs/{id}
Read a job, including texts
UNAUTHENTICATED, JOB_NOT_FOUND, INTERNAL_ERROR
DELETE/v1/jobs/{id}
Delete a finished job and its texts
UNAUTHENTICATED, JOB_NOT_FOUND, JOB_NOT_DELETABLE, INTERNAL_ERROR
GET/v1/jobs/{id}/events
Server-sent events for a job
UNAUTHENTICATED, JOB_NOT_FOUND, INTERNAL_ERROR
POST/v1/jobs/{id}/rehumanize
Run the humanizer again (new charge)
UNAUTHENTICATED, INSUFFICIENT_WORDS, PLAN_REQUIRED, JOB_NOT_FOUND, JOB_NOT_REHUMANIZABLE, REHUMANIZE_LIMIT_REACHED, VALIDATION_ERROR, IDEMPOTENCY_CONFLICT, CONCURRENCY_LIMIT, RATE_LIMITED, INTERNAL_ERROR, UPSTREAM_CREDITS_EXHAUSTED, INTAKE_PAUSED, DAILY_CAPACITY_REACHED
POST/v1/jobs/{id}/cancel
Cancel a running job
UNAUTHENTICATED, JOB_NOT_FOUND, JOB_NOT_CANCELLABLE, INTERNAL_ERROR
GET/v1/detect
List standalone detector runs
UNAUTHENTICATED, VALIDATION_ERROR, INTERNAL_ERROR
POST/v1/detect
Standalone detector check
UNAUTHENTICATED, INSUFFICIENT_WORDS, PLAN_REQUIRED, VALIDATION_ERROR, TEXT_TOO_SHORT, TEXT_TOO_LONG, UNSUPPORTED_LANGUAGE, IDEMPOTENCY_CONFLICT, RATE_LIMITED, INTERNAL_ERROR, UPSTREAM_UNAVAILABLE, UPSTREAM_BUSY, UPSTREAM_CREDITS_EXHAUSTED, DAILY_CAPACITY_REACHED, UPSTREAM_TIMEOUT
GET/v1/detect/{id}
Read one detector run
UNAUTHENTICATED, NOT_FOUND, INTERNAL_ERROR
GET/v1/styles
List style profiles
UNAUTHENTICATED, PLAN_REQUIRED, INTERNAL_ERROR
POST/v1/styles
Create a style profile
UNAUTHENTICATED, PLAN_REQUIRED, STYLE_LIMIT_REACHED, VALIDATION_ERROR, STYLE_SAMPLE_TOO_SHORT, STYLE_SAMPLE_TOO_LONG, UNSUPPORTED_LANGUAGE, IDEMPOTENCY_CONFLICT, INTERNAL_ERROR
GET/v1/styles/{id}
Read one style with its sample
UNAUTHENTICATED, STYLE_NOT_FOUND, INTERNAL_ERROR
PUT/v1/styles/{id}
Replace name and/or sample, or request style replication
UNAUTHENTICATED, PLAN_REQUIRED, STYLE_NOT_FOUND, VALIDATION_ERROR, STYLE_SAMPLE_TOO_SHORT, STYLE_SAMPLE_TOO_LONG, UNSUPPORTED_LANGUAGE, INTERNAL_ERROR
DELETE/v1/styles/{id}
Soft-delete a style
UNAUTHENTICATED, STYLE_NOT_FOUND, INTERNAL_ERROR
GET/v1/me
Current user, plan, limits, features
UNAUTHENTICATED, INTERNAL_ERROR
GET/v1/usage
Word usage and ledger for a period
UNAUTHENTICATED, NOT_FOUND, VALIDATION_ERROR, INTERNAL_ERROR
GET/v1/health
Service health; `probe` is th-probe-ok while D1 answers and no cron heartbeat is stale
INTERNAL_ERROR
GET/v1/openapi.json
This document
INTERNAL_ERROR