/api/v1/investor-documents/doclist/
Find exact CUSIP, EMMA issuer, and accepted reporting-entity documents for one CUSIP9.
Use public endpoints for issuer discovery. Investor document access uses self-service API keys, exact CUSIP9 lookup, signed document links, and MuniPro as the cache and download intermediary.
Start here: sign in to create a lookup key. The raw key appears once, with copy buttons, starter kit snippets, a browser playground, activity history, and a downloadable zip.
Use one header everywhere: Authorization: Bearer bgv1_...
Start with the auth check endpoint. It requires no CUSIP and returns your client, key prefix, scopes, quota state, reset time, and request id.
Then call doclist with a 9-character CUSIP. Use only the status, prepare, download, and text URLs returned by doclist.
# Bash / Linux export BONDGOV_API_KEY="bgv1_your_key_here" curl -s "https://bondgov.com/api/v1/investor-documents/auth/" \ -H "Authorization: Bearer $BONDGOV_API_KEY"
# PowerShell $env:BONDGOV_API_KEY="bgv1_your_key_here" curl.exe -s "https://bondgov.com/api/v1/investor-documents/auth/" ` -H "Authorization: Bearer $env:BONDGOV_API_KEY"
# Python
import os, requests
base = "https://bondgov.com/api/v1/investor-documents"
headers = {"Authorization": f"Bearer {os.environ['BONDGOV_API_KEY']}"}
auth = requests.get(f"{base}/auth/", headers=headers, timeout=30)
auth.raise_for_status()
docs = requests.get(
f"{base}/doclist/",
params={"cusip": "123456AB7", "scope": "all"},
headers=headers,
timeout=30,
)
docs.raise_for_status()
document = docs.json()["documents"][0]
prepare = requests.post(
document["links"]["prepare"],
params={"include": "download,text", "background": "true"},
headers=headers,
timeout=30,
)
prepare.raise_for_status()
status = requests.get(document["links"]["status"], headers=headers, timeout=30)
status.raise_for_status()
# JavaScript
const base = "https://bondgov.com/api/v1/investor-documents";
const headers = { Authorization: `Bearer ${process.env.BONDGOV_API_KEY}` };
const auth = await fetch(`${base}/auth/`, { headers });
if (!auth.ok) throw new Error(await auth.text());
const docs = await fetch(`${base}/doclist/?cusip=123456AB7&scope=all`, { headers });
if (!docs.ok) throw new Error(await docs.text());
const document = (await docs.json()).documents[0];
const prepare = await fetch(`${document.links.prepare}?include=download,text&background=true`, {
method: "POST",
headers
});
if (!prepare.ok) throw new Error(await prepare.text());
const status = await fetch(document.links.status, { headers });
if (!status.ok) throw new Error(await status.text());
Find exact CUSIP, EMMA issuer, and accepted reporting-entity documents for one CUSIP9.
Warms a signed document into the cache and can queue extraction before the client downloads it.
Returns cache, extraction, and client-safe storage readiness without fetching upstream bytes.
Streams a PDF through the BondGov cache. Cache misses fetch upstream, persist, audit, and serve.
Returns extracted text, sections, tables, and image metadata. Pending extraction returns HTTP 202.
curl -s "https://bondgov.com/api/v1/investor-documents/doclist/?cusip=123456AB7&scope=all&limit=100" \ -H "Authorization: Bearer $BONDGOV_API_KEY"
curl -X POST "https://bondgov.com/api/v1/investor-documents/{document_id}/prepare/?include=download,text&background=true" \
-H "Authorization: Bearer $BONDGOV_API_KEY"
curl -I "https://bondgov.com/api/v1/investor-documents/{document_id}/download/" \
-H "Authorization: Bearer $BONDGOV_API_KEY"
curl -L "https://bondgov.com/api/v1/investor-documents/{document_id}/download/" \
-H "Authorization: Bearer $BONDGOV_API_KEY" \
-o disclosure.pdf
curl -s "https://bondgov.com/api/v1/investor-documents/{document_id}/text/?include=sections,tables" \
-H "Authorization: Bearer $BONDGOV_API_KEY"
| Step | Request | Client Behavior |
|---|---|---|
| 1 | GET /auth/ | Confirm the key, scopes, quota, and support request id. |
| 2 | GET /doclist/?cusip=<CUSIP9> | Read grouped and flattened document records. Save the returned signed document_id or links. |
| 3 | POST /{document_id}/prepare/?include=download,text&background=true | Queue cache warming and extraction when a document is not ready yet. Use Retry-After before polling. |
| 4 | GET /{document_id}/status/ | Check ready.download, ready.text, cache status, and extraction status without downloading the file. |
| 5 | HEAD /{document_id}/download/ | Check PDF metadata and cache readiness before a bulk download job. |
| 6 | GET /{document_id}/download/ or GET /{document_id}/text/ | Download the PDF or extracted content only through BondGov URLs returned by doclist. |
Doclist supports safe filters: scope, limit, cursor, category, source, date_from, date_to, cache_status, and text_status. CUSIP6 searches are intentionally rejected in v1; send a full CUSIP9.
| Scope | Allows | Notes |
|---|---|---|
documents:read | Doclist lookup | Required for CUSIP discovery. |
documents:download | PDF downloads | Subject to request and byte quotas. |
documents:text | Extracted content | May return 202 while extraction is queued. |
documents:debug | Provenance fields | Optional operator/debug scope. Not needed for normal integrations. |
Every investor document response includes X-BondGov-Request-ID for support correlation. Quota headers are returned when limits apply. Request responses include X-BondGov-RateLimit-Limit, X-BondGov-RateLimit-Remaining, and X-BondGov-RateLimit-Reset. Download responses can also include X-BondGov-Download-Limit, X-BondGov-Download-Used, and X-BondGov-Download-Remaining. Pending and temporary-failure responses include retry_after_seconds, Retry-After, and docs_url.
Create a lookup-only key with one button, then add PDF or text permissions only when the workflow needs them.
Run auth and doclist checks from the browser using one of your active keys, without pasting the raw token again.
Review a GitHub-style request frequency grid, recent API calls, request ids, errors, downloaded bytes, and requested-document history.
Safety defaults are chosen for the user. Raw keys still appear once and are never stored.
Save structured requests for larger daily limits or shared team administration from the same account page.
Create a replacement key first, update your integration, confirm traffic, then revoke the old key manually.
| Status | Meaning | Typical Fix |
|---|---|---|
202 | Accepted but not ready yet (cache warming or extraction in progress). | Read retry_after_seconds and Retry-After, then poll status or retry after the delay. |
400 | Invalid CUSIP, scope, limit, include value, cursor, or date filter. | Use a normalized 9-character CUSIP and documented filters. |
401 | Missing, invalid, expired, or revoked key. | Create or test a key in My API Keys, then send it as Authorization: Bearer <key>. |
403 | Key lacks the required scope. | Create a replacement key with the preset that includes PDF download or extracted text access. |
404 | CUSIP or signed document id was not found. | Refresh doclist and retry with a returned link. |
429 | Daily request or download quota exceeded. | Use the quota headers and JSON reset time, then retry or save a higher-limit request from My API Keys. |
500 | Unexpected server error. | Include X-BondGov-Request-ID when contacting support. |
502 | Upstream fetch or cache storage unavailable. | Retry later; persistent failures should be escalated with the request id. |
503 | Background preparation or extraction queue unavailable. | Retry with Retry-After or download normally if the PDF is already ready. |
| Area | Guidance |
|---|---|
| Always log | Save the X-BondGov-Request-ID header from every response. Include it in support requests. Log quota headers (X-BondGov-RateLimit-Remaining, X-BondGov-Download-Remaining) to monitor usage before hitting limits. |
| Always store | Keep your bearer token only in a secret manager or environment variable. Never commit it to source control, logs, or shared documents. |
| Rotate safely | Use Create Replacement in My API Keys, update your environment variable, confirm traffic in recent requests, then revoke the old key manually. |
| Watch usage | My API Keys shows per-key usage, requested documents, request ids, errors, downloaded bytes, and a daily activity grid. Use it before escalating quota or authentication issues. |
| Never construct URLs | Use only the links and document_id values returned by doclist. Do not build download, text, status, or prepare URLs from scratch. Document ids are signed and server-issued. |
| Handle 202 and Retry-After | Cache warming and text extraction are asynchronous. When you receive HTTP 202, read retry_after_seconds and poll /status/ before retrying. Do not spin in a tight loop. |
| Paginate large result sets | Use limit, cursor, and doclist filters (category, source, date_from, date_to, cache_status, text_status) when syncing many documents. Follow the pagination.next_cursor field until pagination.has_more is false. |
| Use HEAD before bulk downloads | HEAD /{document_id}/download/ returns cache readiness and PDF metadata without streaming bytes. Use it to check before starting large download jobs. |
Up to ten fiscal years of income-statement totals extracted from an issuer's
filed annual financial statements, with the source document, page, column and
extractor confidence attached to every figure.
Coverage is deliberately narrow today. Only statements an
operator has reviewed and accepted are readable, and the accepted corpus is a
small fraction of what has been extracted. Read
coverage.accepted_statement_count on every response — it is the
measured number for that issuer, and it may be zero.
Ten-year totals with provenance, restatements, continuity segments, and an explicit absent-year ledger. issuer_key is an issuer slug, profile:<id>, cusip6:<6> or cusip9:<9>.
The full extracted line tree for one accepted statement, paged (default 100 lines, maximum 500).
curl -s "https://bondgov.com/api/v1/issuer-financials/state-of-california/income-statement/?years=10" \ -H "Authorization: Bearer $BONDGOV_API_KEY"
| Contract | What it means for your integration |
|---|---|
New scope: financials:read | Keys issued before this endpoint launched do not gain it automatically. Existing keys receive HTTP 403 insufficient_scope naming the scope. Create a replacement key in My API Keys to pick it up. |
| REST and MCP are separate credentials | An OAuth token issued for the MuniPro MCP server cannot authenticate this REST endpoint, and a REST API key is not an MCP credential. If you consume MuniPro over both, you need both. |
| A missing year is stated, never implied | Every year inside window.span_from..window.span_to is either a point with a real value or a row in absent_years carrying a reason (no_afs_pointer, pointer_not_ingested, extracted_not_accepted, extraction_rejected, statement_kind_mismatch, accepted_no_totals_extracted, accepted_outside_requested_window). The reason is derived from database state and always agrees with the row’s own statement_status_counts. Gaps are never zero-filled, and a genuine zero is a real point. |
| Every metric key is always present | All five keys appear in series[] on every response. Read status (series / single_year_only / not_extracted / suppressed_by_issuer) rather than inferring meaning from an absent key. |
| Truncation is declared | The window is capped at ten years. When more accepted years exist, window.truncated is true and window.next_years_before is the value to pass as ?before= to walk back. window.page_size_capped tells you your ?years= was clamped. |
| Statement kinds are never silently mixed | Modified-accrual and full-accrual statements are not comparable. By default the modal statement kind wins and off-kind years are reported in absent_years as statement_kind_mismatch. Pass ?mix_statement_kinds=true to opt in; comparability.basis_consistent then reports false. |
| Restatements are recorded, never reconciled | The year’s own annual report is authoritative. Where a later report’s comparative column disagrees, the point carries restated, restated_value, restated_in_fiscal_year and the signed difference. The two figures are never averaged. |
| The restatement scan is corpus-wide, and says how wide | has_restatements: false is only meaningful alongside restatement_scan, which reports scope, how many accepted statements it read, and complete. Detection is not limited to the page you asked for, so paging back through a walk cannot make a restatement disappear. If the scan could not widen it reports complete: false and raises restatement_scan_window_only rather than letting silence read as “no restatements”. |
| Is this the year’s own figure? | Each point carries value_is_authoritative and an authority block. When a page reaches a year only through a later report’s comparative column, authority.reason is own_acfr_outside_window and it names the statement holding the year’s own number and that number’s value — so two pages of one walk returning different dollars for the same year explain themselves instead of silently disagreeing. |
| Provenance names the exact cell | provenance carries the line, column and evidence span the figure was READ from — recorded when the figure won its slot, not re-found afterwards by matching the number. Two lines holding the same dollars, or a current and comparative column that happen to be equal, cannot cross-cite. A figure we cannot tie to a single cell (a computed revenues total) reports provenance_resolved: false with a warning rather than a plausible-looking source. |
| Continuity breaks are structural | Use segments[] when charting. A polyline drawn across a break is a false statement about the issuer. |
| Numbers vs display strings | value is a lossless decimal string, value_float is a documented-lossy convenience, and value_display is for humans only. Values are full magnitude — scale is already applied, so never re-scale an “(in thousands)” statement. |
| These are extracted figures | Not audited statements, and not investment advice. versions stamps the extraction, taxonomy, triage and series versions; the external taxonomy mappings are still draft. Always confirm against the source filing linked from each point. |
| An issuer can suppress this | An issuer who hides the income-statement module on their public page returns HTTP 200 with coverage.status = suppressed_by_issuer and no figures. That is a fact about the issuer, not an error. |
| Suppression is per-member on a consolidated issuer | When an issuer rolls up several reporting-entity members, each member’s own suppression is honoured — a member that has hidden its income statement contributes no figure to any sibling’s series and no line tree on any sibling’s path. The consolidation block reports readable_member_profile_ids and suppressed_member_profile_ids so a short series is explicable rather than mysterious, and consolidation_members_suppressed_by_issuer is raised as a warning. Every point and every statement header names the member that FILED it in issuer_profile_id; statement detail adds filed_by_issuer. |
| Faults are 5xx, never empty data | An issuer with no accepted statements is HTTP 200 with a zero coverage count. A backend fault is HTTP 500 financial_series_unavailable. Never treat one as the other. |
Issuer search, CUSIP6 lookup, feeds, and embeddable profile widgets remain public read-only endpoints. These do not use investor document API keys.