> ## Documentation Index
> Fetch the complete documentation index at: https://docs.canary.effectiveai.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Search and download filings

> Find regulatory filings, inspect their stored files, and download a selected version.

Use `POST /api/v2/filings/search` to search filing documents or browse by metadata.
You need an API key with permission to view
SERFF filings. Readonly keys can use this operation.

## Search document text

Set `API_BASE` to your API host (for example, `https://canary.effectiveai.app`) and
`EFFECTIVE_API_KEY` to your API key, then run:

```bash theme={null}
curl --request POST "$API_BASE/api/v2/filings/search" \
  --header "Authorization: Bearer $EFFECTIVE_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "query": "roof exclusions",
    "filters": [
      {"field": "state", "operator": "in", "values": ["CA", "TX"]},
      {"field": "toiCode", "operator": "eq", "value": "04.0"},
      {"field": "normalizedOutcome", "operator": "eq", "value": "APPROVED"},
      {"field": "submissionDate", "operator": "gte", "value": "2024-01-01"}
    ],
    "includePassages": true,
    "limit": 20
  }'
```

The response has a `filings` array, a nullable `nextCursor`, and an
`exhaustive` flag. Each filing includes its ID, source and native reference,
company, state, classifications and dates. Omit `includePassages` to use the
configured default. Set it to `true` to request matching passages or `false` to
skip passages. Matching file IDs and names may still be returned when passages
are disabled. Each match contains the file ID, nullable version ID, name, and
any available passages and page numbers. Requesting passages does not guarantee
they are available for every file. This option has no effect when browsing
without a query.
Missing metadata and page numbers are `null`. No matching filings returns an empty
`filings` array.

Filters combine with **AND**. Values within `in` and `containsAny` combine with
**OR**. Use the normalized `toiCode`, `subToiCode`, `normalizedOutcome`, and `filingTypeNormalized`
values defined in the API reference. Raw insurance classification labels and status
strings in the response may vary by source. Text/identifier queries default to
recent filings (2018+ and undated); metadata-only requests default to all dates.
Set `dataScope` explicitly to `historical`, `recent`, or `all` as needed.

Carrier filters use `carrierIds` (`containsAny`/`notContainsAny`) and canonical
`naic:00123` IDs or numeric shorthand **strings**. Group equality uses `groupId`,
for example `group:361`. State values normalize to uppercase. Search's
`trackingNumber` filters and sorts use backend reference spelling (including
`FL-` for IRFS); ordinary GET discovery accepts native source-qualified references.

A match's `versionId` is currently null: the backend does not identify the exact
immutable bytes behind the evidence. Current downloads may differ from indexed
text. Do not treat today's file version as the evidence version.

To match an exact phrase, quote it inside `query`, for example
`"query": "\"roof exclusions\""`. Exact phrases require a state, company,
NAIC, tracking-number or date filter to narrow the search. Exclusion filters alone
do not qualify. Without a narrowing filter, the shared search service removes the
quotes and performs keyword search.

## Browse without document text

Omit `query` to list filings by metadata alone. For example, replace the request
body above with:

```json theme={null}
{
  "filters": [
    { "field": "companyName", "operator": "contains", "value": "Insurance" },
    { "field": "filingTypeNormalized", "operator": "in", "values": ["FORM", "RATE"] },
    { "field": "submissionDate", "operator": "between", "from": "2024-01-01", "to": "2024-12-31" }
  ],
  "sort": [{ "field": "submissionDate", "direction": "desc" }],
  "limit": 50
}
```

Both text search and browsing default to submission date descending, with a page
size of 50 (maximum 200). To rank text matches by relevance, explicitly set
`sort` to `[{"field":"relevance","direction":"desc"}]`. Browsing does not return snippets.

Tracking-number queries use the existing identifier lookup: complete identifiers
match exactly; partial identifiers use prefix matching with up to 10 results.
Metadata filters and data scope apply before the lookup result limit.

Metadata sorts use filing ID ascending as a final tie-breaker. Within text results,
missing values sort last; metadata-only sorting puts missing values first for descending sorts and last
for ascending sorts. Relevance retains backend
score order, including backend tie behavior. Neither ordering freezes membership.

## Continue a search

When `nextCursor` is non-null, repeat the same request with that value in `cursor`.
Keep the query, filters, data scope, sorting, passage setting and page size unchanged.
Opaque server-held cursors bind the normalized query and caller scope and expire
24 hours after the first page; continuation never extends this lifetime. Tampered,
expired, or differently bound cursors return `400 invalid_cursor`. Equivalent
explicit defaults and reordered set-valued filters continue successfully.

The private position is a live backend offset, not a saved search. Every page
reruns the query; index/ranking changes and concurrent edits may cause repeats or
omissions. Deduplicate IDs and restart when a fresh view is needed. Prefer GET
browsing for its keyset behavior when the simpler metadata filters suffice.

Text search returns a **bounded candidate window**, with `exhaustive: false`.
With `includePassages: true`, search retrieves at most 100 candidate documents.
With `false` or an omitted setting, it requests up to 1,200. The omitted setting
preserves the configured passage behavior. Requesting passages can therefore
reduce the available result window. Several documents can belong to one filing, so the number of filings
can be smaller. Metadata sorting applies within this window. A null cursor means
the available window is exhausted, and does not prove no more corpus matches exist.
Narrow your filters to investigate more specific results. No exact corpus total
is returned.

Metadata browsing returns `exhaustive: true`: pagination can traverse the matching
metadata records. This describes the available dataset, not completeness of every
state's public filings or document indexing coverage.

## Follow a result to bytes

Use IDs returned by search and the file inventory; never derive them from names.

```http theme={null}
GET /api/v2/filings/{filingId}
GET /api/v2/filings/{filingId}/files?limit=50
GET /api/v2/files/{fileId}/download?versionId={versionId}
```

Follow inventory cursors until null and download selected files with bounded
concurrency. Use an inventory version when present to pin a download, while
keeping its identity distinct from the unknown search-evidence version. Follow
the download redirect. Retrying the same version can renew its short-lived URL;
a deleted/unavailable version must fail rather than substitute current bytes.
Metadata-only IRFS can return no stored files; reads never trigger acquisition.

## Handle errors

API-generated failures include `error` and `code`; responses are private and not
cacheable. JSON requests are limited to 64 KiB, reject unknown body/query fields,
and require `application/json`.

* `400 invalid_request`: correct filters, dates, sorting or malformed JSON.
* `400 invalid_cursor`: restart with the original semantic request and no cursor.
* `401 unauthenticated`: check the API key.
* `403 forbidden`: the caller needs current Filing access within its credential scope.
* `413 request_too_large` / `415 unsupported_media_type`: correct the request body.
* `503 temporarily_unavailable`: respect `Retry-After`; narrow expensive queries
  before retrying timeouts. Cursor-store failure does not silently drop continuation.
* `500 internal_error`: unexpected failure; no backend details are exposed.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.