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 (received→workingheartbeats →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
detailmessage — 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 headerStp-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-Keyheader on a POST so a retry replays the stored response instead of processing twice (a reused key with a different body is a422). - Rate-limit headers: every response carries
RateLimit-Limit/RateLimit-Remaining/RateLimit-Resetso 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.
| Parameter | Type | Description |
|---|---|---|
| file | text | bytes_b64 | url | body | The document: multipart file, raw prose, inline base64, or a public URL. |
| deep | query, bool | Also fetch each cited source and judge support/contradiction (slower, ~a minute). |
| stream | query, bool | Return an SSE lifecycle stream instead of a single response. |
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.
curl https://www.stipple.sh/v1/fact-checks/fc_6d22fb4033cbac40Detect 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.
| Parameter | Type | Description |
|---|---|---|
| file | text | bytes_b64 | url | body | The document or prose to analyse. |
{
"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.
| Parameter | Type | Description |
|---|---|---|
| file | bytes_b64 | url | body | The document: multipart PDF/image, inline base64, or a direct public document URL (fetched raw, SSRF-guarded). |
{
"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.
| Parameter | Type | Description |
|---|---|---|
| file | bytes_b64 | url | body | The document: multipart PDF/image, inline base64, or a public URL (SSRF-guarded). |
| fields | body / query | Optional ad-hoc field list — names, or {name, type, description}. As a query param, repeat ?fields=. |
| template | body / query | Optional preset: payslip | tax_invoice | bank_statement | receipt. |
| country | body / query | Optional hint (e.g. AU, IN, US) to sharpen the read. |
| max_pages | body / query | Pages to read per document (default a few; hard ceiling 10). |
{
"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.
| Parameter | Type | Description |
|---|---|---|
| files | body, multipart | One or more documents (PDF/image). Or JSON documents[] (below). |
| documents | body, JSON | Array of {"url": "…"} or {"bytes_b64": "…", "filename": "…"} (up to 10). |
| scheme | query | body | afp_100_point (default) or austrac_safe_harbour. |
{
"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.
| Parameter | Type | Description |
|---|---|---|
| files | body, multipart | One or more documents (PDF/image), up to 12. Or JSON documents[] (below). |
| documents | body, JSON | Array of {"url": "…"} or {"bytes_b64": "…", "filename": "…"}. |
| scheme | query | body | income_proof | lending_prequal | rental_application. |
| requirements | query | body | Ad-hoc checklist: type names, or {key, accepts:[types], optional}. As a query param, repeat ?requirements=. |
{
"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.
| Parameter | Type | Description |
|---|---|---|
| file | bytes_b64 | url | body | The document: multipart PDF/image, inline base64, or a public URL (SSRF-guarded). |
| max_pages | body / query | Pages to read for the vision pass (default a few; hard ceiling 10). The deterministic pass covers all text. |
{
"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.
| Parameter | Type | Description |
|---|---|---|
| name | body | The subject to screen. Provide this, OR a document to read the subject from. |
| dob, country, aliases, employer, role | body | Optional disambiguators — they sharpen matching and cut same-name false positives. |
| file | bytes_b64 | url | body | Alternatively, an identity document (multipart / base64 / public URL) to read the subject from. |
{
"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.
| Parameter | Type | Description |
|---|---|---|
| file | bytes_b64 | body | The document (PDF or image). URL intake is not available on this endpoint. |
| stream | query, bool | SSE lifecycle stream — recommended for images (deep visual analysis is slower). |
| fresh | query, bool | Bypass 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
Authorization: Bearer stp_… on any endpoint. An invalid key is a hard 401, never a silent fallback.