Shell snippets you can copy-paste. Every snippet uses shell variables so the moving parts are visible — set these two first:
# Your API key, minted at /api-keys in the dashboard.
# The full key is shown once at creation.
export FS_KEY="fs_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
# The API base URL.
export BASE="https://fieldscroll.app/api/v1"The other ids used below (ORG, FORM, USER, TEMPLATE) are UUIDs
you can read out of the dashboard URL when viewing the matching page,
or from the id field of an earlier API response.
Health
No key needed — this is the public uptime probe.
curl -s "$BASE/health" | jqA healthy response:
{
"data": { "status": "ok", "db_latency_ms": 7, "version": "1.0.0", "commit": "abc1234" },
"request_id": "req_..."
}List your organizations
curl -s -H "Authorization: Bearer $FS_KEY" "$BASE/organizations" | jqEach row in data carries an id — that UUID is the ORG value the
later snippets use.
Create an organization
Only instance-scoped keys can do this; an organization-scoped key gets
403. The Idempotency-Key header makes a retry safe: if the same
request runs twice, the second call returns the first result instead
of creating a duplicate.
curl -s -X POST \
-H "Authorization: Bearer $FS_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Acme Inc","slug":"acme-inc"}' \
"$BASE/organizations" | jqCreate a draft form
# The organization that should own the form.
ORG="00000000-0000-0000-0000-000000000000"
curl -s -X POST \
-H "Authorization: Bearer $FS_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Site inspection",
"form_identifier": "site_inspection",
"fields": [
{ "id": "site_id", "label": "Site ID", "type": "text",
"interface": "single_line", "attributes": {} },
{ "id": "condition", "label": "Condition", "type": "list",
"interface": "dropdown",
"attributes": { "options": [
{ "label": "good" }, { "label": "fair" }, { "label": "poor" }
] } }
]
}' \
"$BASE/organizations/$ORG/forms" | jqThe response's data.id is the FORM value below. The form is a
draft at this point — nobody in the field can see it yet.
Publish a form
Publishing creates a numbered, immutable version that assigned devices download on their next sync.
# The draft form to publish (from the previous response).
FORM="00000000-0000-0000-0000-000000000000"
curl -s -X POST \
-H "Authorization: Bearer $FS_KEY" \
-H "Content-Type: application/json" \
-d '{"changelog":"initial version"}' \
"$BASE/organizations/$ORG/forms/$FORM/publish" | jqSubmit a record
The same idempotency safety applies — submitting twice with the same
Idempotency-Key creates one record, not two.
# The user the submission is recorded under.
USER="00000000-0000-0000-0000-000000000000"
curl -s -X POST \
-H "Authorization: Bearer $FS_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d "{
\"created_by\": \"$USER\",
\"field_values\": {
\"site_id\": \"S-12\",
\"condition\": \"fair\"
}
}" \
"$BASE/organizations/$ORG/forms/$FORM/records" | jqThe keys inside field_values are the form's field id values. The
payload is checked against the published version the record binds to:
an unknown id, a wrong value type, or a list value outside the current
options returns 400 with a details array naming each problem.
Update a record
An update is a merge: only the field ids you send change, and sending
null as a value clears that field. Everything else stays as it was.
# The record to update (from a create response or a list).
RECORD="00000000-0000-0000-0000-000000000000"
curl -s -X PATCH \
-H "Authorization: Bearer $FS_KEY" \
-H "Content-Type: application/json" \
-d '{ "field_values": { "condition": "good" } }' \
"$BASE/organizations/$ORG/forms/$FORM/records/$RECORD" | jqBy default the update is validated against the record's own form
version — publishing a new version of the form does not break this
call. To move the record to the newest version instead, add
"schema": "current" to the body: the whole merged record is checked
against the current version, and the record adopts it when it
conforms. A record that is checked out (dispatched)
returns 409 until the dispatch ends.
List records, filter + paginate
# Up to 100 records submitted since May 1.
curl -s -H "Authorization: Bearer $FS_KEY" \
"$BASE/organizations/$ORG/forms/$FORM/records?limit=100&created_after=2026-05-01T00:00:00Z" | jqWhen more than 100 match, the response's meta.next_cursor holds a
bookmark — pass it back as cursor= to resume after the last row you
received. The loop below walks the whole list that way:
URL="$BASE/organizations/$ORG/forms/$FORM/records?limit=100"
while [ -n "$URL" ]; do
# Fetch one page.
RESP=$(curl -s -H "Authorization: Bearer $FS_KEY" "$URL")
# Do something with the rows — here, print them.
echo "$RESP" | jq '.data[]'
# Read the bookmark; empty means this was the last page.
CURSOR=$(echo "$RESP" | jq -r '.meta.next_cursor // empty')
if [ -z "$CURSOR" ]; then break; fi
URL="$BASE/organizations/$ORG/forms/$FORM/records?limit=100&cursor=$CURSOR"
donePagination explains why the API uses cursors instead of page numbers.
Subscribe to webhooks
curl -s -X POST \
-H "Authorization: Bearer $FS_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "production",
"url": "https://hooks.example.com/fieldscroll",
"events": ["record.created", "form.published"]
}' \
"$BASE/organizations/$ORG/webhooks" | jqNote: records created through this API do not fire webhooks — see webhooks and the API.
Retry a failed delivery
# Both ids come from the delivery history:
# GET $BASE/organizations/$ORG/webhooks/$WEBHOOK/deliveries
WEBHOOK="00000000-0000-0000-0000-000000000000"
DELIVERY="00000000-0000-0000-0000-000000000000"
curl -s -X POST \
-H "Authorization: Bearer $FS_KEY" \
"$BASE/organizations/$ORG/webhooks/$WEBHOOK/deliveries/$DELIVERY/retry" | jqRender and deliver a report
The body you post is exactly what the template's merge fields see —
a template field {$name} reads the name key below. The render is
queued, the document is rendered once, and it is then sent to every
destination configured on the report; the response confirms the queued
run.
TEMPLATE="00000000-0000-0000-0000-000000000000"
curl -s -X POST \
-H "Authorization: Bearer $FS_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Site 12", "score": 84 }' \
"$BASE/organizations/$ORG/reports/$TEMPLATE/merge" | jqResponse:
{ "merge_run_id": "a1b2c3d4-..." }Track it under the template's deliveries — one row per destination, each with its own status and retries:
curl -s -H "Authorization: Bearer $FS_KEY" \
"$BASE/organizations/$ORG/reports/$TEMPLATE/deliveries" | jq