Reference

API reference

Every endpoint that ships with Pellucid today, with copy-paste cURL, Python, and JavaScript examples.

All endpoints sit under /api/v1. The default base URL in development is http://localhost:8000. Replace it with your deployment’s origin in production.

Conventions

  • Identifiers. Documents, findings, and other rows use opaque string IDs (typically prefixed, e.g. doc_…, find_…).
  • Timestamps. ISO-8601 UTC. Stored naive in SQLite, emitted with a trailing Z.
  • Errors. Standard HTTP statuses. Error bodies are{ "detail": "..." }.
  • Auth. Currently unauthenticated. The bearer keys minted via integrations are scaffolding for the auth middleware on the roadmap.

Health

GET/health#

Liveness probe. Useful for Docker, k8s, and load balancers.

Request
bash
curl http://localhost:8000/health
Response · 200 OK
json
{
  "status": "ok",
  "service": "pellucid-api"
}

Documents

Documents are the unit of analysis. Create one, then call analyze on it. A document keeps its content and any findings produced by the most recent analyze run; re-running analyze wipes the prior findings and debate.

POST/api/v1/documents#

Create a document from inline content. Returns the persisted row.

Parameters
  • contentstringrequired
    The document body. Plain text or markdown; min length 1.
  • titlestring?
    Optional title. Falls back to the first non-empty line, trimmed to 80 chars.
Request
bash
curl -X POST http://localhost:8000/api/v1/documents \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Vehicle Telematics SRS",
    "content": "The system shall be user-friendly and respond quickly."
  }'
Response · 200 OK
json
{
  "id": "doc_01HZ...",
  "title": "Vehicle Telematics SRS",
  "content": "The system shall be user-friendly and respond quickly.",
  "created_at": "2025-04-08T17:21:09Z",
  "updated_at": "2025-04-08T17:21:09Z"
}
POST/api/v1/documents/upload#

Multipart upload for .pdf, .docx, .txt, or .md. Server parses to clean text.

Parameters
  • filemultipart/form-datarequired
    One of .pdf, .docx, .txt, .md. 415 if the type isn't supported.
Request
bash
curl -X POST http://localhost:8000/api/v1/documents/upload \
  -F "file=@./srs.pdf"
Response · 200 OK
json
{
  "id": "doc_01HZ...",
  "title": "srs",
  "content": "1. Introduction\nThe system shall...",
  "created_at": "...",
  "updated_at": "..."
}
GET/api/v1/documents#

List up to 50 documents, newest-updated first.

Request
bash
curl http://localhost:8000/api/v1/documents
Response · 200 OK
json
[
  {
    "id": "doc_01HZ...",
    "title": "Vehicle Telematics SRS",
    "content": "...",
    "created_at": "...",
    "updated_at": "..."
  }
]
GET/api/v1/documents/{id}#

Fetch a single document with its current findings array embedded.

Parameters
  • idstringrequired
    Document id.
Request
bash
curl http://localhost:8000/api/v1/documents/doc_01HZ...
Response · 200 OK
json
{
  "id": "doc_01HZ...",
  "title": "Vehicle Telematics SRS",
  "content": "The system shall be user-friendly...",
  "created_at": "...",
  "updated_at": "...",
  "findings": [
    {
      "id": "find_01J0...",
      "document_id": "doc_01HZ...",
      "start": 16,
      "end": 29,
      "text": "user-friendly",
      "type": "vague_term",
      "severity": "high",
      "rationale": "Subjective adjective with no measurable acceptance criterion.",
      "confidence": 0.92,
      "source": "rules",
      "suggestion": null,
      "created_at": "..."
    }
  ]
}
PUT/api/v1/documents/{id}#

Replace a document's content. Used by the live-edit (?edit=1) surface.

Note. PUT does not invalidate findings — the live-edit client is responsible for re-running analyze, which wipes them itself.

Parameters
  • idstringrequired
    Document id.
  • contentstringrequired
    New body.
  • titlestring?
    Optional rename.
Request
bash
curl -X PUT http://localhost:8000/api/v1/documents/doc_01HZ... \
  -H "Content-Type: application/json" \
  -d '{ "content": "The system shall present a labelled control..." }'
Response · 200 OK
json
{
  "id": "doc_01HZ...",
  "title": "...",
  "content": "...",
  "created_at": "...",
  "updated_at": "..."
}
DELETE/api/v1/documents/{id}#

Hard-delete a document and its findings, debates, and shares.

Parameters
  • idstringrequired
    Document id.
Request
bash
curl -X DELETE http://localhost:8000/api/v1/documents/doc_01HZ...

Analyze

POST/api/v1/documents/{id}/analyze#

Open a Server-Sent Events stream of findings. Wipes prior results.

The stream emits, in order: rule_findings, several agent_findings, one or more critique, final, then done. On error, an error event is emitted and the stream closes. See the streaming events table for full payload shapes.

Parameters
  • idstringrequired
    Document id.
Request
bash
# -N disables curl's output buffering so events render live
curl -N -X POST http://localhost:8000/api/v1/documents/doc_01HZ.../analyze

Findings

POST/api/v1/findings/{id}/rewrite#

Generate 2–3 candidate rewrites for a finding via the configured LLM.

Parameters
  • idstringrequired
    Finding id.
Request
bash
curl -X POST http://localhost:8000/api/v1/findings/find_01J0.../rewrite
Response · 200 OK
json
{
  "finding_id": "find_01J0...",
  "candidates": [
    { "text": "The system shall present a labelled control...", "rationale": "..." },
    { "text": "The system shall complete every action within 200 ms...", "rationale": "..." }
  ]
}
POST/api/v1/findings/{id}/apply#

Replace the finding's span in the document with the given text.

Returns 200 with the updated document. On success the server also clears every finding + the persisted debate transcript for this document — every offset past the edit point shifted by len(replacement) - len(original), so the previous analysis is stale. Re-trigger POST /analyze to repopulate findings against the cleaned-up text.

Refuses with 409 when document[finding.start:finding.end] != finding.text — the document was edited under us and applying would corrupt it. 422 on empty replacement; 404 on missing finding or document.

Parameters
  • idstringrequired
    Finding id.
Request
bash
curl -X POST http://localhost:8000/api/v1/findings/find_01J0.../apply \
  -H "Content-Type: application/json" \
  -d '{"replacement":"operable by a novice in under one minute"}'
Response · 200 OK
json
{
  "id": "doc_01HZ...",
  "title": "Vehicle Telematics System",
  "content": "The system shall be operable by a novice in under one minute and provide optimal performance...",
  "created_at": "2026-05-09T12:34:00Z",
  "updated_at": "2026-05-10T09:15:42Z"
}

Debate

GET/api/v1/debate/{document_id}#

Fetch the full multi-agent transcript persisted by the most recent analyze.

Parameters
  • document_idstringrequired
    The document the analysis ran against.
Request
bash
curl http://localhost:8000/api/v1/debate/doc_01HZ...
Response · 200 OK
json
{
  "document_id": "doc_01HZ...",
  "transcript": {
    "reports": [
      {
        "agent": "lexical",
        "findings": [ { "start": 16, "end": 29, "text": "user-friendly", "type": "vague_term", ... } ],
        "reasoning": "Two adjectives flagged...",
        "self_confidence": 0.85
      }
    ],
    "critiques": [
      { "voter": "domain", "target_agent": "lexical", "target_finding_index": 0, "agree": true, "reasoning": "Concur — no measurable criterion." }
    ],
    "consensus_finding_ids": ["find_01J0..."]
  },
  "created_at": "..."
}

Settings · Provider

Configure the active LLM provider. See BYOK for the long form.

GET/api/v1/settings/provider#

Return the active provider, whether a key is present, and where it came from.

Request
bash
curl http://localhost:8000/api/v1/settings/provider
Response · 200 OK
json
{
  "model": "anthropic/claude-sonnet-4-6",
  "has_key": true,
  "source": "byok"
}
POST/api/v1/settings/provider#

Save a model + API key. Plaintext is encrypted at rest with Fernet.

Parameters
  • modelstringrequired
    LiteLLM model identifier.
  • api_keystringrequired
    Plaintext key. Encrypted before persistence.
Request
bash
curl -X POST http://localhost:8000/api/v1/settings/provider \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-sonnet-4-6",
    "api_key": "sk-ant-..."
  }'
Response · 200 OK
json
{ "model": "anthropic/...", "has_key": true, "source": "byok" }
DELETE/api/v1/settings/provider#

Drop the BYOK row. Pellucid falls back to the matching env var.

Request
bash
curl -X DELETE http://localhost:8000/api/v1/settings/provider
Response · 200 OK
json
{ "model": "xai/grok-4", "has_key": false, "source": "env" }

Audit

GET/api/v1/audit/{document_id}.json#

Download the full audit trail (document, findings, debate) as a JSON file.

Parameters
  • document_idstringrequired
    Document id.
Request
bash
curl -O -J http://localhost:8000/api/v1/audit/doc_01HZ....json
GET/api/v1/audit/{document_id}.md#

Same as above, rendered as a human-readable Markdown report.

Parameters
  • document_idstringrequired
    Document id.
Request
bash
curl -O -J http://localhost:8000/api/v1/audit/doc_01HZ....md

Share + badge

Public read-only document views and an embeddable clarity-score SVG.

POST/api/v1/share#

Mint a tokenised share link for a document.

Parameters
  • document_idstringrequired
    Document id to share.
  • expires_atdatetime?
    Optional ISO-8601 expiry.
Request
bash
curl -X POST http://localhost:8000/api/v1/share \
  -H "Content-Type: application/json" \
  -d '{ "document_id": "doc_01HZ...", "expires_at": "2025-12-31T23:59:59Z" }'
Response · 200 OK
json
{
  "token": "shr_01J0...",
  "public_url": "/share/shr_01J0...",
  "expires_at": "2025-12-31T23:59:59Z",
  "created_at": "2025-04-08T17:30:00Z"
}
GET/api/v1/share/{token}#

Resolve a share token to its public read-only payload. 404 on invalid/expired.

Request
bash
curl http://localhost:8000/api/v1/share/shr_01J0...
Response · 200 OK
json
{
  "document": { "id": "doc_01HZ...", "title": "...", "content": "..." },
  "findings": [ { "id": "find_...", "type": "vague_term", "severity": "high", ... } ],
  "expires_at": "2025-12-31T23:59:59Z"
}
DELETE/api/v1/share/{token}#

Revoke a share token immediately.

Request
bash
curl -X DELETE http://localhost:8000/api/v1/share/shr_01J0...
GET/api/v1/badge/{document_id}.svg#

Embed-friendly clarity score SVG. CORS-open, cached for 60s + SWR.

Parameters
  • document_idstringrequired
    Document id.
Request
bash
# Markdown:
![Clarity](http://your.host/api/v1/badge/doc_01HZ....svg)

Glossary

Approved-term registry that the rule layer cross-references. Use it to enforce canonical terminology across a team.

GET/api/v1/glossary#

List every glossary entry, ordered by term.

Request
bash
curl http://localhost:8000/api/v1/glossary
Response · 200 OK
json
[
  {
    "id": "gloss_01J...",
    "term": "Telematics Unit",
    "definition": "Embedded device that...",
    "synonyms": ["TCU"],
    "disallowed": ["unit", "device"]
  }
]
POST/api/v1/glossary#

Create a glossary entry.

Parameters
  • termstringrequired
    Canonical term.
  • definitionstringrequired
    Plain-English meaning.
  • synonymsstring[]
    Equivalent forms — accepted, no rewrite.
  • disallowedstring[]
    Forms that should be rewritten to the canonical term.
Request
bash
curl -X POST http://localhost:8000/api/v1/glossary \
  -H "Content-Type: application/json" \
  -d '{
    "term": "Telematics Unit",
    "definition": "Embedded vehicle device.",
    "synonyms": ["TCU"],
    "disallowed": ["unit", "device"]
  }'
GET/api/v1/glossary/{id}#

Fetch one glossary entry.

Request
bash
curl http://localhost:8000/api/v1/glossary/gloss_01J...
PUT/api/v1/glossary/{id}#

Replace an entry. Same body as POST.

Request
bash
curl -X PUT http://localhost:8000/api/v1/glossary/gloss_01J... \
  -H "Content-Type: application/json" \
  -d '{ "term": "...", "definition": "...", "synonyms": [], "disallowed": [] }'
DELETE/api/v1/glossary/{id}#

Delete an entry. 404 if it doesn't exist.

Request
bash
curl -X DELETE http://localhost:8000/api/v1/glossary/gloss_01J...

Custom rules

User-authored regex rules layered on top of the built-in detectors. The rule layer compiles them once per process and runs them on every analyze.

GET/api/v1/rules#

List all custom rules, newest first.

Request
bash
curl http://localhost:8000/api/v1/rules
POST/api/v1/rules#

Create a custom rule. The pattern is compiled at write-time; bad regex returns 422.

Parameters
  • patternstringrequired
    Regex (Python re syntax). 1–4000 chars.
  • flagsstring
    Subset of i, m, s, x — case-insensitive, multiline, dotall, verbose.
  • typeFindingTyperequired
    One of vague_term, passive_voice, etc. See /api/v1/rules/_meta.
  • severitySeverityrequired
    low | medium | high | critical.
  • rationalestringrequired
    Why this match is a finding. Surfaces in the UI.
  • enabledboolean
    Defaults to true.
Request
bash
curl -X POST http://localhost:8000/api/v1/rules \
  -H "Content-Type: application/json" \
  -d '{
    "pattern": "(?i)\\b(easy|simple|intuitive)\\b",
    "flags": "",
    "type": "vague_term",
    "severity": "medium",
    "rationale": "Subjective UX adjectives — replace with a measurable affordance."
  }'
PUT/api/v1/rules/{id}#

Replace a custom rule.

Request
bash
curl -X PUT http://localhost:8000/api/v1/rules/rule_01J... -H "Content-Type: application/json" -d '{...}'
DELETE/api/v1/rules/{id}#

Delete a custom rule. 404 if missing.

Request
bash
curl -X DELETE http://localhost:8000/api/v1/rules/rule_01J...
GET/api/v1/rules/_meta#

Enumerate the allowed finding types, severities, and regex flags.

Request
bash
curl http://localhost:8000/api/v1/rules/_meta
Response · 200 OK
json
{
  "types": ["vague_term", "passive_voice", "pronoun_antecedent", ...],
  "severities": ["low", "medium", "high", "critical"],
  "flags": ["i", "m", "s", "x"]
}

Templates

Read-only registry of bundled document templates. The web app uses these to seed the editor; you can use them to bootstrap synthetic test data.

GET/api/v1/templates#

List every bundled template summary.

Request
bash
curl http://localhost:8000/api/v1/templates
Response · 200 OK
json
{
  "templates": [
    { "slug": "saas-srs", "name": "SaaS SRS", "industry": "Software", "description": "..." }
  ]
}
GET/api/v1/templates/{slug}#

Fetch a template including its full markdown body.

Request
bash
curl http://localhost:8000/api/v1/templates/saas-srs

Compare

GET/api/v1/compare#

Diff two documents' findings. Returns clarity scores, deltas, and matched spans.

Parameters
  • leftstringrequired
    Older document id.
  • rightstringrequired
    Newer document id.
Request
bash
curl "http://localhost:8000/api/v1/compare?left=doc_a&right=doc_b"

Analytics

GET/api/v1/analytics#

Dashboard payload — totals, severity histogram, agent agreement-rate proxy.

Request
bash
curl http://localhost:8000/api/v1/analytics

Ontology

Approved-reference documents and the term graph extracted from them. Endpoints respond even when Neo4j is unconfigured — extraction runs in-memory so the UI can preview ingestion without a backing graph DB.

POST/api/v1/ontology/documents#

Mark a document as an approved reference and ingest its terms.

Parameters
  • document_idstring?
    Existing document id. Either this or content is required.
  • titlestring?
    Optional override.
  • contentstring?
    Inline content. Creates a Document row when document_id is omitted.
Request
bash
curl -X POST http://localhost:8000/api/v1/ontology/documents \
  -H "Content-Type: application/json" \
  -d '{ "document_id": "doc_01HZ..." }'
Response · 200 OK
json
{
  "document_id": "doc_01HZ...",
  "title": "ISO 26262 — vehicle safety",
  "graph_enabled": true,
  "term_count": 142,
  "terms": [{ "lemma": "telematics", "frequency": 12, ... }]
}
GET/api/v1/ontology/terms#

List extracted terms with frequencies.

Parameters
  • limitinteger
    1–1000. Defaults to 200.
Request
bash
curl "http://localhost:8000/api/v1/ontology/terms?limit=50"
GET/api/v1/ontology/term/{term}#

Look up ontology context for a single term — synonyms, parents, frequency.

Request
bash
curl http://localhost:8000/api/v1/ontology/term/telematics

Integrations · API keys

API keys are minted server-side, returned in plaintext exactly once, and persisted only as a SHA-256 hash. Use them as the bearer credential when the auth middleware lands.

POST/api/v1/integrations/api-keys#

Mint a new key. Returns the plaintext exactly once.

Parameters
  • labelstringrequired
    Human-readable label, 1–128 chars.
Request
bash
curl -X POST http://localhost:8000/api/v1/integrations/api-keys \
  -H "Content-Type: application/json" \
  -d '{ "label": "CI bot" }'
Response · 200 OK
json
{
  "id": "key_01J...",
  "label": "CI bot",
  "created_at": "...",
  "last_used_at": null,
  "revoked_at": null,
  "plaintext": "pk_live_abcdEFGHijklMNO123..."
}
GET/api/v1/integrations/api-keys#

List minted keys. Plaintext is never included here.

Request
bash
curl http://localhost:8000/api/v1/integrations/api-keys
DELETE/api/v1/integrations/api-keys/{id}#

Hard-delete a key.

Request
bash
curl -X DELETE http://localhost:8000/api/v1/integrations/api-keys/key_01J...

Integrations · Webhooks

POST/api/v1/integrations/webhooks#

Register an outbound webhook target.

Parameters
  • labelstringrequired
    Display name.
  • urlurlrequired
    Receiver URL. Must be http(s)://.
  • kind"generic" | "slack" | "discord"
    Selects the payload shape. Defaults to generic.
  • secretstring?
    Optional shared secret. Encrypted at rest; used for HMAC signing.
  • enabledboolean
    Defaults to true.
Request
bash
curl -X POST http://localhost:8000/api/v1/integrations/webhooks \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Eng Slack",
    "url": "https://hooks.slack.com/services/T0/B0/XXX",
    "kind": "slack"
  }'
GET/api/v1/integrations/webhooks#

List registered webhooks.

Request
bash
curl http://localhost:8000/api/v1/integrations/webhooks
DELETE/api/v1/integrations/webhooks/{id}#

Hard-delete a webhook.

Request
bash
curl -X DELETE http://localhost:8000/api/v1/integrations/webhooks/wh_01J...
POST/api/v1/integrations/webhooks/{id}/test#

Fire a synthetic ping at the receiver. Useful for verifying signing.

Request
bash
curl -X POST http://localhost:8000/api/v1/integrations/webhooks/wh_01J.../test
Response · 200 OK
json
{ "status": 200, "error": null }

Collaboration

Real-time multi-user editing piggy-backs on Yjs over a per-document WebSocket. The route is purely a relay — no auth, no message validation beyond passthrough.

GET/api/v1/collab/{document_id}#

WebSocket endpoint. Clients should be y-websocket compatible.

y-websocket client
js
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";

const ydoc = new Y.Doc();
const ws = new WebsocketProvider(
  "ws://localhost:8000/api/v1/collab",
  docId,
  ydoc,
);
ws.on("status", (e) => console.log(e.status));

Next steps