Developer Reference

API Reference

The Preprints.ai REST API lets you programmatically submit preprints for assessment, retrieve A5–E1 quality grades, and integrate evidence-based scores into your platform, journal workflow, or LLM pipeline.

Base URL: https://api.preprints.ai  ·  All requests require an API key in the X-API-Key header. Get a free key →

Free
100
lookups/day · 10 assessments/day
Pro — $99/mo
10K
lookups/day · 500 assessments/day
Publisher — $499/mo
unlimited lookups · 5K assessments/day · SLA

Authentication

Pass your API key as the X-API-Key request header. Keys are available in your dashboard.

# Include your key in every request curl https://api.preprints.ai/v2/score/10.1101/2025.01.15.633214 \ -H "X-API-Key: prai_live_xxxxxxxxxxxxxxxxxxxx"
import requests headers = { "X-API-Key": "prai_live_xxxxxxxxxxxxxxxxxxxx" } response = requests.get( "https://api.preprints.ai/v2/score/10.1101/2025.01.15.633214", headers=headers ) data = response.json()
const response = await fetch( "https://api.preprints.ai/v2/score/10.1101/2025.01.15.633214", { headers: { "X-API-Key": "prai_live_xxxxxxxxxxxxxxxxxxxx" } } ); const data = await response.json();

Try It Live

Paste any DOI, arXiv ID, or bioRxiv URL to fetch a cached assessment directly from the API.

GET /v2/score/{doi}
Fetch a cached assessment — no API key required for the demo

POST /v2/assess

Submit a paper for assessment. Returns a job_id to poll with GET /v2/status/{job_id}. Processing typically takes 45–120 seconds.

Cached results: If the paper has been previously assessed, this endpoint returns the cached result immediately with status: "complete".
ParameterTypeDescription
doioptionalstringDOI of the paper (e.g. 10.1101/2025.01.15.633214)
arxiv_idoptionalstringarXiv ID (e.g. 2501.12345)
pdf_urloptionalstringDirect URL to the PDF. Required if DOI/arXiv not resolvable.
callback_urloptionalstringWebhook URL to POST result to on completion. Publisher
priorityoptionalintegerProcessing priority 1–10. Default: 5. Pro+
curl -X POST https://api.preprints.ai/v2/assess \ -H "X-API-Key: prai_live_xxxx" \ -H "Content-Type: application/json" \ -d '{"doi": "10.1101/2025.01.15.633214"}'
response = requests.post( "https://api.preprints.ai/v2/assess", headers=headers, json={"doi": "10.1101/2025.01.15.633214"} ) job = response.json() job_id = job["job_id"]
const job = await fetch("https://api.preprints.ai/v2/assess", { method: "POST", headers: { "X-API-Key": "prai_live_xxxx", "Content-Type": "application/json" }, body: JSON.stringify({ doi: "10.1101/2025.01.15.633214" }) }).then(r => r.json()); const jobId = job.job_id;
Response — 202 Accepted
{ "job_id": "job_01JKXXXXXXXXXXXXXXXX", "status": "queued", "doi": "10.1101/2025.01.15.633214", "estimated_seconds": 90, "poll_url": "https://api.preprints.ai/v2/status/job_01JKXXX" }

GET /v2/score/{doi}

Retrieve a completed assessment by DOI. Returns 404 if the paper has not yet been assessed.

curl https://api.preprints.ai/v2/score/10.1101/2025.01.15.633214 \ -H "X-API-Key: prai_live_xxxx"
doi = "10.1101/2025.01.15.633214" result = requests.get( f"https://api.preprints.ai/v2/score/{doi}", headers=headers ).json()
const doi = "10.1101/2025.01.15.633214"; const result = await fetch( `https://api.preprints.ai/v2/score/${doi}`, { headers: { "X-API-Key": "prai_live_xxxx" } } ).then(r => r.json());
Response — 200 OK
{ "doi": "10.1101/2025.01.15.633214", "grade": "B4", "integrity": { "grade": "B", "label": "Convincing", "score": 0.82, "modules": { "paper_mill": { "status": "pass", "score": 0.97 }, "statistical": { "status": "pass", "verified": 12, "errors": 0 }, "open_data": { "status": "partial", "repos": ["GEO:GSE12345"] }, "image_integrity":{ "status": "pass" } } }, "novelty": { "grade": "4", "label": "Fundamental", "score": 0.78 }, "confidence": 0.85, "panel_agreement":0.91, "agents": [ { "role": "Methodologist", "model": "claude-sonnet-4-20250514", "score": 7.8 }, { "role": "Statistician", "model": "gpt-4o-2024-11-20", "score": 8.1 }, /* ... 7 more agents */ ], "assessed_at": "2026-02-25T10:30:00Z", "version": "2.0" }

GET /v2/scores

Bulk lookup — fetch assessments for up to 100 DOIs in a single request. Pass DOIs as a comma-separated query parameter.

ParameterTypeDescription
doisrequiredstringComma-separated list of up to 100 DOIs
include_evidenceoptionalbooleanInclude full evidence object per paper. Default: false.
curl "https://api.preprints.ai/v2/scores?dois=10.1101/2025.01.15.633214,10.1101/2025.02.01.987654" \ -H "X-API-Key: prai_live_xxxx"
dois = ["10.1101/2025.01.15.633214", "10.1101/2025.02.01.987654"] response = requests.get( "https://api.preprints.ai/v2/scores", params={"dois": ",".join(dois)}, headers=headers ).json()

GET /v2/status/{job_id}

Poll the status of a submitted assessment job. Poll every 10–30 seconds until status: "complete".

Response — 200 OK
{ "job_id": "job_01JKXXXXXXXXXXXXXXXX", "status": "complete", // queued | processing | complete | failed "progress": 100, // 0-100 "stage": "deliberation",// layer1 | agentic | deliberation "result_url": "https://api.preprints.ai/v2/score/10.1101/..." }

Error Codes

StatusCodeDescription
400invalid_identifierDOI or arXiv ID could not be resolved
401missing_api_keyNo API key provided
403invalid_api_keyAPI key is invalid or revoked
404not_assessedPaper exists but has not been assessed. Use POST /v2/assess.
429rate_limitedDaily quota exceeded. Retry-After header included.
503assessment_unavailablePDF inaccessible or insufficient text extracted

Score Badges

Embed a dynamically generated SVG score badge on your paper pages. Colour-coded by grade.

<!-- Embed badge for a specific DOI --> <img src="https://preprints.ai/badge/10.1101/2025.01.15.633214" alt="Preprints.ai score: B4" />
[![Preprints.ai score](https://preprints.ai/badge/10.1101/2025.01.15.633214)](https://preprints.ai/doi/10.1101/2025.01.15.633214)

See the embed guide for badge customisation options and live previews.


Webhooks Publisher

Receive assessment results via webhook instead of polling. Configure callback URLs in your dashboard.

Signature verification: Every webhook includes an X-Preprints-Signature header — an HMAC-SHA256 of the request body using your webhook secret. Always verify this before processing.
import hmac, hashlib def verify_webhook(payload: bytes, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode(), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature)
const crypto = require('crypto'); function verifyWebhook(payload, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); }