Flitch

Endpoints

Every route on /api/v1, its parameters, and its response.

All paths are relative to https://app.flitch.io/api/v1 and all require an Authorization: Bearer header.

MethodPathScope
GET/medata:read
GET/datasetsdata:read
GET/datasets/{id}data:read
GET/datasets/{id}/rowsdata:read
GET/connections/{id}/querydata:read
GET/tables/{id}/rowsdata:read
POST/tables/{id}/rowstables:write
PATCH/tables/{id}/rows/{rowId}tables:write
DELETE/tables/{id}/rows/{rowId}tables:write

Identity

GET /me

Confirms the token works and reports what it belongs to. Usually the first call a client makes after sign-in, so it never has to hardcode a space id.

For an API key:

{
  "kind": "api-key",
  "name": "Field app",
  "scopes": ["data:read", "tables:write"],
  "teams": [{ "id": "…" }],
  "allowlist": { "datasets": ["…"], "connections": ["…"], "tables": ["…"] }
}

For a session token, teams lists every space the person belongs to, with id, name, isPersonal, and role. There is no allowlist, because membership is the boundary.

Datasets

GET /datasets

Lists datasets. For an API key this is exactly what its allowlist names, so the response doubles as the answer to "what can this credential see". For a session token, pass ?teamId= (from /me).

{
  "datasets": [
    {
      "id": "…",
      "name": "Jobs",
      "type": "input_table",
      "rowCount": 412,
      "columns": ["job_id", "address", "status"],
      "connectionId": null,
      "connectionType": null,
      "lastRefreshedAt": null,
      "updatedAt": "2026-08-10T04:11:22.000Z"
    }
  ]
}

columns is the schema, so a single list call is enough to discover column names across a whole space.

GET /datasets/{id}

One dataset's schema and freshness, without its rows. Use this when you already hold an id and do not want to read a whole space to learn three fields about it.

{
  "id": "…",
  "name": "Jobs",
  "type": "input_table",
  "rowCount": 412,
  "writable": true,
  "columns": [
    { "name": "job_id", "type": "string" },
    { "name": "status", "type": "string" }
  ],
  "connectionId": null,
  "connectionType": null,
  "lastRefreshedAt": null,
  "updatedAt": "2026-08-10T04:11:22.000Z"
}

columns pairs each name with its type here, rather than the two parallel lists the row endpoint returns, because a client reading a schema wants one list. writable says whether the rows can be changed through /tables/{id}/rows.

GET /datasets/{id}/rows

Rows of an ordinary dataset: an uploaded file, or one backed by a connection. Input tables are writable and live under /tables instead.

ParameterDescription
maxRowsMaximum rows to return. Omit for all.
offsetRows to skip. Defaults to 0.
columnsComma-separated list of columns to return.
filtersURL-encoded JSON object of exact matches, for example {"status":"open"}.
{
  "success": true,
  "data": [{ "job_id": "J-1042", "status": "open" }],
  "metadata": {
    "datasetId": "…",
    "datasetName": "Jobs",
    "totalRows": 412,
    "returnedRows": 1,
    "offset": 0,
    "hasMore": true,
    "columns": ["job_id", "address", "status"],
    "columnTypes": { "job_id": "string" },
    "lastUpdated": "2026-08-10T04:11:22.000Z"
  }
}

Page with offset until hasMore is false.

Connections

GET /connections/{id}/query

Queries the source behind a connection, through the same cache and rate-limit guard the app itself uses.

ParameterDescription
limit, offsetPaging.
filtersURL-encoded JSON object of exact matches.
orderBy, orderDirectionSort column, and ASC or DESC.
datasetIdWhich dataset in the connection to query. Required when the connection carries more than one.
formatSet to geojson for spatial sources.
paramsURL-encoded JSON of upstream query parameters, passed through unchanged. GeoJSON only.

A tabular response is { "success": true, "data": [...], "cached": true | false, "metadata": {...} }.

GeoJSON

format=geojson returns the source's own GeoJSON untouched, alongside a truncation flag:

{
  "success": true,
  "geojson": { "type": "FeatureCollection", "features": [] },
  "metadata": {
    "connectionId": "…",
    "datasetId": "…",
    "sourceType": "arcgis",
    "truncated": false
  },
  "cached": true
}

params reaches the upstream service unchanged, so an ArcGIS source accepts geometry, geometryType, inSR, outFields, and the rest. One connection can serve any area, rather than needing a source per region:

curl -G https://app.flitch.io/api/v1/connections/$ID/query \
  -H "Authorization: Bearer flk_live_..." \
  --data-urlencode 'format=geojson' \
  --data-urlencode 'params={"geometry":"115.8,-31.9,115.9,-31.8","geometryType":"esriGeometryEnvelope","inSR":"4326"}'

Always check metadata.truncated. Upstream services cap a response at a fixed record count and flag it rather than failing, so a large area comes back looking complete when it is not. Narrow the area and query again.

Responses may also carry stale: true, meaning the upstream was throttled and the last good result for this query was served instead of a blank one.

Input tables

Input tables are the writable datasets. Reads and writes go through the same service the app uses, so rows and the edit log stay consistent whichever side made the change.

GET /tables/{id}/rows

Accepts limit and offset.

POST /tables/{id}/rows

The body is a JSON object of column values. Returns 201 with the stored row.

curl -X POST https://app.flitch.io/api/v1/tables/$TABLE_ID/rows \
  -H "Authorization: Bearer flk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5f2c1e90-job-1042-complete" \
  -d '{"job_id":"J-1042","status":"complete"}'

PATCH /tables/{id}/rows/{rowId}

The body is a JSON object of the columns to change. Returns the stored row so the client can reconcile.

Pass expectedUpdatedAt alongside the columns for compare-and-set. If the row changed since that timestamp the write is refused with 409, rather than overwriting someone else. Without it, concurrent writes are last-write-wins.

DELETE /tables/{id}/rows/{rowId}

Returns { "deleted": true }.

Column validation

Writes may only name columns the table declares. An unknown column returns 422 and names the offending field:

{
  "error": {
    "code": "validation_failed",
    "message": "Unknown column: statuss",
    "details": {
      "unknownColumns": ["statuss"],
      "declaredColumns": ["job_id", "address", "status"]
    }
  }
}

This catches a typo on the first call rather than letting a queued client accumulate rows with fields that should not exist. A table with no declared columns accepts any shape.

Attribution

Writes are recorded in the table's edit log against the credential that made them: the key's name for an API key, the user for a session token. They are never attributed to an anonymous system user.

On this page