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

FieldTypeDefaultDescription
sizenumber25Results per page, maximum 100
cursorstringCursor from a previous response

Response Fields

FieldDescription
dataArray of results for this page
totalTotal matching records (accurate count, not capped)
cursorCursor 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 page

Important 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 total field 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:

  1. Flat filters — key-value pairs for straightforward queries
  2. 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:

SyntaxMeaningExample
"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 / falseBoolean match"conforming": true
nullField 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

OperatorDescriptionAccepts
eqEqualsstring, number
neqNot equalsstring, number
inAny ofarray
gt / gteGreater than (or equal)number, date
lt / lteLess than (or equal)number, date
betweenInclusive range[min, max]
matchFull-text searchstring
existsField exists/missingboolean
geoDistanceWithin 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:

ValueScope
"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

On this page