DisclosureLens
Developers

API documentation

Query every formally-filed breach disclosure over a REST/JSON API. Read-scoped keys are free for public-interest use.

A near-real-time, machine-readable feed of formally-filed breach disclosures worldwide — SEC Item 1.05, US state-AG notifications, HHS OCR, EU DPA decisions, and ransomware leak claims. REST, JSON, cursor-paginated. Every response carries meta.ai_assisted: true (EU AI Act Art. 50).

Base URL & authentication

Every data endpoint is under a single base. Send your key as a Bearer token on every request (health endpoints are the only ones that work without one). The few paths that sit at the host root instead — the liveness probe and the public spec — are written out in full in the tables below.

https://api.disclosurelens.com/v1

Authorization: Bearer df_your_key_here

Keys are read-scoped, hashed at rest, and shown to you only once at creation. Create and manage them under Settings → API keys.

Quick start

curl -s "https://api.disclosurelens.com/v1/disclosures?limit=3&jurisdiction=us-ca" \
  -H "Authorization: Bearer $DL_API_KEY"

Response envelope — a data payload plus a meta block:

{
  "data": [ /* … */ ],
  "meta": {
    "cursor": { "next": "<opaque>", "prev": null },
    "ai_assisted": true,
    "attribution": "Data: DisclosureLens (https://disclosurelens.com)"
  }
}

Pagination

List endpoints return up to limit rows (1–200, default 50). Disclosures and incidents use keyset cursors — pass the value from meta.cursor.next (disclosures) or meta.next_cursor (incidents) back as ?cursor= for the next page; a null value means the last page under the default filed_desc sort. The change-feed sorts updated_asc and created_asc are cursor-paginated too; the ranking sorts (severity, ttd, confidence) and filed_asc are single-page and always return a null cursor. created_desc ("recently added", ingestion-time browse) is cursor-paginated, and meta.cursor.prev is a real backward cursor for the previous page. Sort values also accept column:direction spellings (filed_at:desc normalizes to filed_desc). Threat actors use ?limit/?offset instead.

Change feed — keeping a copy in sync

To mirror the corpus, poll on updated_after — not filed_after. Filing dates belong to the regulator, and most of this corpus arrives through archive backfill, so a record's filing date is typically far older than the moment it reached us: the median gap between the two is over a year for state-AG notifications and over four years for HHS OCR. A poller keyed on filed_after would see leak-site claims and almost nothing else, and would miss backdated records permanently — they are born already behind a window that has moved past them.

updated_after covers new records and revisions (re-extraction, entity resolution, incident linkage). Each row carries record.updated_at and record.created_at; store the last value you durably wrote and pass it back on the next poll. Use created_after instead when you want a reproducible snapshot, where later revisions must not move a record into or out of your set.

# page forward through everything changed since your last sync
curl -H "Authorization: Bearer $DL_KEY" \
  "https://api.disclosurelens.com/v1/disclosures?updated_after=2026-07-30T00:00:00Z&sort=updated_asc&limit=200"

# then follow meta.cursor.next until it is null, and persist the last
# record.updated_at you saw as the watermark for the next run.

Records are delivered at least once: a row revised while you are paging reappears later in the walk under its new timestamp. Deduplicate on id.

The same walk exists on /v1/incidents (?updated_after= + sort=updated_asc) for mirroring the deduplicated one-row-per-breach view — an incident's updated_at bumps whenever a filing joins the cluster or its aggregates revise.

Retractions: a record retracted after you mirrored it is removed from list results rather than re-delivered, so the walk alone won't tell you. To detect removals, re-fetch stored ids you care about — the detail endpoint returns 404 for a retracted record; treat that as a tombstone and drop your copy. (A record restored on appeal re-enters the walk under a fresh timestamp automatically.)

Rate limits

The free inquiry tier allows 5 requests per minute per API key — enough to explore the API, not to mirror it. The bucket is keyed on the key itself (minting a second key gives you a second bucket; we ask that you not treat that as the plan). Browsing the dashboard while signed in is metered separately and far more generously, so the limit never affects reading. Paid tiers raise the key ceiling: casework 600/min; watchtower and portfolio 6,000/min; insurance, enterprise and wholesale 60,000/min. Over the limit returns 429 with a retry-after header.

Every response carries x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset (lowercase — HTTP/2 style), plus x-request-id for support correlation. If the limiter's backing store is briefly unavailable, requests pass unmetered and those headers are absent — their absence is not an error. Higher throughput and the signed PDF deliverables are part of the paid tiers; bulk export and webhooks work on any signed-up account, with per-tier caps described in their sections below.

Endpoints

Disclosures — individual filings
GET/disclosuresList disclosures
GET/disclosures/facetsFacet bucket counts
GET/disclosures/{disclosure_id}Single disclosure with classification
GET/disclosures/{disclosure_id}/relatedRelated disclosures for the same incident
GET/disclosures/{disclosure_id}/extractionExtraction provenance for a disclosure
Incidents — deduplicated breaches
GET/incidentsList incidents
GET/incidents/{incident_id}Single incident with linked disclosures
GET/incidents/{incident_id}/litigation_timelineIncident litigation timeline· paid tier
GET/incidents/{incident_id}/cross_filing_comparisonCross-filing comparison matrix· paid tier
Entities — organizations
GET/entitiesSearch entities by name or identifier
POST/entities/resolve/batchResolve a batch of raw names to entities (read-only)· watchtower tier
GET/entities/browseName-ordered entity browse index
GET/entities/{entity_id}Single entity profile
GET/entities/{entity_id}/disclosuresDisclosures linked to an entity
GET/entities/{entity_id}/scorecardEntity compliance scorecard
Stats — aggregate analytics
GET/stats/compliance_distributionCompliance flag distribution
GET/stats/time_to_disclose_percentilesTime-to-disclose percentiles by vertical
GET/stats/disclosure_timing_by_jurisdictionDisclosure-timing benchmark by regime and state
GET/stats/late_disclosure_leaderboardLate-disclosure entity leaderboard
GET/stats/compliance_trendMonthly compliance trend
GET/stats/overviewCorpus overview counts and freshness
GET/stats/dailyDaily filing counts by source type
GET/stats/jurisdictionsFiling counts by jurisdiction
GET/stats/geographyGeography Stats
GET/stats/source_healthPer-source freshness summary
GET/stats/time_to_discloseTime-to-disclose histogram
GET/stats/records_affectedRecords-affected distribution
GET/stats/data_typesCounts per affected data type
GET/stats/data_elementsCounts per statutory data element
GET/stats/top_cvesTop CVEs by mention count
GET/stats/data_quality_eventsData-quality events in a window
GET/stats/extraction_qualityReview-status mix by source type
GET/stats/incidents_by_merge_methodIncident counts by merge method
GET/stats/incidents_jurisdictions_cardinalityIncident jurisdiction-count bands
GET/stats/incidents_lead_timeLeak-to-filing lead-time percentiles
GET/stats/press_firstPress-first incidents + press→filing lead time
GET/stats/claim_corroborationShare of leak-site claims later corroborated
GET/stats/translated_countMachine-translated records in a window
GET/stats/incidents_merge_confidenceIncident merge-confidence buckets
GET/stats/incidents_source_combosIncident counts by source combination
GET/stats/frequency_severityVertical by severity-tier crosstab
GET/stats/records_affected_bandsRecords-affected bands with Wilson CI
GET/stats/data_types_mix_shiftMonthly data-type mix shift
GET/stats/repeat_offendersRepeat-offender entity ranking
GET/stats/source_cadencePer-source monthly cadence with anomalies
GET/stats/enforcementRegulator enforcement rollup
GET/stats/source_archivePreserved-archive size and cross-source utility for one source
Analytics — paid tier
POST/analytics/comparable_incidentsComparable-incident cohort stats· paid tier
GET/analytics/unreported_claimsUnreported leak-site claims early warning· paid tier
POST/analytics/underwriting_briefBroker underwriting brief· paid tier
POST/analytics/freq_severity_curveNotification-exposure (records) log-normal fit· paid tier
POST/analytics/portfolio_ratingPortfolio risk rating· paid tier
GET/analytics/entity_scorecard.pdfSigned entity scorecard PDF· paid tier
GET/analytics/compliance_report.pdfSigned compliance report PDF· paid tier
GET/analytics/broker_benchmark_letter.pdfSigned broker benchmark letter PDF· paid tier
POST/analytics/portfolio_scorecardPortfolio compliance scorecard· watchtower tier
GET/analytics/portfolio_scorecard.pdfSigned portfolio compliance scorecard PDF· watchtower tier
GET/analytics/entity_compliance_distributionEntity position in its sector's clock distribution· paid tier
GET/analytics/evidence_package.pdfSigned incident evidence package PDF· paid tier
Bulk export
GET/export/disclosures.ndjsonStreaming NDJSON export
Source documents
GET/disclosures/{disclosure_id}/source/manifestSource artifact manifest
GET/disclosures/{disclosure_id}/source/artifacts/{artifact_id}Archived source artifact body
Threat actors
GET/threat-actorsList ransomware groups with victim counts
GET/threat-actors/{slug}Enriched threat-actor profile
Supply chain — vendor breach cascades
GET/supply-chain/cascadesList confirmed supply-chain cascades
GET/supply-chain/cascades/{cascade_id}One confirmed cascade with its members
Vendor scan — a company's third parties, from its own DNS
GET/scanName a domain's third-party vendors and check them for breach records
GET/scan/connectionName the connectivity vendors behind a caller's own IP
Email subscriptions
GET/me/subscriptions/decayWatched-entity decay proposals
GET/me/subscriptionsList my email subscriptions
POST/me/subscriptionsCreate an email subscription (idempotent)
PATCH/me/subscriptions/{subscription_id}Update an email subscription
DELETE/me/subscriptions/{subscription_id}Delete an email subscription
Watchlists — named vendor sets
GET/me/watchlistsList my watchlists
POST/me/watchlistsCreate a watchlist (idempotent on name)
GET/me/watchlists/{watchlist_id}Get a watchlist with its entries and channels
PATCH/me/watchlists/{watchlist_id}Rename a watchlist
DELETE/me/watchlists/{watchlist_id}Delete a watchlist (deactivates its channels)
POST/me/watchlists/{watchlist_id}/entriesAdd entries to a watchlist (bulk, idempotent)
DELETE/me/watchlists/{watchlist_id}/entries/{entity_id}Remove one entry from a watchlist
POST/me/watchlists/{watchlist_id}/subscribeCreate the email alert for a watchlist
GET/me/watchlists/{watchlist_id}/decayWatchlist decay proposals
Your API keys
GET/me/api-keysList my API keys
POST/me/api-keysCreate an API key (secret shown once)
DELETE/me/api-keys/{key_id}Revoke an API key
Health — public, no key required
GET/healthLiveness probe
GEThttps://api.disclosurelens.com/healthzLiveness probe
GET/health/readyReadiness probe (DB connectivity)
GET/health/pdfPDF signing cert health
GET/health/extractLLM extraction pipeline health
GET/health/entity-resolutionEntity-resolution pipeline health
GET/health/dedupIncident-dedup pipeline health
GET/health/sourcesPer-source ingestion health
Webhooks — push delivery
GET/me/webhooksList my webhook endpoints
POST/me/webhooksCreate a webhook endpoint (idempotent)
GET/me/webhooks/{endpoint_id}/secretReveal an endpoint's signing secret
POST/me/webhooks/{endpoint_id}/rotate-secretRotate an endpoint's signing secret
PATCH/me/webhooks/{endpoint_id}Update a webhook endpoint
DELETE/me/webhooks/{endpoint_id}Delete a webhook endpoint
GET/me/webhooks/{endpoint_id}/deliveriesRecent delivery attempts for an endpoint
POST/me/webhooks/{endpoint_id}/testSend a test event to an endpoint
Meta
GEThttps://api.disclosurelens.com/openapi.jsonCurated public OpenAPI spec — no key needed. The full spec is at /v1/openapi.json with a key.

Filtering /disclosures

Combine any of these query params (all optional):

jurisdictione.g. us-ca, us-federal, global (canonical lowercase)
source_typesec_8k · state_ag · ocr · leak_site · press · gdpr_dpa
source_categoryclaim · report · filing · enforcement
severitylow · medium · high · critical
naics_sector2-digit NAICS, e.g. 62 (health care)
verticalrepeatable: ?vertical=healthcare&vertical=manufacturing
compliancee.g. ca_60day_late (repeatable)
cveexact CVE-YYYY-NNNN
actor_namedransomware group / actor name
searchfull-text over victim + summary (q is an accepted alias; respects the leak-site default below)
include_leak_siteDEFAULT false — leak-site claims and press reports are EXCLUDED from every list, search, and entity filing history unless you pass true. An entity whose only records are claims returns an empty page without it.
victim_entity_idexact entity id, e.g. ent_… (the resolved victim; not entity_id)
filed_after / filed_beforeISO-8601. Bounds the REGULATOR's filing date — see Change feed
updated_after / updated_beforeISO-8601. Bounds when we created or last revised the record
created_after / created_beforeISO-8601. Bounds when we first ingested the record
sortfiled_desc (default) · filed_asc · severity · ttd · confidence · created_desc · updated_asc · created_asc — column:direction spellings (filed_at:desc) also accepted
limit1–200 (default 50)
cursoropaque keyset cursor from meta.cursor.next

Errors

Every error — auth, rate limit, not-found, validation, paid-tier — returns the same envelope. Branch on error.code, which is stable, rather than on the human-readable message.

{ "error": { "code": "unauthorized", "message": "missing bearer token" } }
  • 401 unauthorized — missing / invalid / revoked key
  • 403 paid_tier_required — endpoint needs a paid plan
  • 404 not_found — no such resource
  • 422 validation_error — invalid parameter; adds an error.fields[] array of { field, message, type }
  • 429 rate_limited — see Retry-After

Webhooks — push instead of polling

Register an endpoint and we POST to it when a matching record lands, so you don't poll for it. Free accounts get one endpoint, casework 20, watchtower unlimited; manage them under Settings → Webhooks.

Events

disclosure.matchedA new record matching the endpoint's filters. Honors the full /disclosures filter set. Filings only by default — leak-site claims and press reports are excluded unless you opt in with source_category of claim/report or a direct source_type, exactly as on the list endpoint.
incident.corroboratedA different kind of source joined an existing incident — e.g. a regulatory filing corroborating a leak-site claim. Fires on the distinct source-type set growing, so one regulator's filings across many states do not trigger it.
incident.determination_changedAn incident moved between suspected and confirmed; the body carries determination_from/determination_to. (enforced exists in the model but has never occurred — don't code against it.)

Incident events are filtered differently. They match on victim_entity_id, jurisdiction and determination only — incidents carry different columns than disclosures. Any other filter key is ignored for them, and the endpoint reports which in inert_filter_keys so a filter can't silently do nothing.

summary is a one-line, human-readable description of the event — the same text a Slack message leads with. It is deliberately outside data (which stays byte-identical to the REST record) and outside meta. A leak-site claim is always described as an unverified claim, never as a breach.

Slack

Paste a Slack incoming-webhook URL and we deliver a Slack message instead of the JSON envelope — the format is set from the URL automatically, and you can pick it by hand on any endpoint. Every message carries the source, jurisdiction, a link to the record, and the AI-assistance and upstream-data credits.

Two things to know. Slack ignores our signature header, so for a chat endpoint the security boundary is the secrecy of the webhook URL itself — treat it like a password. And because chat platforms accept only their own message shape, registering one with the wrong format is refused up front rather than failing on every event. Microsoft Teams, Discord and Google Chat are not supported yet and are blocked at registration for the same reason; point those at your own receiver and forward from there.

Payload and headers

POST https://hooks.example.com/disclosurelens
Content-Type: application/json
User-Agent: DisclosureLens-Webhook/1
X-DisclosureLens-Event: disclosure.matched
X-DisclosureLens-Delivery: whd_9f2c...        # stable across retries — dedupe on this
X-DisclosureLens-Signature: t=1754500000,v1=<hex>

{
  "event": "disclosure.matched",
  "delivery_id": "whd_9f2c...",
  "occurred_at": "2026-08-07T04:15:00.123456+00:00",
  "summary": "Acme Corp — breach disclosure filing (state AG notification, us-id), filed 2026-08-07",
  // "data" carries the same record shape as GET /v1/disclosures/{id},
  // INCLUDING source.artifacts[] — each entry's fetch_url serves our
  // archived copy of the document (relative to the API host), which
  // stays resolvable after the publisher's own source.url rots.
  "data": { /* same record shape as GET /v1/disclosures/{id} */ },
  // Present only when this endpoint's filter watches entities — directly
  // or through a watchlist: WHICH of your watched ids this record fired
  // on, per axis. A spec names many ids and the vendor link is not part
  // of the record, so "data" alone cannot tell you which watched company
  // an event is about. Route on this instead of re-deriving it. Empty
  // lists mean the watch matched but the entity link has since been
  // repointed. For a {"watchlist_id"} spec the block also names the
  // list, so one receiver fanning several lists can route on which
  // list fired.
  "matched": {
    "victim_entity_id": ["ent_1a2b3c..."],
    "vendor_entity_id": [],
    "watchlist_id": "wl_9d8e..."    // only for watchlist specs
  },
  "meta": {
    "ai_assisted": true,
    "attribution": "Data: DisclosureLens (https://disclosurelens.com)"
    // "upstream_attribution": [...] when the payload carries leak-site
    // or press data — credit it the way you would any sourced dataset.
  }
}

Incident events carry an incident summary instead: id, determination, canonical_entity_id, jurisdictions_filed, data_types_affected, incident_start/incident_end, plus corroborating_source_type or determination_from/determination_to depending on the event. Two counts ride along and they are not the same number: linked_disclosures_count is structural (every member, retracted ones included) while visible_member_count counts only members you can fetch. Render totals against the visible count.

Delivery is at least once — dedupe on X-DisclosureLens-Delivery, which is stable across retries. Respond 2xx quickly (within 10s); if you need to do real work, return 202 and process asynchronously.

Verify the signature

The timestamp is inside the signed material, so you can reject replays. Compare with a constant-time function, never ==.

# Python
import hashlib, hmac, time

def verify(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    ts = int(parts["t"])
    if abs(time.time() - ts) > tolerance:
        return False                      # stale — likely a replay
    expected = hmac.new(
        secret.encode(),
        b"df-webhook-v1:" + f"{ts}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
// JavaScript (Node)
import crypto from "node:crypto";

export function verify(secret, rawBody, header, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const ts = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - ts) > toleranceSec) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(Buffer.concat([Buffer.from("df-webhook-v1:"), Buffer.from(`${ts}.`), rawBody]))
    .digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Sign over the raw request body, before any JSON parse/re-serialize — re-encoding changes the bytes and the signature will not match.

Secrets, retries, and auto-disable

  • The signing secret is derived, not stored, so unlike an API key you can view it again at any time. Rotating it invalidates the old value immediately — update your receiver at the same moment.
  • Failures retry with exponential backoff, six attempts. A 429 or 5xx is retried; any other 4xx stops immediately, because an identical retry would fail identically. A 3xx is a failure too — see below.
  • Redirects are not followed: a redirect points at a destination we never validated, so a 3xx is recorded as a failed delivery rather than chased. Register the final URL. We also re-check at delivery time that your host doesn't resolve to a private address; an endpoint that starts resolving privately is disabled rather than retried, and re-enabling it will not stick until the hostname resolves publicly again.
  • 20 consecutive failed deliveries auto-disables an endpoint — not 20 requests. Each delivery is up to six attempts, so a fully dead receiver absorbs on the order of 120 POSTs before we stop. Any success resets the counter, as does resuming the endpoint from Settings.
  • GET /v1/me/webhooks/{id}/deliveries returns recent attempts with status, error and a response snippet — the first place to look when deliveries stop arriving.
  • Historical backfills do not emit webhooks. Webhooks carry what just happened; use the change feed or bulk export to reconcile history.

Recipes

Build a research corpus you can cite

For a reproducible dataset, bound your pull by ingestion time with created_before — later revisions never move records into or out of your set — then keep a working copy current with the change feed:

import requests

BASE, KEY = "https://api.disclosurelens.com/v1", "df_..."

def pull(params):
    rows, cursor = [], None
    while True:
        page = requests.get(f"{BASE}/disclosures",
            params={**params, "sort": "updated_asc", "limit": 200,
                    **({"cursor": cursor} if cursor else {})},
            headers={"Authorization": f"Bearer {KEY}"}).json()
        rows += page["data"]
        cursor = page["meta"]["cursor"]["next"]
        if not cursor:
            return rows

corpus = pull({"created_before": "2026-08-01T00:00:00Z"})   # frozen snapshot
updates = pull({"updated_after": "2026-08-01T00:00:00Z"})   # revisions since

Every payload carries its own citation in meta.attribution; record the retrieval date alongside it. For the whole corpus in one call, see Bulk export below.

Monitor a vendor list

Resolve each vendor to its entity id once, then either save an email alert or poll the change feed filtered to that entity. Pass include_leak_site=true when polling — leak-site claims are usually the earliest signal a vendor has a problem, weeks before any filing.

The simplest way to run a standing list is a named watchlist: create it once, add entries, and reference it from a subscription or webhook endpoint as {"watchlist_id": "wl_..."}. Matching is a live join — edit the list and every attached channel follows immediately, no spec to re-paste — and one watchlist spec covers all three axes described below (own filings, leak-site claims, and breached-vendor client filings) in a single subscription or endpoint.

# create the list with entries (bulk add up to 500 per call)
curl -X POST -H "Authorization: Bearer $DL_KEY" -H "Content-Type: application/json"   -d '{"name":"critical vendors","entries":[{"entity_id":"ent_acme","label":"Acme"},
       {"entity_id":"ent_beta"},{"entity_id":"ent_gamma"}]}'   "https://api.disclosurelens.com/v1/me/watchlists"

# one email alert for the whole list, every axis
curl -X POST -H "Authorization: Bearer $DL_KEY" -H "Content-Type: application/json"   -d '{"frequency":"real_time"}' "https://api.disclosurelens.com/v1/me/watchlists/wl_YOUR_ID/subscribe"

# or one webhook endpoint for the whole list, every axis
curl -X POST -H "Authorization: Bearer $DL_KEY" -H "Content-Type: application/json"   -d '{"url":"https://soc.example.com/hook","event_types":["disclosure.matched"],
       "filter_spec":{"watchlist_id":"wl_YOUR_ID"}}'   "https://api.disclosurelens.com/v1/me/webhooks"

Prefer raw filter specs? The same coverage takes three subscriptions, one per axis:

A complete watch is three subscriptions, not one — one per axis a vendor appears on. Alert matching is default-deny for leak-site claims and press coverage, exactly as the feeds are: a bare {"victim_entity_id": ...} spec fires on filings and enforcement decisions only. The companion spec carrying "source_category": "claim" is what opts into the early signal described above — without it you are subscribed to the paperwork and not to the warning. And a third spec on vendor_entity_id catches filings where the vendor is the breached third-party provider inside its clients’ notices — which is where most of the vendor-breach signal lives: a victim watch on a major vendor sees its own dozen filings and misses the hundreds of client filings naming it.

victim_entity_id takes many ids, not one — repeat the query param, or pass a JSON array in a filter_spec — and matches records for any of them (up to 50 per request). So a whole vendor list is three subscriptions total, not three per vendor. This also matters for a single company: large organizations resolve to several entity rows (a parent, its named subsidiaries, and separately-filed brands), so watching only the biggest row can miss a substantial share of that company's history. Resolve the name, then pass every id it returns.

# 1. resolve each vendor name -> entity id (ent_...)
curl -H "Authorization: Bearer $DL_KEY" "https://api.disclosurelens.com/v1/entities?q=Acme+Corp"

# 2a. one alert covering the whole list: filings + enforcement
curl -X POST -H "Authorization: Bearer $DL_KEY" -H "Content-Type: application/json"   -d '{"name":"watch: vendor list","frequency":"real_time",
       "filter_spec":{"victim_entity_id":["ent_acme","ent_beta","ent_gamma"]}}'   "https://api.disclosurelens.com/v1/me/subscriptions"

# 2a-companion. REQUIRED for leak-site claims -- the earliest signal.
# Without this second subscription you will not be alerted on a claim.
curl -X POST -H "Authorization: Bearer $DL_KEY" -H "Content-Type: application/json"   -d '{"name":"watch: vendor list - leak-site claims","frequency":"real_time",
       "filter_spec":{"victim_entity_id":["ent_acme","ent_beta","ent_gamma"],
                      "source_category":"claim"}}'   "https://api.disclosurelens.com/v1/me/subscriptions"

# 2a-vendor-axis. REQUIRED for third-party exposure -- fires when a watched
# vendor is the BREACHED PROVIDER inside a client's filing (role='vendor').
curl -X POST -H "Authorization: Bearer $DL_KEY" -H "Content-Type: application/json"   -d '{"name":"watch: vendor list - as breached vendor","frequency":"real_time",
       "filter_spec":{"vendor_entity_id":["ent_acme","ent_beta","ent_gamma"]}}'   "https://api.disclosurelens.com/v1/me/subscriptions"

# 2b. or poll programmatically -- repeat the param per entity
curl -H "Authorization: Bearer $DL_KEY"   "https://api.disclosurelens.com/v1/disclosures?victim_entity_id=ent_acme&victim_entity_id=ent_beta&include_leak_site=true&updated_after=$WATERMARK&sort=updated_asc"

Versioning & stability

Every data endpoint lives under /v1 (the liveness probe and the public spec sit at the host root), and v1 evolves additively: new endpoints, new optional query parameters, new response fields, and new enum values may appear without notice — write clients that tolerate unknown fields and unrecognized enum values. Removing or renaming a field, changing a type, or changing the meaning of an existing value counts as a breaking change: it gets a changelog entry and a deprecation window before it ships. No breaking change has occurred in v1 to date, and there is no v2 planned.

The OpenAPI spec is generated from the running code, and this page's endpoint tables are generated from that same spec in CI — the three cannot disagree.

API changelog

  • 2026-08-22Named watchlists: /v1/me/watchlists CRUD, and filter_spec {"watchlist_id"} on subscriptions and webhook endpoints. One spec covers all three watch axes (own filings, leak-site claims, breached-vendor client filings) and matching is a live join — editing the list updates every attached channel immediately. The disclosure.matched envelope's matched block now resolves watchlist entries and names the watchlist_id, and data.source.artifacts[] rides the event so the archived document stays fetchable after the publisher's URL rots. Additive; raw entity-id specs are unchanged.
  • 2026-08-19Bulk export rows now carry incident_link.determination for every incident-linked row, single-filing incidents included (the list endpoint hides the singleton link; the export does not). The incident-level affected_individual_count_total is deliberately not included.
  • 2026-08-16Docs correction, not a behaviour change: the published spec and llms.txt had carried pre-2026-08-10 plan numbers (60 req/min free tier, 3 free webhook endpoints) and still described bulk export as paid-only. The enforced values — 5/600/6,000 req/min per API key, 1/20/unlimited endpoints, export open to every signed-up account — were unchanged throughout; only the documentation was wrong. If you sized a client against those numbers, re-read the limits section.
  • 2026-08-16Bulk export: resuming an interrupted stream with updated_after now draws on a separate per-day continuation budget instead of spending a fresh run, which is what the 429's own advice always told you to do. A request rejected at parameter validation (422) no longer consumes a run either.
  • 2026-08-11Batch entity resolution: POST /v1/entities/resolve/batch (watchtower tier) — up to 20 raw names per call, read-only (nothing is persisted, unlike ?resolve=true), each result carrying a corpus match or cascade candidate with confidence and layer.
  • 2026-08-11Casework caps raised: 50 watched entities + 25 saved filters (was 30/10), 20 webhook endpoints (was 10), 100 scoped export runs/UTC day (was 50). Free-tier caps unchanged (1 webhook endpoint, 1 full-corpus export run/day since 08-10).
  • 2026-08-07Subscription + webhook filters: victim_entity_id / reporting_entity_id are now accepted (they were rejected as a shape mismatch, so per-company alerting could not be set up at all), determination is accepted on webhook endpoints, and a filter naming source_type=press now opts in to press rows the way the list endpoint does.
  • 2026-08-07Webhook payloads now carry a meta block (ai_assisted, attribution, upstream_attribution) and resolved entity ids — victim_entity.id and reporting_entity.id were previously null, and leak-site rows carried the threat actor's posted victim string rather than the registry name.
  • 2026-08-07Webhooks shipped: register endpoints at /v1/me/webhooks, receive signed POSTs for disclosure.matched, incident.corroborated and incident.determination_changed. Free tier: 3 endpoints.
  • 2026-08-05Incidents change feed: /v1/incidents gains updated_after + sort=updated_asc (rows carry updated_at; meta.cursor block added). Admin overrides incl. retractions now bump updated_at.
  • 2026-08-05Bulk export open to every signed-up account — free tier metered at 4 runs/UTC day, paid tiers unmetered (replaces the 08-03 paid gate).
  • 2026-08-05Public curated OpenAPI spec at /openapi.json; this page's endpoint tables now generate from it; llms.txt published.
  • 2026-08-05search/q accepted as aliases on /v1/disclosures and /v1/entities; column:direction sort spellings; GET /v1/stats/overview; HEAD support; gzip.
  • 2026-08-03Bulk NDJSON export gated to the paid tier (analyst+).
  • 2026-08-01Change feed (updated_after + sort=updated_asc), one unified error envelope, streaming NDJSON bulk export, authenticated full OpenAPI spec.

Bulk export

Open to every signed-up account. Metered by runs, not rows: the free tier gets 1 full-corpus run per UTC day (resumable — continuing an interrupted run via resume_after does not spend another). Casework gets 100 runs per UTC day, each scoped to a subject (victim_entity_id or incident_id — an unscoped casework run returns 400 export_scope_required). Watchtower and above are unmetered and unscoped. Over the quota returns 429 export_quota_exceeded with retry-after.

Paging the whole corpus through /disclosures takes hundreds of sequential requests. To take a copy, stream it instead — one JSON object per line, in the same record shape the list endpoint returns, plus one enrichment a mirror wants and a feed hides: incident_link is present for every incident-linked row (single-filing incidents included — most are) and carries determination, the incident's lifecycle state at export time:

curl -H "Authorization: Bearer $DL_KEY" \
  "https://api.disclosurelens.com/v1/export/disclosures.ndjson?updated_after=2026-01-01T00:00:00Z" \
  > corpus.ndjson

The last line is a trailer, not a record — an object under the reserved key _export carrying count, truncated, and resume_after. Always check truncated before treating a file as complete: a stream that hit the row cap, or that died mid-transfer, is otherwise indistinguishable from a finished one, because the response has already committed to 200 OK before the first row is written. To continue, pass resume_after back as updated_after.

Accepts updated_after/updated_before, source_type, source_category, jurisdiction, include_leak_site, and max_rows (default 100,000). Records are delivered at least once — deduplicate on id.

There is no CSV endpoint on purpose: the record is deeply nested, and every flattening is a lossy editorial choice we would rather leave to you. Project the columns you want, e.g. jq -r 'select(._export==null)|[.id,.victim_entity.raw_name]|@csv'.

Use & attribution

Public-interest use is free. Publishing data or statistics obtained from the API — articles, reports, research — requires attribution to DisclosureLens by name, with a link where the medium allows; every response restates this as meta.attribution. Disclosures are reported as filed, with provenance — the platform draws no legal conclusions. See the methodology and terms.

Ready? Create your API key.