Put AI detection inside your product
Send text over HTTPS and get back a score, a verdict and sentence-level highlights as JSON. Same model as the dashboard, paid for from the same credits.
- JSON over HTTPS
- Sentence-level results
- 1 credit per word
curl https://winstonai.com/api/v1/detect \
-H "Authorization: Bearer $WINSTONAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Remote work has become a cornersto…"}'{
"human_score": 47,
"verdict": "mixed",
"sentences": [
{
"text": "Remote work has become a c…",
"ai_probability": 0.755,
"verdict": "ai"
},
{
"text": "Moreover, it enables organ…",
"ai_probability": 0.524,
"verdict": "mixed"
},
{
"text": "I still miss the office co…",
"ai_probability": 0.218,
"verdict": "human"
}
]
}Overview
Step 1: Create a key
Open Developers in the dashboard and create an API key.
Step 2: Send some text
POST at least 40 words to
/api/v1/detect.Step 3: Read the result
Use
human_scorefor the overall call andsentencesfor highlights.
Conventions
- Base URL
https://winstonai.com/api/v1- Format
- JSON request and response bodies in UTF-8, snake_case field names, timestamps in ISO 8601 UTC.
- Versioning
- The version is part of the path. Within v1 we only add fields, never remove or rename them, so ignore any field you don't recognize.
- Safe retries
- Send an
Idempotency-Keyheader, up to 255 characters, with any POST. Repeating the request with the same key and body within 24 hours replays the first response, markedIdempotent-Replayed: true, instead of scanning and charging twice. - OpenAPI
GET /api/v1/openapi.jsonreturns an OpenAPI 3.1 description of every endpoint, with no key needed, so you can generate a client from it. Open the spec.- Request ids
- Every response carries an
X-Request-Idheader. Include it when you contact support.
Authentication
Authorization: Bearer wai_live_8f3b2c7e91d04a6fK2mQx7Rt5LwZ9pNcCreate and revoke keys on the Developers page of your dashboard. Keys start with wai_live_ followed by 32 letters and numbers. Each key belongs to a workspace and spends that workspace's credits. A key is shown only once when you create it, so store it somewhere safe, such as an environment variable called WINSTONAI_API_KEY.
Keep keys on your server
unauthorized.Credits and rate limits
| Endpoint | Cost | Example |
|---|---|---|
POST /api/v1/detect | 1 credit per word | A 1,200-word essay uses 1,200 credits. |
POST /api/v1/plagiarism | 2 credits per word | The same essay uses 2,400 credits. |
POST /api/v1/image | 300 credits per image | Coming soon. |
GET /api/v1/scans/{id} | Free | Fetch results as often as rate limits allow. |
GET /api/v1/usage | Free | Check your balance before a large batch. |
Credits are reserved when a scan is accepted and refunded automatically if it fails. If your balance is too low, the request fails with insufficient_credits before anything is scanned.
Limits
- Text per request40 to 25,000 words
- Images, coming soonUp to 10 MB: JPEG, PNG, WebP or HEIC
- Requests60 per minute for each API key
- Plagiarism scans10 running at once per workspace
Starting another plagiarism check while 10 are running returns rate_limited with retry_after set to 30 seconds.
Response headers
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute for this key. Sent with every authenticated response. |
X-RateLimit-Remaining | Requests left in the current minute. |
X-RateLimit-Reset | Seconds until the count resets. |
Retry-After | Sent with 429 responses: seconds to wait before trying again. |
X-Credits-Remaining | Your credit balance afterwards. Sent on requests that spend credits: POST /detect and POST /plagiarism. |
X-Request-Id | Unique id for this request, the same value as request_id in error bodies. |
Idempotent-Replayed | true when the response is a replay of an earlier request with the same Idempotency-Key. |
Check your balance and limits at any time with Check usage. Need higher limits for a large rollout? Talk to sales.
Detect AI text
POSThttps://winstonai.com/api/v1/detect
Scores text for signs of machine writing and returns the result straight away, usually in about a second for an essay. Each call creates a scan you can also open in the dashboard.
Body parameters
textstringRequired- The text to check, from 40 to 25,000 words.
languagestring- ISO 639-1 code such as
enorfr. Detected automatically when left out. titlestring- A name for the scan, shown in your dashboard. Up to 200 characters.
authorstring- Who wrote the text. Up to 120 characters.
folder_idstring- Save the scan into one of your dashboard folders. An unknown id fails with
invalid_body. sentencesboolean- Include the per-sentence results. Defaults to
true; setfalseto leavesentencesout of the result. metadataobject- Up to 20 string key-value pairs, such as your own submission id. Returned with the scan and in webhooks.
curl https://winstonai.com/api/v1/detect \
-H "Authorization: Bearer $WINSTONAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Remote work has become a cornerstone of modern business, offering employees unparalleled flexibility and a better balance between work and home. Moreover, it enables organizations to tap into a diverse, global talent pool. I still miss the office coffee, though, and the chats that only happen by accident.",
"title": "Remote work essay"
}'Response
A scan object with type set to ai_text and status set to completed.
{
"id": "scn_6f1c2a9e4b7d4c1e9a3f5b8d2e7c4a10",
"object": "scan",
"type": "ai_text",
"status": "completed",
"credits_used": 48,
"metadata": {},
"created_at": "2026-10-18T09:24:11Z",
"completed_at": "2026-10-18T09:24:12Z",
"result": {
"human_score": 47,
"verdict": "mixed",
"word_count": 48,
"sentence_count": 3,
"readability": 66,
"language": "en",
"sentences": [
{
"text": "Remote work has become a cornerstone of modern business, offering employees unparalleled flexibility and a better balance between work and home.",
"ai_probability": 0.755,
"verdict": "ai"
},
{
"text": "Moreover, it enables organizations to tap into a diverse, global talent pool.",
"ai_probability": 0.524,
"verdict": "mixed"
},
{
"text": "I still miss the office coffee, though, and the chats that only happen by accident.",
"ai_probability": 0.218,
"verdict": "human"
}
]
},
"error": null
}The result object
human_scoreinteger- 0 to 100. Higher means more likely written by a person.
verdictstringhumanwhenhuman_scoreis 60 or more,mixedfrom 35 to 59,aibelow 35.sentences[].ai_probabilitynumber- 0 to 1, to three decimal places. How likely this sentence is to be machine-written.
sentences[].verdictstringaifrom 0.65,mixedfrom 0.40, otherwisehuman. Use it to color highlights.readabilityinteger- Reading ease, from 0 (hard) to 100 (easy).
languagestring | null- ISO 639-1 code of the language the text was scored as, or
nullif it couldn't be detected. word_count, sentence_countinteger- Counts for the text that was scanned.
Check plagiarism
POSThttps://winstonai.com/api/v1/plagiarism
Compares text against web pages and your library. A check takes 10 to 60 seconds, so this endpoint queues the scan and replies at once with status set to queued. Get the result from a webhook or by polling Retrieve a scan.
Included on Advanced and Team plans
plan_required. Credits are only charged once the check is accepted.Body parameters
textstringRequired- The text to compare against web pages and your library, from 40 to 25,000 words.
titlestring- A name for the scan, shown in your dashboard. Up to 200 characters.
authorstring- Who wrote the text. Up to 120 characters.
folder_idstring- Save the scan into one of your dashboard folders. An unknown id fails with
invalid_body. exclude_urlsstring[]- Sources to ignore, such as the writer's own site. Up to 50 URLs.
exclude_quotesboolean- Skip text inside quotation marks. Defaults to
true. metadataobject- Up to 20 string key-value pairs, such as your own submission id. Returned with the scan and in webhooks.
curl https://winstonai.com/api/v1/plagiarism \
-H "Authorization: Bearer $WINSTONAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Remote work has become a cornerstone of modern business, offering employees unparalleled flexibility and a better balance between work and home. Moreover, it enables organizations to tap into a diverse, global talent pool. I still miss the office coffee, though, and the chats that only happen by accident.",
"metadata": {
"submission_id": "essay-1042"
}
}'Response
{
"id": "scn_b3e07d5a1c9f4e2d8a6b0c4f7e1d9a35",
"object": "scan",
"type": "plagiarism",
"status": "queued",
"credits_used": 96,
"metadata": {
"submission_id": "essay-1042"
},
"created_at": "2026-10-18T09:26:40Z",
"completed_at": null,
"result": null,
"error": null
}Retrieve a scan
GEThttps://winstonai.com/api/v1/scans/{id}
Returns any scan in your workspace, whether it was created through the API or the dashboard. Use it to poll a plagiarism scan until status is completed or failed. Every 5 seconds is plenty, or use webhooks and skip polling.
Path parameters
idstringRequired- The scan id returned when you created the scan:
scn_followed by 32 hex characters.
curl https://winstonai.com/api/v1/scans/scn_b3e07d5a1c9f4e2d8a6b0c4f7e1d9a35 \
-H "Authorization: Bearer $WINSTONAI_API_KEY"Response
{
"id": "scn_b3e07d5a1c9f4e2d8a6b0c4f7e1d9a35",
"object": "scan",
"type": "plagiarism",
"status": "completed",
"credits_used": 96,
"metadata": {
"submission_id": "essay-1042"
},
"created_at": "2026-10-18T09:26:40Z",
"completed_at": "2026-10-18T09:27:05Z",
"result": {
"similarity": 22.9,
"word_count": 48,
"matched_words": 11,
"sources": [
{
"url": "https://example.com/blog/remote-hiring-guide",
"title": "A practical guide to hiring remotely",
"type": "web",
"matched_words": 11,
"percent": 22.9,
"excerpt": "it enables organizations to tap into a diverse, global talent pool"
}
]
},
"error": null
}The scan object
Every endpoint and webhook returns the same scan object. Only the shape of result changes with type.
idstring- Unique id:
scn_followed by 32 hex characters. typestringai_text,plagiarismorai_image.statusstringqueued,processing,completedorfailed.credits_usedinteger- Credits charged for this scan. Refunded if it fails.
resultobject | null- The findings, once
statusiscompleted. Its shape depends ontype. errorobject | null- A
codeandmessage, whenstatusisfailed. metadataobject- Whatever you sent when creating the scan.
created_at, completed_atstring- ISO 8601 timestamps in UTC.
The plagiarism result
similaritynumber- 0 to 100, to one decimal place. The share of words that match at least one source.
matched_wordsinteger- How many words match a source.
sources[].typestringwebfor a public web page;documentfor a match in your own workspace library.sources[].urlstring | null- Link to the source.
nullfor documents in your workspace. sources[].percentnumber- Share of your text that matches this source, to one decimal place.
sources[].matched_wordsinteger- Words in your text that match this source.
sources[].excerptstring | null- The matching passage, when one is available.
Check usage
GEThttps://winstonai.com/api/v1/usage
Returns your workspace's plan, credit balance and current rate limit. It's free, so call it before a large batch to make sure you have enough credits.
curl https://winstonai.com/api/v1/usage \
-H "Authorization: Bearer $WINSTONAI_API_KEY"Response
{
"object": "usage",
"workspace_id": "6f1c2a9e-4b7d-4c1e-9a3f-5b8d2e7c4a10",
"plan": "advanced",
"credits": {
"balance": 154230,
"monthly_allowance": 200000
},
"rate_limit": {
"limit": 60,
"remaining": 59,
"reset": 42
}
}planstringfree,essential,advancedorteam.credits.balanceinteger- Credits left to spend right now.
credits.monthly_allowanceinteger- Credits your plan adds each month.
rate_limitobjectlimit,remainingandreset(seconds), the same as the rate limit headers.
Detect AI images
Coming soonPOSThttps://winstonai.com/api/v1/image
Checks whether an image was made, or heavily edited, by an image model. Send a public URL as JSON, or upload the file as multipart/form-data. Results come back straight away.
Not live yet
not_available (501). The request and response below show the planned shape. When it launches it will be included on Advanced and Team plans; keys on other plans will get plan_required.Body parameters
urlstring- A public HTTPS link to a JPEG, PNG, WebP or HEIC image up to 10 MB. Send this or
file. filefile- The image itself, sent as
multipart/form-data. Send this orurl. metadataobject- Up to 20 string key-value pairs, such as your own submission id. Returned with the scan and in webhooks.
curl https://winstonai.com/api/v1/image \
-H "Authorization: Bearer $WINSTONAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/uploads/product-shot.png"
}'Upload a file instead
curl https://winstonai.com/api/v1/image \
-H "Authorization: Bearer $WINSTONAI_API_KEY" \
-F "file=@photo.png"Response
For images, human_score is the chance the picture came from a camera or a person rather than a model, and ai_probability is the same signal from 0 to 1.
{
"id": "scn_0d8e4f2a7c1b4e9d93a5f6c8b2e0d471",
"object": "scan",
"type": "ai_image",
"status": "completed",
"credits_used": 300,
"metadata": {},
"created_at": "2026-10-18T09:31:02Z",
"completed_at": "2026-10-18T09:31:04Z",
"result": {
"human_score": 4,
"verdict": "ai",
"ai_probability": 0.96,
"format": "png",
"width": 1024,
"height": 1024
},
"error": null
}Errors
{
"error": {
"type": "billing_error",
"code": "insufficient_credits",
"message": "This scan needs 2,360 credits and your balance is 420.",
"cost": 2360,
"balance": 420,
"request_id": "req_5bT1kz0Qe2"
}
}Extra fields sit inside error when they help: issues for invalid_body, each with a path and message; cost and balance for insufficient_credits; and retry_after for rate_limited. The request_id matches the X-Request-Id header.
Error types
| Type | Status | What it means |
|---|---|---|
invalid_request_error | 400, 404, 409 | Something about the request needs to change. |
authentication_error | 401 | The API key is missing or no longer valid. |
billing_error | 402 | The workspace needs more credits. |
permission_error | 403 | The plan doesn't include this check. |
rate_limit_error | 429 | Too many requests in the current minute. |
api_error | 5xx | A problem on our side, or a feature that isn't live yet. |
Error codes
| Code | Status | What it means |
|---|---|---|
invalid_body | 400 | The body isn't valid JSON or a field is wrong. issues lists each problem with its path. |
too_short | 400 | The text has fewer than 40 words. |
too_long | 400 | The text has more than 25,000 words. Split it into parts. |
invalid_image | 400 | The image couldn't be read, or is too large or an unsupported type. |
unauthorized | 401 | The API key is missing, revoked or mistyped. |
insufficient_credits | 402 | Not enough credits. cost and balance are included. |
plan_required | 403 | Your plan doesn't include this check. Plagiarism and images need Advanced or Team. |
not_found | 404 | No scan with that id exists in your workspace. |
idempotency_key_reused | 409 | This Idempotency-Key was already used with a different request body or endpoint. |
request_in_progress | 409 | A request with this Idempotency-Key is still running. Retry shortly. |
rate_limited | 429 | Too many requests, or too many plagiarism checks running at once. Wait retry_after seconds, also sent as the Retry-After header. |
server_error | 500 | Something failed on our side. Safe to retry with the same Idempotency-Key. |
not_available | 501 | The endpoint isn't live yet. Today that only applies to POST /image. |
unavailable | 503 | Detection is briefly unavailable. Retry with exponential backoff. |
Retry 429, 500 and 503 responses with exponential backoff, and request_in_progress after a short pause. Other 4xx errors won't succeed on retry until the request changes. Include the request_id when you contact support.
Webhooks
- Step 1: In the Webhooks section of the Developers page, add your endpoint URL and pick the events you want. Send a test event to check it.
- Step 2: Copy that endpoint's signing secret, which starts with
whsec_, into your server's environment. - Step 3: On every delivery, verify the signature, then reply with any 2xx status within 10 seconds.
| Event | When it's sent |
|---|---|
scan.completed | Any scan finished, including ones from POST /detect. data is the scan object with its result. |
scan.failed | A scan couldn't finish. data.error explains why, and the credits are refunded. |
webhook.test | Sent once, without retries, when you press Send test event on the Developers page. It goes to that endpoint whatever its subscriptions, even if paused. data is a sample completed ai_text scan with metadata.test set to "true". |
{
"id": "evt_9sK1mP4rTbX2cQ7vN3wL8yZd",
"object": "event",
"type": "scan.completed",
"created_at": "2026-10-18T09:27:05Z",
"data": {
"id": "scn_b3e07d5a1c9f4e2d8a6b0c4f7e1d9a35",
"object": "scan",
"type": "plagiarism",
"status": "completed",
"credits_used": 96,
"metadata": {
"submission_id": "essay-1042"
},
"created_at": "2026-10-18T09:26:40Z",
"completed_at": "2026-10-18T09:27:05Z",
"result": {
"similarity": 22.9,
"word_count": 48,
"matched_words": 11,
"sources": [
{
"url": "https://example.com/blog/remote-hiring-guide",
"title": "A practical guide to hiring remotely",
"type": "web",
"matched_words": 11,
"percent": 22.9,
"excerpt": "it enables organizations to tap into a diverse, global talent pool"
}
]
},
"error": null
}
}Delivery headers
| Header | Meaning |
|---|---|
WinstonAI-Signature | Timestamp and signature, such as t=1760779625,v1=5f2b9c…. |
WinstonAI-Event | The event type, such as scan.completed. |
WinstonAI-Delivery | Unique id for this delivery attempt. Useful when you contact support. |
Verify the signature
Compute an HMAC-SHA256 of the timestamp from WinstonAI-Signature, a period and the raw request body, using the endpoint's signing secret. Compare it with v1 in constant time, and reject deliveries more than five minutes old.
import crypto from "node:crypto";
// rawBody: the exact request body as a string, before JSON parsing.
export function verifyWebhook(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? "");
return fresh && a.length === b.length && crypto.timingSafeEqual(a, b);
}Delivery and retries
- Deliveries that time out or get a non-2xx reply are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 15 hours: seven attempts over about a day.
- An event can arrive more than once or out of order. Store the event
idand skip ones you have already handled. - Each endpoint has its own signing secret, so check deliveries with the secret of the endpoint they reached.
SDKs
Official SDKs are coming
Every endpoint above has a cURL, JavaScript and Python example you can copy as it is. They use only fetch and requests, with your key read from WINSTONAI_API_KEY. Tell us which language you'd like an SDK for first.
Send your first scan in minutes
Create a key on the Developers page and run the cURL example above. You'll have a result in about a second. Sending more than a million words a month? We'll price it with you.