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.
| Method | Path | Scope |
|---|---|---|
GET | /me | data:read |
GET | /datasets | data:read |
GET | /datasets/{id} | data:read |
GET | /datasets/{id}/rows | data:read |
GET | /connections/{id}/query | data:read |
GET | /tables/{id}/rows | data:read |
POST | /tables/{id}/rows | tables: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.
| Parameter | Description |
|---|---|
maxRows | Maximum rows to return. Omit for all. |
offset | Rows to skip. Defaults to 0. |
columns | Comma-separated list of columns to return. |
filters | URL-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.
| Parameter | Description |
|---|---|
limit, offset | Paging. |
filters | URL-encoded JSON object of exact matches. |
orderBy, orderDirection | Sort column, and ASC or DESC. |
datasetId | Which dataset in the connection to query. Required when the connection carries more than one. |
format | Set to geojson for spatial sources. |
params | URL-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.