Veridion Disclosure API / 1.14.0
Quickstart
Node.js 20 or later. No key required. Free disclosure reads allow 25 rows per page and 60 requests per hour per IP.
Three requests
GET /api/v1/disclosureswithtransaction_date_fromset to 30 days before the run andlimit=25. The unfiltered walk starts with the oldest positions, not the newest transactions.- Repeat that exact URL with its returned
ETaginIf-None-Match. A 304 has no JSON body; a 200 contains the changed page. GET /api/v1/coverage. Read each block's population basis and measurement instant. Physical warehouse rows and served identities are different populations.
curl --fail --show-error --silent \
https://www.veridionmarkets.com/examples/disclosure-quickstart.mjs \
--output disclosure-quickstart.mjs
node disclosure-quickstart.mjsThe output contains the returned page, conditional status/body, and coverage response. An empty data array means this request returned no rows; it is not proof of no government filings. No example response or count is substituted on failure.
At this contract version, anonymous disclosure admission occurs before validation and conditional matching: a 304 or a subsequent 400 can consume a request. Do not assume revalidation is free. On 429 or 503, inspect Retry-After; the example stops instead of retrying automatically.
A 304 carries no quota headers. Measured 2026-09-18: the origin sets RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and X-API-Version on every response, but the serving platform reduces a 304 to its validators (ETag, Cache-Control, Vary, Date) before it reaches you. This is not an origin choice and cannot be changed by the origin. A client that only ever revalidates will see its budget consumed but never reported. Read quota from the most recent 200, or issue an unconditional request when you need a fresh balance. The version you hold is safe: it is part of the entity tag, so a 304 can only match a response produced under the version currently served.
Executable source
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
export const API_ORIGIN = "https://www.veridionmarkets.com";
export function recentFilters(now = new Date()) {
const from = new Date(now);
from.setUTCDate(from.getUTCDate() - 30);
return { transaction_date_from: from.toISOString().slice(0, 10), limit: "25" };
}
async function request(url, fetcher, headers = {}) {
const response = await fetcher(url, {
headers: { Accept: "application/json", ...headers },
redirect: "error",
signal: AbortSignal.timeout(30_000),
});
if (response.status === 304) {
if (!headers["If-None-Match"]) throw new Error("Unexpected 304; no state committed");
return { response, body: null };
}
const contentType = response.headers.get("content-type") || "";
if (!/\bapplication\/(?:problem\+)?json\b/i.test(contentType)) {
throw new Error(`HTTP ${response.status}: expected JSON; no state committed`);
}
const body = await response.json();
if (!response.ok) {
const error = new Error(`HTTP ${response.status}: ${body.code || "request_failed"}`);
Object.assign(error, {
code: body.code, requestId: response.headers.get("x-request-id"),
retryAfter: response.headers.get("retry-after"),
});
throw error;
}
return { response, body };
}
// Three requests: a recent page, its conditional read, then coverage.
export async function preview({ fetcher = fetch, now = new Date() } = {}) {
const url = new URL("/api/v1/disclosures", API_ORIGIN);
url.search = new URLSearchParams(recentFilters(now)).toString();
const first = await request(url, fetcher);
if (!Array.isArray(first.body?.data)) throw new Error("Disclosure page unavailable");
const etag = first.response.headers.get("etag");
if (!etag) throw new Error("ETag unavailable; conditional request not attempted");
const conditional = await request(url, fetcher, { "If-None-Match": etag });
const coverage = await request(new URL("/api/v1/coverage", API_ORIGIN), fetcher);
return {
firstPage: first.body,
conditional: { status: conditional.response.status, body: conditional.body },
coverage: coverage.body,
};
}
function requiredString(value) {
return typeof value === "string" && value.length > 0;
}
function timestamp(value) {
return typeof value === "string"
&& /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/.test(value)
&& Number.isFinite(Date.parse(value));
}
// State is replaced only after every page succeeds. Keep one writer per replica.
/**
* @typedef {{ disclosure_id: string, version_id: string, change: { type: string }, [field: string]: unknown }} Row
* @typedef {{ transaction_date_from: string, limit: string }} Filters
* @typedef {{ rows: Row[], filters: Filters, watermark: string }} State
* @param {{ state?: State | null, filters?: Filters, fetcher?: typeof fetch, maxPages?: number }} options
* @returns {Promise<State>}
*/
export async function walk({ state = null, filters = recentFilters(), fetcher = fetch, maxPages = 50 } = {}) {
if (!Number.isSafeInteger(maxPages) || maxPages < 1) throw new Error("Invalid page budget");
if (state && (!timestamp(state.watermark) || !Array.isArray(state.rows) || !state.filters)) {
throw new Error("Invalid saved state; do not invent a watermark");
}
const fixedFilters = { ...(state ? state.filters : filters) };
if (Object.keys(fixedFilters).some((key) => !["transaction_date_from", "limit"].includes(key))
|| !/^\d{4}-\d{2}-\d{2}$/.test(fixedFilters.transaction_date_from)
|| !/^([1-9]|1[0-9]|2[0-5])$/.test(String(fixedFilters.limit))) {
throw new Error("Use the fixed recent-window filters and a free-tier limit from 1 to 25");
}
const rows = new Map();
for (const row of state?.rows ?? []) {
if (!requiredString(row?.disclosure_id) || !requiredString(row?.version_id) || rows.has(row.disclosure_id)) {
throw new Error("Invalid saved rows; no state committed");
}
rows.set(row.disclosure_id, row);
}
let cursor = null;
let pin = null;
const seen = new Set();
for (let pageNumber = 0; pageNumber < maxPages; pageNumber += 1) {
const url = new URL("/api/v1/disclosures", API_ORIGIN);
url.search = new URLSearchParams(fixedFilters).toString();
if (state) url.searchParams.set("updated_since", state.watermark);
if (cursor) url.searchParams.set("cursor", cursor);
const { body } = await request(url, fetcher);
const page = body?.page;
if (!Array.isArray(body?.data) || !page
|| body.coverage?.rows_dropped_failed_validation !== 0
|| body.coverage?.rows_returned !== body.data.length
|| page.mode !== (state ? "changes" : "snapshot")
|| !Number.isSafeInteger(page.snapshot_generation) || page.snapshot_generation < 1
|| !timestamp(page.snapshot_as_of)
|| page.requested_updated_since !== (state?.watermark ?? null)
|| page.next_updated_since !== page.snapshot_as_of
|| typeof page.has_more !== "boolean") {
throw new Error("Incomplete or invalid page; no state committed");
}
const pagePin = JSON.stringify([page.snapshot_generation, page.snapshot_as_of, page.next_updated_since]);
if (pin !== null && pin !== pagePin) throw new Error("Snapshot changed; no state committed");
pin = pagePin;
for (const row of body.data) {
if (!requiredString(row?.disclosure_id) || !requiredString(row?.version_id)
|| !["upsert", "supersede"].includes(row.change?.type)
|| (!state && row.change.type !== "upsert")) {
throw new Error("Invalid change; no state committed");
}
if (row.change.type === "upsert") rows.set(row.disclosure_id, row);
else if (rows.get(row.disclosure_id)?.version_id === row.version_id) rows.delete(row.disclosure_id);
}
if (!page.has_more) {
if (page.next_cursor !== null) throw new Error("Unexpected cursor; no state committed");
return { filters: fixedFilters, rows: [...rows.values()], watermark: page.next_updated_since };
}
if (!requiredString(page.next_cursor) || seen.has(page.next_cursor)) {
throw new Error("Missing or repeated cursor; no state committed");
}
cursor = page.next_cursor;
seen.add(cursor);
}
throw new Error("Page budget reached; no state committed. Narrow the initial window or use a licensed integration.");
}
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
try {
console.log(JSON.stringify(await preview(), null, 2));
} catch (error) {
console.error(JSON.stringify({ error: error.message, code: error.code, request_id: error.requestId, retry_after: error.retryAfter }));
process.exitCode = 1;
}
}
Incremental walk
The first walk exhausts a pinned snapshot for the same recent window. The second uses its exact page.next_updated_since watermark and follows every cursor. Keep that window fixed between runs; changing it requires a new full pull. The page limit does not limit the total result.
node --input-type=module <<'JS'
import { walk } from './disclosure-quickstart.mjs';
let state = await walk();
state = await walk({ state });
console.log(JSON.stringify(state, null, 2));
JSStore rows, filters, and watermark together only after walk returns. Reuse that state on the next poll. If any page fails, keep the previous state and watermark; do not commit a partial pull. The example stops after 50 pages and does not certify source completeness.
An upsert replaces the stored disclosure_id. A supersede removes it only when the stored version_id matches the event. A withdrawal of an older version cannot erase a newer one. Never derive a watermark from change.observed_at or group rows by ticker, amount or date.
For invalid_cursor or cursor_snapshot_unavailable, discard the unfinished walk and restart page one with the previous committed state. If the watermark predates retained history, rebuild the replica from a fresh full pull; do not silently advance the watermark past missed changes.
Client actions
21 problem types in the current catalogue. Branch on code, retain X-Request-ID, and follow the action below. A refusal is not an empty successful page.
- unknown_query_parameterHTTP 400
Remove or correct the parameter. The detail field lists the parameters this operation accepts.
Fix the request; do not retry as sent.
- method_not_allowedHTTP 405
Reissue the request as GET. The response carries an Allow header listing the methods this path accepts.
Fix the request; do not retry as sent.
- invalid_requestHTTP 400
The detail field names each offending parameter and what it expected. Fix and resend.
Fix the request; do not retry as sent.
- invalid_disclosure_idHTTP 400
Use the disclosure_id exactly as it appears on a served row.
Fix the request; do not retry as sent.
- invalid_cursorHTTP 400
Restart from page one without a cursor and follow page.next_cursor or the Link: rel="next" header.
Restart from page one without a cursor.
- temporal_history_unavailableHTTP 400
Use a timestamp at or after the floor, which is published on every response and in the detail field.
Fix the request; do not retry as sent.
- unauthorizedHTTP 401
Check the key, or drop the Authorization header for free-tier access.
Fix the request; do not retry as sent.
- insufficient_scopeHTTP 403
Use a key scoped for this endpoint, or request the scope through the access form on the API page.
Fix the request; do not retry as sent.
- route_not_foundHTTP 404
Correct the path. The detail field lists every path this contract defines, and /api/v1/openapi.json is the machine-readable source.
Fix the request; do not retry as sent.
- disclosure_not_foundHTTP 404
Check the identifier against a served row.
Fix the request; do not retry as sent.
- no_snapshot_yetHTTP 404
Retry after the next daily snapshot; /v1/status reports the export state.
Retry later; the request was sound. Every 503 carries Retry-After with the minimum wait.
- cursor_snapshot_unavailableHTTP 409
Restart the pull without a cursor; page one pins a new snapshot.
Restart from page one without a cursor.
- rate_limitedHTTP 429
Wait for Retry-After, or request a key, which raises the limits and lifts the row cap.
Retry after the Retry-After header elapses.
- api_rate_limit_unavailableHTTP 503
Wait for Retry-After, then retry. If it persists, quote X-Request-ID to support.
Retry later; the request was sound. Every 503 carries Retry-After with the minimum wait.
- warehouse_unavailableHTTP 503
Retry later. If it persists, quote the X-Request-ID to support@veridionmarkets.com.
Retry later; the request was sound. Every 503 carries Retry-After with the minimum wait.
- api_auth_unavailableHTTP 503
Retry later with the same key.
Retry later; the request was sound. Every 503 carries Retry-After with the minimum wait.
- api_usage_meter_unavailableHTTP 503
Wait at least the Retry-After interval before retrying. The delay is a backoff instruction, not an estimate of recovery time.
Retry later; the request was sound. Every 503 carries Retry-After with the minimum wait.
- contract_validation_failedHTTP 503
Quote the X-Request-ID to support@veridionmarkets.com; the row is identifiable from it.
Retry later; the request was sound. Every 503 carries Retry-After with the minimum wait.
- temporal_capability_unavailableHTTP 503
Retry later, or issue the request without as_of for the current state.
Retry later; the request was sound. Every 503 carries Retry-After with the minimum wait.
- signing_failedHTTP 503
Retry later.
Retry later; the request was sound. Every 503 carries Retry-After with the minimum wait.
- manifest_timestamp_unreadableHTTP 503
Retry later. The snapshot file itself is unaffected; only its manifest is withheld.
Retry later; the request was sound. Every 503 carries Retry-After with the minimum wait.