Working with records
Record operations live under a layout, and the layout is not a formality: it defines which fields exist for the call. A field that is not placed on the layout does not exist as far as that request is concerned (error 102). When an integration "cannot see" a field, check the layout before anything else.
All examples assume a session token in $TOKEN; see Authentication.
Create a record
POST the new record's fields as a fieldData object. Values are strings or numbers; date, time, and timestamp fields take text in the file's configured formats. Container fields cannot be set here (they use a separate upload call).
- curl
- Python
- JavaScript
curl -sS -X POST \
"https://fms.example.com/fmi/data/v1/databases/Inventory/layouts/Products/records" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"fieldData": {
"Name": "Karl Fischer titrant, 5L",
"SKU": "KF-5000",
"UnitPrice": 412.50,
"Status": "Active"
}
}'
import requests
BASE = "https://fms.example.com/fmi/data/v1/databases/Inventory"
HEADERS = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
payload = {
"fieldData": {
"Name": "Karl Fischer titrant, 5L",
"SKU": "KF-5000",
"UnitPrice": 412.50,
"Status": "Active",
}
}
resp = requests.post(f"{BASE}/layouts/Products/records",
headers=HEADERS, json=payload, timeout=30)
resp.raise_for_status()
record_id = resp.json()["response"]["recordId"]
print("Created record", record_id)
const BASE = "https://fms.example.com/fmi/data/v1/databases/Inventory";
const resp = await fetch(`${BASE}/layouts/Products/records`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
fieldData: {
Name: "Karl Fischer titrant, 5L",
SKU: "KF-5000",
UnitPrice: 412.5,
Status: "Active",
},
}),
});
if (!resp.ok) throw new Error(`Create failed: ${resp.status}`);
const { response } = await resp.json();
console.log("Created record", response.recordId);
The response returns the new record's identifiers. Keep both: recordId addresses the record, modId versions it.
{
"response": { "recordId": "1042", "modId": "0" },
"messages": [{ "code": "0", "message": "OK" }]
}
Get one record
curl -sS \
"https://fms.example.com/fmi/data/v1/databases/Inventory/layouts/Products/records/1042" \
-H "Authorization: Bearer $TOKEN"
The record arrives in response.data[0] with its fieldData, any portalData from portals on the layout, and its current recordId and modId.
List records
GET the records endpoint with paging and sorting query parameters. Two behaviors to plan around: _offset starts at 1, and _sort takes a JSON array as a URL-encoded string.
curl -sS -G \
"https://fms.example.com/fmi/data/v1/databases/Inventory/layouts/Products/records" \
-H "Authorization: Bearer $TOKEN" \
--data-urlencode "_offset=1" \
--data-urlencode "_limit=20" \
--data-urlencode '_sort=[{"fieldName":"Name","sortOrder":"ascend"}]'
The response includes a dataInfo block; use it to drive paging instead of guessing:
{
"response": {
"dataInfo": {
"database": "Inventory",
"layout": "Products",
"table": "Products",
"totalRecordCount": 3210,
"foundCount": 3210,
"returnedCount": 20
},
"data": ["..."]
},
"messages": [{ "code": "0", "message": "OK" }]
}
_limit defaults to 100. For large tables, page deliberately; asking for everything at once eventually hits the response size limit (error 953).
Edit a record
PATCH the record with the fields you want to change. Only the fields you send are touched.
The optional modId turns the edit into optimistic locking: include the modId you read, and the server rejects the write with error 306 if anyone changed the record in between. For human-facing workflows, send it. For last-writer-wins pipelines, omit it, and do so on purpose rather than by accident.
payload = {
"fieldData": {"Status": "Discontinued"},
"modId": "7", # from the read; remove for last-writer-wins
}
resp = requests.patch(f"{BASE}/layouts/Products/records/1042",
headers=HEADERS, json=payload, timeout=30)
if resp.status_code == 400 and resp.json()["messages"][0]["code"] == "306":
print("Record changed since read. Re-read, reconcile, resubmit.")
else:
resp.raise_for_status()
print("New modId:", resp.json()["response"]["modId"])
Delete a record
curl -sS -X DELETE \
"https://fms.example.com/fmi/data/v1/databases/Inventory/layouts/Products/records/1042" \
-H "Authorization: Bearer $TOKEN"
Deletion is immediate and permanent at the API level, and it cascades to related records where the file's relationship graph says so. If the layout's underlying table has delete-related rules, the API honors them exactly as FileMaker Pro would. Know the file before you point a delete loop at it.