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_hereKeys 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
| GET | /disclosures | List disclosures |
| GET | /disclosures/facets | Facet bucket counts |
| GET | /disclosures/{disclosure_id} | Single disclosure with classification |
| GET | /disclosures/{disclosure_id}/related | Related disclosures for the same incident |
| GET | /disclosures/{disclosure_id}/extraction | Extraction provenance for a disclosure |
| GET | /incidents | List incidents |
| GET | /incidents/{incident_id} | Single incident with linked disclosures |
| GET | /incidents/{incident_id}/litigation_timeline | Incident litigation timeline· paid tier |
| GET | /incidents/{incident_id}/cross_filing_comparison | Cross-filing comparison matrix· paid tier |
| GET | /entities | Search entities by name or identifier |
| POST | /entities/resolve/batch | Resolve a batch of raw names to entities (read-only)· watchtower tier |
| GET | /entities/browse | Name-ordered entity browse index |
| GET | /entities/{entity_id} | Single entity profile |
| GET | /entities/{entity_id}/disclosures | Disclosures linked to an entity |
| GET | /entities/{entity_id}/scorecard | Entity compliance scorecard |
| GET | /stats/compliance_distribution | Compliance flag distribution |
| GET | /stats/time_to_disclose_percentiles | Time-to-disclose percentiles by vertical |
| GET | /stats/disclosure_timing_by_jurisdiction | Disclosure-timing benchmark by regime and state |
| GET | /stats/late_disclosure_leaderboard | Late-disclosure entity leaderboard |
| GET | /stats/compliance_trend | Monthly compliance trend |
| GET | /stats/overview | Corpus overview counts and freshness |
| GET | /stats/daily | Daily filing counts by source type |
| GET | /stats/jurisdictions | Filing counts by jurisdiction |
| GET | /stats/geography | Geography Stats |
| GET | /stats/source_health | Per-source freshness summary |
| GET | /stats/time_to_disclose | Time-to-disclose histogram |
| GET | /stats/records_affected | Records-affected distribution |
| GET | /stats/data_types | Counts per affected data type |
| GET | /stats/data_elements | Counts per statutory data element |
| GET | /stats/top_cves | Top CVEs by mention count |
| GET | /stats/data_quality_events | Data-quality events in a window |
| GET | /stats/extraction_quality | Review-status mix by source type |
| GET | /stats/incidents_by_merge_method | Incident counts by merge method |
| GET | /stats/incidents_jurisdictions_cardinality | Incident jurisdiction-count bands |
| GET | /stats/incidents_lead_time | Leak-to-filing lead-time percentiles |
| GET | /stats/press_first | Press-first incidents + press→filing lead time |
| GET | /stats/claim_corroboration | Share of leak-site claims later corroborated |
| GET | /stats/translated_count | Machine-translated records in a window |
| GET | /stats/incidents_merge_confidence | Incident merge-confidence buckets |
| GET | /stats/incidents_source_combos | Incident counts by source combination |
| GET | /stats/frequency_severity | Vertical by severity-tier crosstab |
| GET | /stats/records_affected_bands | Records-affected bands with Wilson CI |
| GET | /stats/data_types_mix_shift | Monthly data-type mix shift |
| GET | /stats/repeat_offenders | Repeat-offender entity ranking |
| GET | /stats/source_cadence | Per-source monthly cadence with anomalies |
| GET | /stats/enforcement | Regulator enforcement rollup |
| GET | /stats/source_archive | Preserved-archive size and cross-source utility for one source |
| POST | /analytics/comparable_incidents | Comparable-incident cohort stats· paid tier |
| GET | /analytics/unreported_claims | Unreported leak-site claims early warning· paid tier |
| POST | /analytics/underwriting_brief | Broker underwriting brief· paid tier |
| POST | /analytics/freq_severity_curve | Notification-exposure (records) log-normal fit· paid tier |
| POST | /analytics/portfolio_rating | Portfolio risk rating· paid tier |
| GET | /analytics/entity_scorecard.pdf | Signed entity scorecard PDF· paid tier |
| GET | /analytics/compliance_report.pdf | Signed compliance report PDF· paid tier |
| GET | /analytics/broker_benchmark_letter.pdf | Signed broker benchmark letter PDF· paid tier |
| POST | /analytics/portfolio_scorecard | Portfolio compliance scorecard· watchtower tier |
| GET | /analytics/portfolio_scorecard.pdf | Signed portfolio compliance scorecard PDF· watchtower tier |
| GET | /analytics/entity_compliance_distribution | Entity position in its sector's clock distribution· paid tier |
| GET | /analytics/evidence_package.pdf | Signed incident evidence package PDF· paid tier |
| GET | /export/disclosures.ndjson | Streaming NDJSON export |
| GET | /disclosures/{disclosure_id}/source/manifest | Source artifact manifest |
| GET | /disclosures/{disclosure_id}/source/artifacts/{artifact_id} | Archived source artifact body |
| GET | /threat-actors | List ransomware groups with victim counts |
| GET | /threat-actors/{slug} | Enriched threat-actor profile |
| GET | /supply-chain/cascades | List confirmed supply-chain cascades |
| GET | /supply-chain/cascades/{cascade_id} | One confirmed cascade with its members |
| GET | /scan | Name a domain's third-party vendors and check them for breach records |
| GET | /scan/connection | Name the connectivity vendors behind a caller's own IP |
| GET | /me/subscriptions/decay | Watched-entity decay proposals |
| GET | /me/subscriptions | List my email subscriptions |
| POST | /me/subscriptions | Create an email subscription (idempotent) |
| PATCH | /me/subscriptions/{subscription_id} | Update an email subscription |
| DELETE | /me/subscriptions/{subscription_id} | Delete an email subscription |
| GET | /me/watchlists | List my watchlists |
| POST | /me/watchlists | Create 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}/entries | Add 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}/subscribe | Create the email alert for a watchlist |
| GET | /me/watchlists/{watchlist_id}/decay | Watchlist decay proposals |
| GET | /me/api-keys | List my API keys |
| POST | /me/api-keys | Create an API key (secret shown once) |
| DELETE | /me/api-keys/{key_id} | Revoke an API key |
| GET | /health | Liveness probe |
| GET | https://api.disclosurelens.com/healthz | Liveness probe |
| GET | /health/ready | Readiness probe (DB connectivity) |
| GET | /health/pdf | PDF signing cert health |
| GET | /health/extract | LLM extraction pipeline health |
| GET | /health/entity-resolution | Entity-resolution pipeline health |
| GET | /health/dedup | Incident-dedup pipeline health |
| GET | /health/sources | Per-source ingestion health |
| GET | /me/webhooks | List my webhook endpoints |
| POST | /me/webhooks | Create a webhook endpoint (idempotent) |
| GET | /me/webhooks/{endpoint_id}/secret | Reveal an endpoint's signing secret |
| POST | /me/webhooks/{endpoint_id}/rotate-secret | Rotate 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}/deliveries | Recent delivery attempts for an endpoint |
| POST | /me/webhooks/{endpoint_id}/test | Send a test event to an endpoint |
| GET | https://api.disclosurelens.com/openapi.json | Curated 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):
| jurisdiction | e.g. us-ca, us-federal, global (canonical lowercase) |
| source_type | sec_8k · state_ag · ocr · leak_site · press · gdpr_dpa |
| source_category | claim · report · filing · enforcement |
| severity | low · medium · high · critical |
| naics_sector | 2-digit NAICS, e.g. 62 (health care) |
| vertical | repeatable: ?vertical=healthcare&vertical=manufacturing |
| compliance | e.g. ca_60day_late (repeatable) |
| cve | exact CVE-YYYY-NNNN |
| actor_named | ransomware group / actor name |
| search | full-text over victim + summary (q is an accepted alias; respects the leak-site default below) |
| include_leak_site | DEFAULT 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_id | exact entity id, e.g. ent_… (the resolved victim; not entity_id) |
| filed_after / filed_before | ISO-8601. Bounds the REGULATOR's filing date — see Change feed |
| updated_after / updated_before | ISO-8601. Bounds when we created or last revised the record |
| created_after / created_before | ISO-8601. Bounds when we first ingested the record |
| sort | filed_desc (default) · filed_asc · severity · ttd · confidence · created_desc · updated_asc · created_asc — column:direction spellings (filed_at:desc) also accepted |
| limit | 1–200 (default 50) |
| cursor | opaque 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" } }401unauthorized— missing / invalid / revoked key403paid_tier_required— endpoint needs a paid plan404not_found— no such resource422validation_error— invalid parameter; adds anerror.fields[]array of{ field, message, type }429rate_limited— seeRetry-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.matched | A 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.corroborated | A 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_changed | An 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
429or5xxis retried; any other4xxstops immediately, because an identical retry would fail identically. A3xxis a failure too — see below. - Redirects are not followed: a redirect points at a destination we never validated, so a
3xxis 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}/deliveriesreturns 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 sinceEvery 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-22 — Named 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-19 — Bulk 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-16 — Docs 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-16 — Bulk 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-11 — Batch 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-11 — Casework 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-07 — Subscription + 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-07 — Webhook 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-07 — Webhooks 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-05 — Incidents 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-05 — Bulk 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-05 — Public curated OpenAPI spec at /openapi.json; this page's endpoint tables now generate from it; llms.txt published.
- 2026-08-05 — search/q accepted as aliases on /v1/disclosures and /v1/entities; column:direction sort spellings; GET /v1/stats/overview; HEAD support; gzip.
- 2026-08-03 — Bulk NDJSON export gated to the paid tier (analyst+).
- 2026-08-01 — Change 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.ndjsonThe 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.