Skip to main content

Finding records

Finds are POST requests to the _find endpoint. The query grammar is compact and, once you know the two rules, predictable:

  1. Fields inside one query object are ANDed. A record must match all of them.
  2. Multiple query objects are ORed. A record may match any of them.
{
"query": [
{ "Status": "Active", "UnitPrice": ">=100" },
{ "Status": "Backordered" }
]
}

That reads: active records priced at 100 or more, or any backordered record.

Run a find

curl -sS -X POST \
"https://fms.example.com/fmi/data/v1/databases/Inventory/layouts/Products/_find" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": [{ "Status": "Active", "UnitPrice": ">=100" }],
"sort": [{ "fieldName": "UnitPrice", "sortOrder": "descend" }],
"offset": "1",
"limit": "25"
}'

Note the quirk: in the find body, offset and limit are strings ("25"), unlike the integer _offset and _limit query parameters on the records endpoint. Sending integers usually works on current servers, but the documented contract is strings, and strings are what survive server upgrades.

Operators

Criteria are strings, and the leading characters carry FileMaker's find operators:

CriteriaMatches
KarlWords beginning with "Karl" anywhere in the field
==Karl Fischer titrant, 5LThe entire field, exactly
=KarlThe whole word "Karl"
Karl*"Karl" followed by anything (explicit wildcard)
>=100Numbers, dates, or times greater than or equal to the value
<2026-01-01Dates before January 1, 2026
2025-01-01...2025-12-31Anything in the range, inclusive
!Duplicate values across records

To exclude matches, add "omit": "true" (a string, matching the convention this API uses) to a query object:

{
"query": [
{ "Status": "Active" },
{ "Name": "*deprecated*", "omit": "true" }
]
}

When nothing matches

An empty result is HTTP 404 with FileMaker code 401. It is not an authentication problem, and in most workflows it is not an error at all: it is a found set of zero. Integrations that treat it as a failure page their own users with false alarms.

Wrap your find calls so an empty set comes back as an empty list:

def find_records(layout: str, payload: dict) -> list:
"""Run a find; return [] when no records match instead of raising."""
resp = requests.post(f"{BASE}/layouts/{layout}/_find",
headers=HEADERS, json=payload, timeout=30)
if resp.status_code == 404:
body = resp.json()
if body["messages"][0]["code"] == "401":
return [] # zero matches: a result, not a failure
resp.raise_for_status() # anything else is a real error
return resp.json()["response"]["data"]

The same 404 status can also mean a wrong layout name (code 105) or database name (code 100), which is exactly why the wrapper checks the body code instead of trusting the HTTP status alone. The two-layer error model is covered in Error handling.

Paging a large found set

Run the find once with a small limit to learn dataInfo.foundCount, then walk the set with increasing offset. Keep the sort stable between pages: an unsorted find has no guaranteed order, and paging an unstable order silently drops and duplicates records across page boundaries.