Pagination & Filtering
Narrow result sets with flat filters, boolean trees, or natural language, then iterate through them with cursor-based pagination.
Every list endpoint pairs a flexible filtering system with cursor-based pagination. Filter to define what you want, then page through the matching records — no matter how large the dataset.
Pagination
The Model Match API uses cursor-based pagination to let you iterate through datasets that can contain hundreds of millions of records. Each response includes an opaque cursor that points to the next page of results.
Cursor-based pagination is more reliable than offset-based approaches — it guarantees stable iteration even when data is being added or updated between requests.
Basic Usage
Pass a pagination object in your request body:
{
"flatFilters": { "state": "CA" },
"pagination": {
"size": 50
}
}The response includes your results, a total count, and a cursor for the next page:
{
"data": [{ ... }, { ... }],
"total": 142857,
"cursor": "eyJzb3J0IjpbIjIwMjQt..."
}To get the next page, pass the cursor back:
{
"flatFilters": { "state": "CA" },
"pagination": {
"size": 50,
"cursor": "eyJzb3J0IjpbIjIwMjQt..."
}
}When there are no more results, the cursor field is absent from the response.
Parameters
| Field | Type | Default | Description |
|---|---|---|---|
size | number | 25 | Results per page, maximum 100 |
cursor | string | — | Cursor from a previous response |
Response Fields
| Field | Description |
|---|---|
data | Array of results for this page |
total | Total matching records (accurate count, not capped) |
cursor | Cursor for the next page — absent when you've reached the end |
Iterating Through All Results
To collect all matching records, keep requesting until no cursor is returned:
const allResults = [];
let cursor: string | undefined;
do {
const response = await fetch("https://api.modelmatch.com/v1/loans", {
method: "POST",
headers: {
"x-api-key": "mm_your_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
flatFilters: { state: "CA" },
pagination: { size: 100, cursor },
}),
});
const result = await response.json();
allResults.push(...result.data);
cursor = result.cursor;
} while (cursor);
console.log(`Fetched ${allResults.length} records`);With the TypeScript SDK, pagination is handled for you:
const result = await client.loans.list({
body: {
flatFilters: { state: "CA" },
pagination: { size: 100 },
},
});
// result.data is the response body; result.data.cursor is the next pageImportant Notes
Cursors are tied to your query. A cursor encodes the sort order and filters of the original request. Don't reuse a cursor from one query with a different sort order or filter set — the results will be unpredictable.
- Maximum page size is 100. Requesting more will be clamped silently.
- Cursors are opaque strings. Don't try to parse, modify, or construct them.
- The
totalfield is always accurate — it reflects the true count of matching records with no upper cap. - Use the largest page size you can. Fewer round-trips means faster iteration. If you're collecting all results, use
size: 100.
Filtering
Every list endpoint supports two ways to filter data, from simplest to most powerful:
- Flat filters — key-value pairs for straightforward queries
- Advanced filters — boolean trees for complex logic (AND/OR/NOT)
Both approaches can be combined with the period parameter to scope results to a time range. Available filter fields vary by entity — e.g. loans expose mortgageAmount, transactionType, and conforming; originators expose nmlsId, volume, and companyName.
Flat Filters
The simplest and most common way to filter. Pass field names as keys with the values you want to match:
{
"flatFilters": {
"state": "CA",
"transactionType": "purchase"
}
}By default, all filters are combined with AND — every condition must match.
Value Syntax
Flat filters support several value formats depending on what you need:
| Syntax | Meaning | Example |
|---|---|---|
"value" | Exact match | "state": "CA" |
["a", "b"] | Any of (IN) | "state": ["CA", "TX", "FL"] |
{ "gte": n, "lte": n } | Numeric range | "mortgageAmount": { "gte": 500000 } |
{ "match": "text" } | Full-text search | "lender": { "match": "Wells Fargo" } |
true / false | Boolean match | "conforming": true |
null | Field is null | "endDate": null |
For negation (not equal) or existence checks, use Advanced Filters with the neq and exists operators.
Range Filters
Use gte, lte, gt, and lt to filter numeric fields. You can combine them for bounded ranges:
{
"flatFilters": {
"mortgageAmount": { "gte": 500000, "lte": 1000000 }
}
}Geo Filtering
Filter by distance from a geographic point:
{
"flatFilters": {
"geoPoint": {
"lat": 34.0522,
"lon": -118.2437,
"radius": "50mi"
}
}
}radius accepts units like "50mi" (miles) or "80km" (kilometers).
OR Mode
To match records where any filter matches instead of all, set mode: "or". To match a field against several values, pass an array:
{
"flatFilters": {
"mode": "or",
"city": ["Los Angeles", "San Francisco"]
}
}Advanced Filters
When you need full boolean logic — nested ANDs, ORs, and complex conditions — use advancedFilters with a recursive tree. Each node is tagged with a type: groups are { "type": "and" | "or", "children": [...] }, negation is { "type": "not", "child": {...} }, and a leaf condition is { "type": "filter", "field", "op", "value" }.
{
"advancedFilters": {
"type": "and",
"children": [
{ "type": "filter", "field": "state", "op": "eq", "value": "CA" },
{
"type": "or",
"children": [
{ "type": "filter", "field": "transactionType", "op": "eq", "value": "purchase" },
{ "type": "filter", "field": "transactionType", "op": "eq", "value": "refinance" }
]
},
{ "type": "filter", "field": "mortgageAmount", "op": "gte", "value": 500000 }
]
}
}This query finds loans in California that are either purchases or refinances, with a mortgage amount of $500k+.
Operators
| Operator | Description | Accepts |
|---|---|---|
eq | Equals | string, number |
neq | Not equals | string, number |
in | Any of | array |
gt / gte | Greater than (or equal) | number, date |
lt / lte | Less than (or equal) | number, date |
between | Inclusive range | [min, max] |
match | Full-text search | string |
exists | Field exists/missing | boolean |
geoDistance | Within distance | { lat, lon, radius } |
For "none of," wrap an in condition in a not group.
Nesting
You can nest and and or groups as deeply as you need. Each node is either a condition ({ "type": "filter", "field", "op", "value" }), a group ({ "type": "and" \| "or", "children": [...] }), or a negation ({ "type": "not", "child": {...} }).
Period Scoping
All filter types can be combined with the period parameter to restrict results to a time window:
{
"flatFilters": { "state": "CA" },
"period": "last12Months"
}period accepts one of a fixed set of values:
| Value | Scope |
|---|---|
"2017" … "2026" | A full calendar year |
"last3Months" | Trailing 3 months from today |
"last6Months" | Trailing 6 months |
"last12Months" | Trailing 12 months |
"last14Months" / "last16Months" / "last18Months" / "last24Months" | Trailing 14 / 16 / 18 / 24 months |
"yearToDate" | January 1 of the current year through today |
"allTime" | No time restriction |