API guide
Base URL: https://api.p3n.si. All requests and responses are JSON. Send Content-Type: application/json.
This page walks through the API with examples. Every endpoint, field and error, with a live console to try calls, is in the API reference. The machine-readable OpenAPI document is at https://api.p3n.si/openapi.json, for SDK generators, Postman and coding agents.
Try it without a key
No sign-up needed. Calls without an Authorization header are free, 10 per hour per IP (shared with the try box on the homepage):
curl https://api.p3n.si/v1/detect -H 'Content-Type: application/json' \
-d '{"text": "Paste a few sentences here to see whether they read as machine-written."}'
Each response carries X-Free-Remaining. When it reaches zero you get 429 free_limit with a signup_url. Sign up for $10.00 of credit and an API key to keep going. Every skill is open source on GitHub if you'd rather run it yourself.
Authentication
Create a key on the API keys page and send it as a bearer token:
Authorization: Bearer p3n_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
A wrong or revoked key is 401; it never falls back to keyless use.
Optionally, name your tool in X-P3N-Client (up to 40 characters, e.g. X-P3N-Client: my-agent/1.2; browsers may send it too, CORS allows it). The usage page splits your runs and spend by it; without it, the page guesses from the User-Agent (curl, python, node, browser).
Billing
Every call reserves credit up front and is charged for the work it actually did: a base fee of $0.001 plus twice what the models cost us. The pricing page shows what a typical call to each skill costs. Each response includes charged_usd. If your balance is too low you get 402 insufficient_credit and nothing runs. Failed calls are not charged.
Versions and caching
Every skill result names the skill version that produced it (output.version for detect). A version is fixed: any change to a skill's rules, prompts or models ships as a new version, and published versions stay available.
- Pin a version with
"options": {"version": "detect-v1.1"}. Without it you get the latest. An unknown version is a422that lists the available ones. - The same version, options and text give the same result, served from cache (
"cached": true) and charged the base fee alone. Rescanning a document is free of surprises. "options": {"fresh": true}skips the cache and runs again (useful for rewrite skills, where you may want a different rewrite).- The model providers can change a model behind the same name; a pinned version is exact for text already scanned and best-effort for new text.
Errors
{"error": {"type": "insufficient_credit", "message": "..."}}
| Status | type | Meaning |
|---|---|---|
| 401 | unauthorized |
Missing or invalid key |
| 402 | insufficient_credit |
Top up to continue |
| 404 | not_found |
Unknown skill, run or session |
| 422 | invalid_request, skill_error |
Bad input |
| 429 | free_limit |
Keyless allowance used up for this hour; sign up |
| 429 | rate_limited |
60 requests per minute per key |
| 502 | upstream_error |
A model provider failed; not charged |
| 504 | too_long |
The text took longer than 85 s to process; send a shorter text. Not charged |
Skills
GET /v1/skills lists them.
AI Detection
Was this written by a person or a model?
POST /v1/detect
{"text": "..."}
Deslop
Cuts the filler so your text says what you mean.
POST /v1/deslop
{"text": "..."}
Humanize
Rewrites text so it stops sounding like AI.
POST /v1/humanize
{"text": "..."}
Response (a run):
{
"id": "01JD...",
"skill": "detect",
"status": "succeeded",
"output": {"version": "detect-v1.1", "ai": 0.94, "label": "ai", "verdict": "...", "confidence": {"level": "high", "reason": "..."},
"breakdown": {"human": 0.01, "ai": 0.97, "ai_edited": 0.01, "humanized": 0.01},
"sentences": [{"text": "...", "why": "..."}], "examples": [{"sentence": "...", "points": 10, "rules": [...]}],
"tells": [...], "agent": {"family": "unknown", "why": "..."}, "checks": {...}},
"charged_usd": "0.002600",
"cached": false,
"error": null,
"created_at": "2026-10-18T20:00:00+00:00"
}
Every skill takes "options": {"fast": false} to run on the model's standard tier, at about half the model cost and a third slower (default true: Luna's fast tier; Haiku is the same either way).
Detect takes "options": {"model": "luna"} (default) or "haiku" for the judge, and "version" to pin one. See AI Detection for every field.
humanize returns {"version", "model", "text", "ai_before", "ai_after", "label_after", "passes", "candidates", "invented_blocked"} as output: the rewrite, and the p(ai) before and after from our AI classifier (one of AI Detection's checks, not its full score). Options: model (haiku, default, or luna), version, before (default true: also score the text as sent). See Humanize.
Deslop
Deslop is an editor that works in rounds. Start a session with the text (and, optionally, who it is for), then send each revision back with your answers to the editor's questions.
POST /v1/deslop/sessions
{"text": "...", "options": {"brief": "new hires on the support team"}}
→ 201 {"session": {"id": "01JD...", ...}, "run": {"output": {"round": 1, "score": 40, "summary": "...", "items": [...], "revised": "..."}}}
POST /v1/deslop/sessions/{id}/messages
{"message": "<the revised text>", "answers": "P1S2: the marker is the lock file"}
→ {"session": {...}, "run": {"output": {"round": 2, "score": 72, "progress": "...", ...}}}
Each item has a label (P2S3 is paragraph 2, sentence 3; P2 the whole paragraph), a type (change, question, cut, note), the original, a proposal or question, and why. revised is the text with every change and cut applied. Options: brief, model (haiku default, luna), version, target_score (default 80) and max_rounds (default 5). Every round says done and done_reason: score (reached the target), rounds (reached the cap) or stalled (the score rose less than 5 points over two rounds); you may keep going.
To try the loop without writing the answers yourself, POST /v1/deslop/sessions/{id}/author {"text": "<current text>"} has a model play the author (the original path, /writer, still works and labels the run writer): it returns output.text (revised) and output.answers for the next message. Nothing is sent to the editor; billed like a run. See Deslop.
Editorial
Editorial works like Deslop, in rounds with the same request and output, with two additions: the editor also reads for AI writing artifacts, and the output carries ai, what AI detection makes of the text that round sent: by default only on the round that ends the session (done), null before it; options.detect is every or off to change that.
POST /v1/editorial/sessions
{"text": "...", "options": {"brief": "new hires on the support team"}}
→ 201 {"session": {"id": "01JD...", ...}, "run": {"output": {"round": 1, "score": 30, "items": [...], "revised": "...",
"ai": {"ai": 0.91, "label": "ai", "verdict": "..."}}}}
POST /v1/editorial/sessions/{id}/messages
{"message": "<the revised text>", "answers": "P1S2: ..."}
GET /v1/editorial/sessions/{id}
→ the session with all messages
Sessions expire 24 hours after the last message.
Long inputs: async runs
Add "async": true (large inputs switch automatically). You get 202 with a run id and poll URL:
GET /v1/runs/{id}
Poll until status is succeeded or failed.
Account
GET /v1/me
→ {"email": "...", "balance_usd": "9.998000", "key": {"name": "laptop", "prefix": "a1b2c3d4e5f6"}}
Limits
- Input up to 200,000 characters per call.
- 60 requests per minute per key.