API reference

Plain REST over HTTPS with JSON responses. No authentication while the service is free — limits apply per IP. All endpoints live under https://www.stipple.sh.

Conventions

  • Document inputs are accepted three ways: multipart file, JSON {"bytes_b64": "...", "filename": "..."}, or JSON {"url": "https://..."} (public hosts only; fetched server-side with strict SSRF protections; analysis endpoints only).
  • Long-running calls support ?stream=1 — a Server-Sent Events stream (receivedworking heartbeats → result) so proxies never time out mid-analysis.
  • Identical bytes are cached by content hash — re-submitting the same document is free and instant.
  • Errors use standard status codes with a JSON detail message — see Errors & limits.
  • OpenAPI: the full machine-readable contract is published at https://www.stipple.sh/openapi.json — generate a typed client or import it into your tooling.
  • Sandbox: add ?sandbox=true (or the header Stp-Sandbox: true) to any tool for a deterministic, clearly-labelled fixture response ("sandbox": true) — free, and it never spends your daily quota.
  • Idempotency: send an Idempotency-Key header on a POST so a retry replays the stored response instead of processing twice (a reused key with a different body is a 422).
  • Rate-limit headers: every response carries RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset so a client can self-throttle.

Fact-check a document

POST /v1/verify-references

Verifies a document's citations (live / archived / dead, scholarly papers matched against their claimed titles), recomputes its internal arithmetic, and — in deep mode — fetches each cited source to judge whether it supports the claim. Results persist under a content-hash id and include a public shareable permalink.

ParameterTypeDescription
file | text | bytes_b64 | urlbodyThe document: multipart file, raw prose, inline base64, or a public URL.
deepquery, boolAlso fetch each cited source and judge support/contradiction (slower, ~a minute).
streamquery, boolReturn an SSE lifecycle stream instead of a single response.
bash
curl -X POST "https://www.stipple.sh/v1/verify-references?deep=1&stream=1" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/report.pdf"}'

Key response fields: summary (totals + headline), references[], claims[], internal_consistency[], limitations, check_id, permalink.

Retrieve a fact-check

GET /v1/fact-checks/{check_id}

Fetch a stored fact-check result by its content-hash id (fc_ + 16 hex). This is the data behind the public /fc/<slug>-<hex> permalink pages.

bash
curl https://www.stipple.sh/v1/fact-checks/fc_6d22fb4033cbac40

Detect AI-written text

POST /v1/detect-ai-text

Estimates the probability that a document's prose was AI-written, with the specific linguistic tells. Abstains (applicable: false) on forms, tables, and scans, where style detection is unreliable. Stateless — nothing is stored.

ParameterTypeDescription
file | text | bytes_b64 | urlbodyThe document or prose to analyse.
json
{
  "applicable": true,
  "probability": 0.92,
  "lean": "ai",
  "tells": ["uniform paragraph rhythm", "hedged superlatives", ...],
  "reasoning": "...",
  "limitations": "Probability is a confidence, not a calibrated truth. ..."
}

Classify a financial document

POST /v1/classify

Identify a document's type — we specialise in financial documents: payslip, tax_invoice, bank_statement, salary_certificate, payg_summary, receipt — with issuing country and confidence. Anything non-financial returns other; below the confidence threshold it abstains as unknown rather than guessing. Stateless; type only, never an authenticity judgment.

ParameterTypeDescription
file | bytes_b64 | urlbodyThe document: multipart PDF/image, inline base64, or a direct public document URL (fetched raw, SSRF-guarded).
json
{
  "document_type": "payslip",
  "country_code": "AU",
  "confidence": 0.93,
  "is_financial_document": true,
  "evidence": ["Gross Pay", "PAYG withheld", "Superannuation"],
  "supported_types": ["bank_statement", "payg_summary", "payslip", ...],
  "limitations": "Type classification only — NOT an authenticity or fraud judgment. ..."
}

Extract document fields

POST /v1/extract

Pull structured fields out of any document. A vision model reads up to a few pages and returns the values you ask for. Say what to pull four ways: an ad-hoc fields list (names, or {name, type, description}, where type is text / amount / date / boolean); a template (payslip, tax_invoice, bank_statement, receipt); neither (auto — classify, then apply that type's fields); or auto on an unrecognised type (schema-free — every labelled field). Reads checkboxes (type: boolean), merged-cell tables, and nested tables. Extraction, not verification — values are what the document shows; the document is never stored.

ParameterTypeDescription
file | bytes_b64 | urlbodyThe document: multipart PDF/image, inline base64, or a public URL (SSRF-guarded).
fieldsbody / queryOptional ad-hoc field list — names, or {name, type, description}. As a query param, repeat ?fields=.
templatebody / queryOptional preset: payslip | tax_invoice | bank_statement | receipt.
countrybody / queryOptional hint (e.g. AU, IN, US) to sharpen the read.
max_pagesbody / queryPages to read per document (default a few; hard ceiling 10).
json
{
  "mode": "fields",
  "document_type": "document",
  "fields": {
    "gross_pay": { "value": 5800, "confidence": 0.95, "page": 1 },
    "consent_checkbox": { "value": "checked", "confidence": 0.98, "page": 1 }
  },
  "not_found": ["middle_name"],
  "pages_read": 1,
  "page_limit": 3,
  "limitations": "Extraction, not verification - values are what the document shows. ..."
}

Verify identity (AU 100-point)

POST /v1/identity-check

Run an Australian identity check over a set of documents. A vision model reads each one — its type, which identity fields it shows (name / photo / address / signature), and its issue date — then a deterministic engine tallies them against a scheme: afp_100_point (points, the default) or austrac_safe_harbour (category combinations). It reports whether identity is established and exactly what's missing if not. Identity coverage, not a forgery judgment — run /v1/warrants for authenticity. Stateless; documents are never stored.

ParameterTypeDescription
filesbody, multipartOne or more documents (PDF/image). Or JSON documents[] (below).
documentsbody, JSONArray of {"url": "…"} or {"bytes_b64": "…", "filename": "…"} (up to 10).
schemequery | bodyafp_100_point (default) or austrac_safe_harbour.
json
{
  "established": true,
  "rule": "points_accumulation",
  "points": 110,
  "target": 100,
  "primaries": 2,
  "reason": "Identity established: 110 points with 2 primary document(s).",
  "documents": [
    { "classified_type": "australian_passport", "shows": ["N", "P"],
      "points": 70, "counts": true, "note": "+70 pts (primary)" },
    { "classified_type": "drivers_licence", "shows": ["N", "A", "P"],
      "points": 40, "counts": true, "note": "+40 pts (primary)" }
  ],
  "limitations": "Verification COVERAGE, not a legal identity decision. ..."
}

Check a document pack (completeness)

POST /v1/check-pack

Does a set of documents satisfy a checklist? Each document is classified (one cheap page-1 read, hash-cached — never field extraction or multi-page reads), then a deterministic engine maps the types onto the checklist's required slots and reports what's present and what's missing. Supply a named scheme (income_proof, lending_prequal, rental_application) or an ad-hoc requirements checklist. Completeness coverage, not approval — that the right document types are present, not that any is genuine.

ParameterTypeDescription
filesbody, multipartOne or more documents (PDF/image), up to 12. Or JSON documents[] (below).
documentsbody, JSONArray of {"url": "…"} or {"bytes_b64": "…", "filename": "…"}.
schemequery | bodyincome_proof | lending_prequal | rental_application.
requirementsquery | bodyAd-hoc checklist: type names, or {key, accepts:[types], optional}. As a query param, repeat ?requirements=.
json
{
  "scheme": { "id": "lending_prequal", "name": "Lending pre-qualification" },
  "complete": false,
  "slots": [
    { "key": "proof_of_income", "label": "Proof of income", "satisfied": true, "matched": "payslip.pdf" },
    { "key": "bank_statement", "label": "Recent bank statement", "satisfied": false, "matched": null }
  ],
  "missing": ["bank_statement"],
  "documents": [{ "filename": "payslip.pdf", "classified_type": "payslip" }],
  "unmatched_documents": [],
  "limitations": "Completeness COVERAGE, not approval - that the right document types are present. ..."
}

Detect & redact PII

POST /v1/redact

Find and mask personal data in a document. A deterministic regex+checksum pass catches structured identifiers — emails, payment cards (Luhn), US SSNs, India PANs, AU ABNs — and a vision model catches the unstructured PII: names, addresses, dates of birth, phone numbers, and photo/signature presence. Values are masked in the response (the raw PII is never returned), and a redacted copy of the text is included when the document has a text layer. Detection coverage, not a guarantee — it may miss PII or over-flag.

ParameterTypeDescription
file | bytes_b64 | urlbodyThe document: multipart PDF/image, inline base64, or a public URL (SSRF-guarded).
max_pagesbody / queryPages to read for the vision pass (default a few; hard ceiling 10). The deterministic pass covers all text.
json
{
  "pii_found": 4,
  "by_type": { "email": 1, "person_name": 1, "us_ssn": 1, "payment_card": 1 },
  "items": [
    { "type": "email", "masked": "j***@e***", "method": "regex" },
    { "type": "person_name", "masked": "J***", "method": "vlm" }
  ],
  "redacted_text": "... [EMAIL] ... [US_SSN] ...",
  "has_photo": false,
  "has_signature": true,
  "limitations": "DETECTION COVERAGE, not a guarantee - it may miss or over-flag. ..."
}

Screen adverse media & sanctions/PEP

POST /v1/adverse-media

Screen a person or organisation for adverse media and sanctions/PEP exposure. Two layers: a deterministic, corroboration-gated match against consolidated global sanctions & PEP lists, and an entity-disambiguated scan of public news for adverse coverage (fraud, money laundering, bribery, sanctions, enforcement). Same-name articles about other people are surfaced under excluded, and a name-only sanctions hit is flagged possible, never confirmed. Screening coverage, not a determination — a hit means “review”, and “nothing found” is not a clean record.

ParameterTypeDescription
namebodyThe subject to screen. Provide this, OR a document to read the subject from.
dob, country, aliases, employer, rolebodyOptional disambiguators — they sharpen matching and cut same-name false positives.
file | bytes_b64 | urlbodyAlternatively, an identity document (multipart / base64 / public URL) to read the subject from.
json
{
  "subject": { "name": "Jane Doe", "source": "provided", "country": "AU" },
  "sanctions": {
    "candidates": [
      { "name": "Jane Doe", "score": 0.93, "strength": "possible",
        "corroboration": "name-only", "topics": ["role.pep"], "datasets": 3 }
    ],
    "strong_matches": 0, "possible_matches": 1, "common_name_ambiguity": false
  },
  "adverse_media": {
    "matched_hits": 1, "possible_hits": 0, "excluded_same_name": 2,
    "categories": { "financial_crime": 1 },
    "hits": [
      { "url": "https://…", "title": "…", "source": "law360.com",
        "match": "match", "category": "financial_crime", "severity": "convicted",
        "evidence": "found guilty of fraud" }
    ],
    "excluded": [ { "url": "https://…", "title": "…", "reason": "different person (same name)" } ]
  },
  "risk_flag": "adverse_findings",
  "headline": "Jane Doe: 1 possible sanctions/PEP match; 1 matched adverse-media hit; 2 same-name false matches excluded.",
  "limitations": "Screening COVERAGE, not a determination. ..."
}

Inspect a document (forensics)

POST /v1/warrants

Runs the full forensic inspection — tampering signals, structure and metadata analysis, arithmetic reconciliation — and returns a warrant: the document's verification record. The warrant is a signal for review, not a fraud verdict.

ParameterTypeDescription
file | bytes_b64bodyThe document (PDF or image). URL intake is not available on this endpoint.
streamquery, boolSSE lifecycle stream — recommended for images (deep visual analysis is slower).
freshquery, boolBypass the content-hash cache and re-inspect.

Key response fields: warrant_id, risk_band (low / medium / high / insufficient / error), inspection_quality, recommended_action, summary, signals[].

Cache check

GET /v1/warrants/check?sha256={hex}

Has this exact document already been inspected? Hash the file yourself (SHA-256, lowercase hex) and check before submitting — identical bytes return the existing warrant for free.

Retrieve a warrant

GET /v1/warrants/{warrant_id}

Fetch a stored warrant by id (warrant_ + 16 hex). Add ?format=md for a human-readable Markdown report.

PDF report

GET /v1/warrants/{warrant_id}/report.pdf

An audit-ready, branded PDF scorecard of the warrant — suitable for case files.

Warrant feedback

POST /v1/warrants/{warrant_id}/feedback

Record a reviewer's verdict on a warrant: {"verdict": "up" | "down", "note": "..."}. Feedback improves the engine's precision over time.

Tool feedback

POST /v1/feedback

Thumbs up/down on a fact-check or AI-text result: {"tool": "detect_ai_text" | "verify_references", "verdict": "up" | "down"}, with optional ref and note.

Health

GET /health

Liveness and version: {"status": "ok", "engine_version": "...", ...}

Create an API key

POST /v1/keys

Self-serve, free, instant: {"email": "you@company.com"} → the key (returned once, stored only as a hash), its key_id, and your daily limit. Up to 5 keys per email. See Authentication.

Metered usage

GET /v1/usage

Requires Authorization: Bearer stp_…. Returns today's used/remaining plus the last 7 days — read from the same counters that enforce the quota.

Authentication

Anonymous calls work everywhere (20 documents/day per IP). A free API key gives you your own 50/day quota and usage metering — send it as Authorization: Bearer stp_… on any endpoint. An invalid key is a hard 401, never a silent fallback.