curl examples

Copy-paste shell snippets for every common API operation, with each step spelled out.

Updated 2026-08-07

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

A 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" | jq

Each 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" | jq

Create 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" | jq

The 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" | jq

Submit 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" | jq

The 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" | jq

By 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" | jq

When 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"
done

Pagination 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" | jq

Note: 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" | jq

Render 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" | jq

Response:

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