p3n

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 a 422 that 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.