API reference

Plain REST over HTTPS with JSON responses. Tender search, contract awards, buyer summaries and signals are free GETs with no key. The checkers are free and rate-limited under a weekly cap (the live caps are at GET /v1/pricing). 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 → working 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 counts against your weekly limit.
  • 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.

Search open tenders

GET /v1/tenders

Free, no key. Open tenders across Australia; New Zealand rows only when you ask for them (jurisdiction=NZ). Federal, state, council, university and health. Grants ride the same search with kind=grant. Returns {total, results[], coverage}. The link to the original portal (link, url, listing_url, source_url) comes only with an API key or a signed-in session.

Read coverage. It names which sources were searched and which returned nothing, so an empty result means “no match in what we searched” — never “no such tender exists”. There is deliberately no location filter: it is populated on a small minority of rows against jurisdiction's every row, so filtering by it would silently hide most of the corpus.

ParameterTypeDescription
qqueryFree-text search across title, summary and buyer.
jurisdictionqueryA country (AU, NZ) or a subdivision: AU-NSW, AU-VIC, AU-QLD, AU-WA, AU-SA, AU-TAS, AU-ACT, AU-NT.
kindquerygrant for grant rounds from every grant source, tender for everything else; leave it out for both.
tier, source, categoryqueryFilter by government tier, the source registry id, or category.
buyerqueryThe organisation name as published, matched exactly (case-insensitive).
closing_beforequeryISO date — tenders closing on or before this day.
closing_within_daysquery0 to 366 — closing between today and today plus N, in the buyer’s own day. 0 is today. A tender with no closing date never matches.
changed_within_daysquery1 to 31 — a closing or published date changed in the last N days.
first_seen_afterqueryISO-8601 instant, strictly newer — what is new since your last look.
include_closedqueryDefault false. Open tenders only unless you ask otherwise.
limit, offsetqueryPaging.
bash
curl "https://www.stipple.sh/v1/tenders?jurisdiction=AU-QLD&q=road&limit=2"

One result, as captured from the live API on 15 September 2026:

json
{
  "total": 15,
  "results": [
    {
      "title": "RFT260091 - Capella Rubyvale Road Upgrade CH 47.3 - 48.6",
      "buyer": "CENTRAL HIGHLANDS REGIONAL COUNCIL",
      "jurisdiction": "AU-QLD",
      "tier": "council",
      "closing_date": "2026-09-21",
      "source_tag": "VendorPanel",
      "link": "https://www.vendorpanel.com.au/PublicTenderAccess.aspx?id=...",
      "tender_key": "17d2cc3cca27d22ef5874d9be40ab210"
    }
  ],
  "coverage": {
    "sources_total": 23,
    "sources_empty": [ { "source": "sa", "status": "blocked", "why": "login wall: ..." }, ... ],
    "last_run": "2026-09-14T12:32:04+00:00"
  }
}

One tender, open or closed

GET /v1/tenders/{key}

Free, no key. One tender by the page_key every search result carries (32 hex, or f + 32 hex for a tender that publishes no reference or buyer), in the same shape as a search result plus open — the search's own rule: no closing date, or a closing date of today or later in the buyer's time zone, taken from its state (Sydney for a tender labelled with the country alone). A closed tender still answers. An unknown key is a 404. An Australian tender's public page is /tenders/au/<slug>-<page_key>.

The link to a tender's original portal

GET /v1/tenders/{key}/portal

An API key or a signed-in session. The link to the tender's or grant's original portal: {url, portal_name, link_is_listing}. link_is_listing is true when the portal has no page per tender and the link opens the list it appeared on. 401 without a key or a session; 404 for an unknown key or a tender we hold no link for.

What a tender's page shows beside it

GET /v1/tenders/{key}/context

Free, no key. One read for everything around a tender: buyer (the buyer's nightly summary, its five most recent contract awards and five contracts ending soonest), similar (keywords chosen from the tender's title, the contract awards whose titles carry them with their count and published value range, and up to five open tenders like it, each with its page_key) and sources (attributions, computed_at and the award window). Either block is null when nothing is held.

The keywords are a heuristic, and the payload says so (match: {method: "heuristic", rule: "title_keyword"}): the rarest title word, matched as a whole word, that at least three contract awards carry, plus a second word only when the pair narrows the match. Supplier names are shown to a signed-in caller only, as on the contract award routes; each award says whether one is held (supplier_held). An unknown key is a 404; while the intelligence store loads the answer is 503.

Counts for the search page's quick views

GET /v1/tenders/strip

Free, no key. Three counts of open tenders: closing_48h (closing today or tomorrow), dates_changed_7d (a closing or published date changed in the last 7 days) and new_7d (first seen in the last 7 days). Each equals the total of the matching search (closing_within_days=1, changed_within_days=7, first_seen_after).

Where the tender data comes from

GET /v1/tenders/sources

Free. Every nightly and observed snapshot source, including its compact tag, full name, refresh mode, tender count, and most recent run or snapshot date; its URL with an API key or a signed-in session. This is the honesty surface — it names the sources behind login walls and the ones that returned nothing, which is usually the answer to “why did my search come back empty?”

Rank tenders against what a company does

POST /v1/tenders/match

Reads a company's website, builds a capability profile, and ranks open tenders against it. Free. Each match carries why[] — the company's own stated capabilities the tender needs — and gaps[], what the tender asks for that their site does not mention.

score is relative fit within these results, not a probability of winning. degraded: true means scoring was unavailable and the order is keyword relevance only, with no why/gaps — present it as unjudged rather than as ranked. An unauthenticated call returns the strongest few and counts the rest in withheld; shown + withheld == matched always holds.

ParameterTypeDescription
urlbodyA company website. A bare domain is fine — myrisk.io resolves.
examplebodyInstead of a url: civil, it or facilities. Skips the site read.
jurisdiction, closing_beforebodyOptional filters, same values as the search endpoint.

Signed in, POST /v1/tenders/watch saves any of these as a daily email: a search, a company profile, a buyer, a signal filter or a grants search — five of each kind, one email a day at most, nothing when nothing is new.

Contract awards and buyer intelligence

Awards as published — AusTender (Department of Finance, CC BY 3.0 AU) and the Queensland Government's contract disclosure reports (CC BY 4.0) — rolled up nightly by buyer, with the contracts ending within 12 months and buyers' planned procurements. Every payload carries computed_at, the night its figures are true for, and sources with the attribution each licence requires. Values are the published amount and currency, never converted.

Two rungs, one truncation. Counts, values, dates and buyers are public. The supplier or incumbent name and its identifier are shown to a signed-in caller (a free key or a session) and withheld from an anonymous one: withheld counts the records affected and withheld_reason carries the sentence to show. The same rule applies on the MCP tools. Until the store has loaded after a deploy, these routes answer 503 {"ready": false} — retry in a moment.

Contract awards

GET /v1/awards

ParameterTypeDescription
buyer, buyer_keyqueryThe buyer as published (exact), or its key from a previous answer.
supplier, supplier_keyqueryThe supplier as published, or its key (AU-ABN:<abn>).
qqueryFree-text search across title, description, buyer and supplier.
jurisdictionqueryA country matches everything in it; a subdivision matches itself.
sincequeryISO date — awarded on or after this day.
limit, offsetqueryPaging; limit up to 100.

Returns {total, results[], computed_at, sources, withheld, withheld_reason}. Each award carries contract_id, buyer, buyer_key, title, value_amount, value_currency, awarded_date, start_date, end_date, categories (UNSPSC), and — signed in or with a key — url, supplier, supplier_id and supplier_key.

The buyer cohort

GET /v1/awards/buyers

ParameterTypeDescription
countryqueryDefault AU.
orderquerycount (default) or value — rank by award_count or by value_total.

Every buyer with a public page for a country: at least 20 contract awards in the window or at least 5 open tenders, recomputed nightly. Returns {country, total, buyers[{buyer_key, name, slug, jurisdiction, award_count, open_tenders, expiring_12m, planned_12m, median_value, value_total, value_currency}], computed_at, sources}. A buyer page lives at /buyers/au/<slug>-<buyer_key>.

Buyers new to the corpus

GET /v1/awards/buyers/new

ParameterTypeDescription
sincequeryISO date. Required.
countryqueryDefault AU.

How many buyers were first seen in a contract award on or after since. Returns {since, count, computed_at}.

A buyer by the name a tender publishes

GET /v1/awards/buyers/lookup

ParameterTypeDescription
namequeryThe organisation name as published — what a tender’s buyer field holds.
jurisdictionqueryThe tender’s jurisdiction. Resolves under it first, then the federal key of the same country; never across states.
sourcequeryThe tender’s source_id. A source that files every tender under the country alone never resolves to a national buyer.

Returns the same payload as the buyer route below, or 404 when nothing is held for that name. When the exact name misses, two labelled heuristics are tried: the name without a trailing division code, and, for a tender labelled with the country alone, the one state the name names. Their result adds match, which names the rule used.

bash
curl "https://www.stipple.sh/v1/awards/buyers/lookup?name=Transport%20for%20NSW&jurisdiction=AU-NSW"

One buyer: the summary and what is behind it

GET /v1/awards/buyers/{buyer_key}

{buyer, expiring[], planned[], recent_awards[], top_suppliers[], open_tenders[], computed_at, sources}. The buyer object is the nightly summary: award_count and window_from (the start of the awards window the counts cover), median_value, p25_value, p75_value, value_currency, median_response_days (publication to close across its tenders), expiring_12m and planned_12m. top_suppliers is a name and a share, never a page. Buyers are one row per published spelling — no alias merging.

The entity behind an identifier

GET /v1/awards/suppliers/{identifier}

An ABN (digits only or spaced) or a scheme:id key. Returns {entity, contracts[], buyers[], categories[], standing, computed_at, sources}: the names seen, the registry number and standing, the contracts held, the buyers won from and the categories. The website's supplier page is for signed-in people only; this route is the same entity. Anonymous callers get the counts with the names and identifiers withheld.

What may be tendered before it is

GET /v1/signals

ParameterTypeDescription
kindquerycontract_expiry (a contract ending, with its incumbent), planned_procurement (a buyer’s stated plan with its quarter and spend band as published), or recurring_tender (derived from our own history).
jurisdiction, buyer, qqueryFilters; q searches the subject, buyer and incumbent.
window_beforequeryISO date — signals whose window starts on or before it.
window_afterqueryISO date — signals whose window has not ended by it; a window with no end date is kept.
first_seen_afterqueryISO-8601 instant — signals first seen after it.
limit, offsetqueryPaging.

Each signal carries confidence — published or derived, a vocabulary, not a score — window_start and window_end (never invented: an expiry's window is the contract's end date, and a planned row with no parseable quarter has none), value_text, evidence_ref, status (the source's own word for a plan, for example As Published or To Market; empty where the source gives none), released (true when the plan is marked To Market), and — signed in or with a key — evidence_url and incumbent. A plan its agency withdrew is never returned.

bash
curl "https://www.stipple.sh/v1/signals?kind=contract_expiry&jurisdiction=AU-QLD&limit=5"

Planned procurements, with counts by jurisdiction

GET /v1/intelligence/planned

The signals route filtered to planned_procurement, plus by_jurisdiction counts for building chips. Same filters and paging, and buyer_key, quarter (an id like 2026Q4 from by_quarter, later or ended; a well-formed id outside the current five answers an empty list), window_after (YYYY-MM-DD; a window with no end date is kept), sort (soon or buyer) and exact=1 (the jurisdiction code alone, not its states); a bad value returns 400. A withdrawn plan is never returned, and each row carries status, the agency's own word, and released. The facets ignore the row filters: figures, by_quarter, top_buyers and top_categories.

One planned procurement, in context

GET /v1/intelligence/planned/{plan_id}/context

The plan's buyer summary and that buyer's own contract awards for similar work, inside the summary's window: a count and how many end within 12 months, matched by words in the title (a heuristic, labelled by match). Counts only, no names.

The live block for a state or country

GET /v1/intelligence/{jurisdiction}

What the state pages render: {jurisdiction, name, expiring{total, federal_by_region, horizon_days, rows[]}, planned{total, quarter, rows[]}, buyers[], open_tenders, region_basis, computed_at, sources}. Commonwealth contracts are attributed to a state by the buyer's published office region, and region_basis says so.

The front page's one payload

GET /v1/intelligence/summary

{open_now, new_this_week, expiring_12m, planned_quarter, buyers_month, grants_open, sources_total, countries, by_jurisdiction, previews, computed_at}. Always the anonymous rung — counts and previews, no names.

Every trade and state, with its market page's figures

GET /v1/intelligence/trades

{pairs[{trade, jurisdiction, open_now, ending_12m, planned_quarter, year_awards, volume, eligible}], minimum, year, match, computed_at}, largest first. A trade is a labelled heuristic: whole words of a row's title and published category, and category code families. Counts only.

One trade in one state: what is coming up

GET /v1/intelligence/trades/{trade}/{state}

{open, ending{days_90, covers_90, days_365, by_month[{month, count, rows[]}], chart, rows[], calendar}, planned, recurring, buyers{rows[], by_value[]}, concentration, eligible, computed_at}. Every figure carries covers, the jurisdictions its rows came from. The open tenders are /v1/tenders?trade=&jurisdiction=&kind=tender. Supplier and holder names, and the re-tender calendar, with a key or a signed-in session.

Open grant rounds

GET /v1/grants/summary

From the tender corpus alone (GrantConnect): {open, closing_month, month_end, value_open, value_currency, with_value, by_jurisdiction, closing_soon[], computed_at, sources}. value_open sums only the rounds that state a value, and with_value says how many did. Each closing round carries its value, eligibility and GrantConnect's own type as published.

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. ..."
}

Check text against published sources

POST /v1/check-source-overlap

Finds whether submitted text overlaps text published on the public web — a plagiarism-style check. Exact matches are confirmed against the fetched source page and returned as quotes from both sides with char spans and the URL. A second tier — clearly-labelled model judgements on possible paraphrases, in possible_paraphrases, never mixed with exact matches — is currently disabled pending its evaluation gate: until it passes, results carry an empty list, paraphrase_checked: false, and a note saying so. Reports overlap, never accusations — no match is never proof of originality. Free, like every checker — counted against your weekly limit only when outcome: "assessed"; abstentions (applicable: false) and search outages don't count. Stateless — nothing is stored.

ParameterTypeDescription
file | text | bytes_b64 | urlbodyThe document or prose to check. A url’s own site is excluded from matches.
json
{
  "outcome": "assessed",
  "overlap_band": "notable",
  "overlap_ratio": 0.14,
  "matches": [{
    "tier": "exact",
    "source_url": "https://example.com/reference-handbook",
    "matched_quote": "...",
    "source_quote": "...",
    "passage_span": {"start_char": 118, "end_char": 214},
    "tokens_matched": 17
  }],
  "possible_paraphrases": [],
  "paraphrase_checked": false,
  "note": "Paraphrase comparison is not enabled on this deployment — the exact-match tier still ran.",
  "top_sources": [...],
  "headline": "Matching published text found at 1 source ...",
  "limitations": "Public-web overlap only ... never an originality certificate. ..."
}

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, contract); neither (auto — classify, then apply that type's fields); or auto on an unrecognised type (schema-free — every labelled field). Reads checkboxes (type: boolean). Extraction, not verification — values are what the document shows; the document is never stored.

options turns on extra capabilities, every one off by default — and asking for one this server does not support is an error naming it, never a silent no-op. tables returns whole tables as rows: on a born-digital PDF they are read deterministically from the document's own coordinates (exact cells, merged-cell colspans, no charge for the table pass); on a scan the vision model reads the rows and the table says source: "vlm". grounding adds a bounding box and text_layer_match to every value the PDF's own text layer can locate — a scan has no coordinate source yet, so its values return bbox: null. classify and redact fold the classification verdict and the masked PII inventory into the same call. layout, links and figures return the document's structure; chunks returns retrieval-ready chunks (six strategies, heading path and position on every chunk; tables are kept whole by default); split returns the page ranges of the distinct documents in one file; flag_below lists the fields under a confidence threshold. GET /v1/extract/capabilities (free, no key) is the machine-readable contract for exactly what this server supports.

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|question}. As a query param, repeat ?fields=.
templatebody / queryOptional preset: payslip | tax_invoice | bank_statement | receipt | contract.
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). Also caps the charge.
optionsbody / queryCapabilities to turn on: tables, grounding, classify, redact, layout, links, figures, chunks, split, flag_below. JSON body takes an object (tables and chunks tune with nested settings); a query string sets each as a flat param, e.g. ?tables=true&grounding=true.
render_scalebody / queryRasterisation scale for small or dense print: 1.0 | 1.5 | 2.0 (default) | 3.0 | 4.0.
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,
  "page_count": 1,
  "capabilities_used": ["tables.native_pdf"],
  "tables": [ { "page": 1, "source": "native_pdf", "header": ["Description", "Amount"],
                "json": [ { "Description": "Consulting", "Amount": "1000.00" } ] } ],
  "limitations": "Extraction, not verification - values are what the document shows. ..."
}

Free, like every checker — counted against your weekly limit once per call regardless of page count or which options (grounding, layout, links, figures, chunks, redact, split, classify) are requested.

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. ..."
}

Screen adverse media & sanctions

POST /v1/adverse-media

Screen a person or organisation for adverse media and sanctions exposure (PEP is not screened, and the reply says so). Two layers: a deterministic, corroboration-gated match against four primary sanctions lists (OFAC, EU, UK, AU DFAT), 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 match (verify); 1 matched + 0 possible adverse-media hits (fraud); 2 same-name false matches excluded.",
  "limitations": "Screening COVERAGE, not a determination. ..."
}

Resolve a company, and check its standing

POST /v1/companies/resolve

Work out which Australian company a name or number refers to, and whether it is in good standing. Give a name (Acrux Ltd) or an identifier (ABN, ACN or NZBN). An identifier resolves directly — no name search, no ranking — so an exact number can never land on a near-miss. A name only ever produces candidates.

Read status before candidates. ambiguous means several real companies match, and selected is null on purpose — choosing between them is yours to do, with the evidence in front of you. not_found means every register answered and none has it; inconclusive means one could not be read, so absence proves nothing. coverage lists every source and how well it was read, including the ones that were blocked.

selected.asic carries registration standing from ASIC's own register. An active ABN is not the same as a registered company — they are different registers answering different questions, and a company in external administration can still hold an active ABN. Standing is republished weekly, so every answer carries as_of and age_days. It does not include directors or officeholders.

ParameterTypeDescription
querybodyA company name, or an ABN / ACN / NZBN. A number whose check digit fails is reported as a typo, not searched for as a name.
jurisdictionbodyOptional hint (AU, NZ). New Zealand identifiers currently return inconclusive — there is no NZ register adapter yet.
json
{
  "query": "AQC Dartbrook Pty Ltd",
  "parsed_as": "name",
  "status": "resolved",
  "resolved_because": "exact registered-name match, and the only one among 3 results",
  "selected": {
    "identifiers": { "abn": "46000012813", "acn": "000012813" },
    "names": { "legal": "AQC DARTBROOK PTY LTD" },
    "abn_status": "Active",
    "asic": {
      "status": "EXAD",
      "status_meaning": "External administration",
      "in_good_standing": false,
      "registered": "1923-03-01",
      "former_names": ["ANGLO COAL (DARTBROOK) PTY LTD", "THE BELLAMBI COAL CO LTD"],
      "as_of": "2026-08-24T14:53:11", "age_days": 3,
      "caveat": "ASIC standing as of 2026-08-24T14:53:11. This copy of ASIC's file is no longer refreshed, so the status may have changed since; ASIC Connect is the real-time source."
    }
  },
  "candidates": [ /* every match, ranked; match_quality orders them and never selects */ ],
  "coverage": [
    { "source": "abr", "status": "ok", "detail": "2 rows of 3 matches" },
    { "source": "asic", "status": "ok", "detail": "4,002,081 companies, 3d old" }
  ]
}

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

Free and instant, from a signed-in account (the key shares the account's weekly limit): the key is returned once, stored only as a hash, with its key_id. Up to 5 keys per account. See Authentication.

Metered usage

GET /v1/usage

Requires Authorization: Bearer stp_…. Returns your usage this week and when your weekly limit resets — read from the same store that enforces the limit.

Authentication

Metered calls need an account-owned API key (free, no card) — send it as Authorization: Bearer stp_… on any endpoint. Reads, tender browsing and the sandbox need nothing. An invalid key is a hard 401, never a silent fallback.