> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.defdex.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.defdex.io/_mcp/server.

# Published datasets and pagination

Use the same list/detail pattern for every collection, including organizations,
agencies (filtered organizations), programs, awards and relationship data.

## Discover the schema

`GET /api/v1/datasets` returns `data` and `build_id`. Each catalog entry contains:

| Field              | Meaning                                                                |
| ------------------ | ---------------------------------------------------------------------- |
| `name`             | Exact dataset name for the URL, SDK, CLI and MCP                       |
| `fields`           | Allowed output/exact-filter fields with published database type labels |
| `keys`             | Fields used for published ordering and identity                        |
| `identity`         | `stable-key` or `build-scoped-position`                                |
| `required_filters` | Non-null exact filters required before listing                         |

Fields are an explicit reviewed projection. New database columns are not automatically
exposed. Some expensive collections are unavailable; the runtime catalog decides access.

## List and search

`GET /api/v1/datasets/{dataset}` returns `{data, next_cursor, build_id}`.
Each item is `{record_id, attributes}`. A positional page may also have `truncated`.

| Parameter | Behavior                                                       |
| --------- | -------------------------------------------------------------- |
| `limit`   | Integer 1–100; default 25                                      |
| `q`       | Case-insensitive substring, up to 200 characters               |
| `filters` | URL-encoded JSON object of exact field values; up to 12 fields |
| `cursor`  | Opaque `next_cursor` from the preceding successful page        |

`q` searches whichever of `name`, `title`, `description`, `summary` and
`recipient_name` exist in the dataset. If none exists, text search returns 400.
It does not search aliases, codes or every string field. It provides no fuzzy
matching, relevance ranking or custom ordering.

Exact filters use AND. Supported values are a string (up to 500 characters), number,
boolean or null. Null means SQL `IS NULL`. Use strings for precise large integers
and decimals. Array containment, nested JSON predicates, OR, ranges, field selection
and arbitrary sorting are not supported. Unknown filter fields return 400.

```sh
curl --fail-with-body --get \
  'https://app.defdex.io/api/v1/datasets/organizations' \
  --header "Authorization: Bearer $DEFDEX_API_KEY" \
  --data-urlencode 'filters={"type":"agency","sub_type":"department"}' \
  --data-urlencode 'limit=25'
```

Every database read has a five-second query budget. A query-budget 400 means narrow
with exact filters; repeating the same broad request is unlikely to help. In particular,
`summary-award-actions` requires `award_key`, and `summary-fy-totals` requires
`fiscal_year`. See each collection's `required_filters`.

## Follow pagination

Continue with the same dataset, `q` and `filters`; add the returned `next_cursor`.
Do not decode, edit or construct a cursor. Save a cursor only after processing its
successful page. Never advance after a failed request.

```python
from defdex import DefDexClient
api = DefDexClient()
cursor = None
while True:
    page = api.list("organizations", filters={"type": "agency"},
                    limit=100, cursor=cursor)
    for record in page["data"]:
        print(record["attributes"]["id"], record["attributes"]["name"])
    if page.get("truncated"):
        raise RuntimeError("Query cap reached: narrow exact filters before continuing")
    cursor = page["next_cursor"]
    if cursor is None:
        break
```

This example stops on an exception; use the bounded retry policy in [errors](/errors)
for production workloads. Make requests sequentially and respect the shared account limit.

## Know which identity you are using

| Identity                | Pagination                                              | Detail lookup               | Across builds                                           |
| ----------------------- | ------------------------------------------------------- | --------------------------- | ------------------------------------------------------- |
| `stable-key`            | Keyset in the collection's key order; no positional cap | Returned opaque `record_id` | ID/cursor can survive; values and membership may change |
| `build-scoped-position` | Position in a deterministic published ordering          | Returned opaque `record_id` | Old IDs/cursors return 409                              |

Positional datasets expose at most **10,000 positions per query**. If more matches
exist, the final accessible page has `truncated: true` and `next_cursor: null`.
A null cursor in that case does not mean a complete export. Narrow exact filters;
there is no bulk-export endpoint.

A stable-key traversal is not a point-in-time snapshot across a publication change.
Compare `build_id` across pages and related reads. Restart if your task requires
one consistent build. The API rejects a build change during an individual read.
Award convenience cursors are build-bound and return 409 after a publication change.

## Read a single record

`GET /datasets/{dataset}/{record_id}` returns `{data, build_id}`. Use the ID returned
by the same collection, encoded as one path segment. `attributes.id`, an external
award key and a website slug are not interchangeable with `record_id`.

Use an exact `id` or `slug` filter to resolve an entity first. The SDKs encode detail
IDs automatically. Retain both the entity key and opaque record ID when they serve
different purposes in your application.