Skip to content

API reference

Send text to one endpoint and get a verdict back as JSON. This page lists the request, the response, three working examples and every error you can get.

Authentication

Every request carries a key in a header: Authorization: Bearer <key>. Create a key from the dashboard after you sign in. API access comes with the Education plan today, and with paid plans once billing starts.

A key is a signed token. Its scope, its expiry date and a random id are written inside the key, and the signature shows nobody changed them. Nothing about the key is stored on the server, which has two consequences. The dashboard shows a key once, and cannot show it again. And you cannot revoke a key yourself: it works until it expires, 90 days after you create it. If a key leaks, stop using it and create a new one. To have a leaked key blocked before it expires, email hello@olive.is; it is added to a block list at the next deploy.

POST /api/v1/detect

Send JSON with a text field. It is required and must be at least 50 words; shorter text is refused before it reaches the detector.

An optional sensitivity field sets how much of the text must read as AI-written before it is flagged. "standard" flags above 6%. It is the default, and a missing or unrecognized value is read as standard. "accusation-safe" flags above 15%, and misses more AI-edited text than the default. "sensitive" flags above 2%; in the default model’s release test it flagged 1 of 600 human-written papers the model had never been trained on, and the 95% ceiling on that false-flag rate is 0.788%. No setting turns an AI verdict into Human.

An optional model field picks which model reads the text. Leave it out and you get "standard", the default, and the request behaves exactly as it did before the field existed. "standard" (fewer false flags): Flagged none of 600 human-written papers. Called 20 of 176 humanized AI papers human. In a newer test of one humanizer tool, it called 231 of 255 rewritten long papers human. "humanizer" (catches more humanizers): Called 8 of 176 humanized AI papers human. In the same newer test, it called 219 of 255 rewritten long papers human. Flagged 2 of 600 human-written papers the default cleared. "students" (for student essays): Called none of 4,816 student essays AI. Caught 39 of 41 humanized student essays. In a newer test of one humanizer tool, it called 218 of 255 rewritten long papers human. Flagged 1 of 600 human-written papers the default cleared. Those counts are under the default verdict mode. The humanizer and students models run at sensitivity "standard" only: any other sensitivity with either is refused with 422. When you name a model, the response carries it back in a model field, and run names the run that read the text.

An optional mode field takes "verdict" (the default) or "edit", which returns more detail for each sentence.

Read the abstained field first. When it is true, the text was under 50 words, the verdict is "Unknown", and nothing else in the response is a reading. Otherwise verdict is "Human", "Mixed" or "AI". fraction_ai is the share of words in sentences labeled AI; it is not a confidence score. word_count is the number of words the detector counted. sentences is an ordered list of {start, end, label, label_3, p_ai, text}: the sentence as the detector split it, its label, and p_ai, an internal model score for that sentence.

To show highlights, render sentences[].text in order. The start and end numbers point into a normalized copy of your text, so they will not line up if you use them to slice your original string.

The response fields

The response contains these fields and no others: verdict, abstained, fraction_ai, fraction_human, fraction_assisted, confidence, word_count, sensitivity, estimate, version, sentences (each {start, end, label, label_3, p_ai, text}), segments, run and source. fraction_human, fraction_assisted, confidence, estimate, version and label_3 appear only when the detector returns them. model appears only when the request named a model.

confidence and p_ai are internal model scores. Neither is a measured probability that the text was AI-written, that a person used AI, or that the verdict is right. Do not show them to a student as a probability.

The detection service also computes internal values, such as its decision threshold. Those are not published, because they are the numbers someone would tune rewritten text against. A field reaches you only if it is listed above, so a change inside the service cannot add one.

Every reading carries source "our detector" and run, the name of the detector run that produced it. Store run next to any reading you keep. The live run changes from time to time, and the run name is how you know which one produced a stored result.

curl

curl -X POST https://olive.is/api/v1/detect \
  -H "Authorization: Bearer $HUMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "paste at least 50 words of the text you want checked here", "sensitivity": "standard"}'

Python

import os
import requests

response = requests.post(
    "https://olive.is/api/v1/detect",
    headers={"Authorization": f"Bearer {os.environ['HUMAN_API_KEY']}"},
    json={"text": text, "sensitivity": "standard"},
    timeout=120,
)
result = response.json()

if result["abstained"]:
    print("Not scored: paste at least 50 words.")
else:
    print(result["verdict"], result["fraction_ai"], result["run"])

Node

const response = await fetch("https://olive.is/api/v1/detect", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.HUMAN_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ text, sensitivity: "standard" }),
});

const result = await response.json();
if (result.abstained) {
  console.log("Not scored: paste at least 50 words.");
} else {
  console.log(result.verdict, result.fraction_ai, result.run);
}

Timeouts

The first check after a quiet spell can take longer, while the detector starts. The server gives up on the detector within 120 seconds and answers with an error, so set your own timeout to at least that. The server does not retry for you.

Errors

Every error is a JSON body with an error field.

400 {"error":"bad_request"}: the body was not JSON, or text was missing or not a string.

401: the key was refused. The error field gives the reason in one word: missing, malformed, wrong-key-id, bad-signature, expired, revoked or wrong-scope. Fix the key rather than retrying. wrong-scope means the key was made for a different service.

413 {"error":"too_long","maxChars":200000}: the text is over 200,000 characters. That limit is the same on every plan.

422 {"error":"too_short","minWords":50}: the text is under 50 words. Send a longer passage.

422 {"error":"unknown_model","message":...}: model was not "standard" or "humanizer". Nothing was scored, and the default model is not used in its place.

422 {"error":"sensitivity_unavailable","message":...}: the humanizer model was asked for at a sensitivity other than "standard". Leave sensitivity out or send "standard".

429 {"error":"rate_limited","resetAt":<unix ms>}: an hourly limit below was reached. The retry-after header gives the wait in seconds.

502 or 503 {"error":"not_scored"}: the detector could not be reached, or this deployment has no detector set up. You get "not scored", never a score from anywhere else, and the server does not retry. Retry when you choose to. The same holds for each model on its own: a request for the humanizer model that cannot be served is not scored, and is never answered by the default model under its name.

Rate limits

API usage is not metered or billed yet. No word count is kept against a key, no invoice is produced, and the plan allowances on the pricing page are not enforced on this endpoint.

What is enforced is an hourly limit, so that one key cannot run the service without end: 60 requests and 120,000 words per key, and 120 requests per IP address, each counted over the last hour. Over a limit, the answer is 429 with a retry-after header. The counts are kept in each running server instance, not in a shared store, so in practice each limit applies per instance.

Education accounts

An email address at a recognized school or university domain gets the Education plan free, including the API and the MCP server, and 50 papers a month in the web app. The endpoint, the key format and the response are the same as on any other plan.

Verify the address before you create a key. Until it is verified, the account is on the Free plan, and creating a key answers 403 {"error":"plan_required"}.

Limits

The detector reads English academic and student writing. From 50 to 149 words, a reading is weak. Machine-written text under 300 words has not been measured.

A flag should be reason to look closer and not verified proof of cheating. Do not build a system that penalizes a person on a verdict alone.