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/dashboardsdashboards:read
GET/dashboards/{id}/viewsdashboards:read
GET/space/{object}dashboards: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
GET/auditaudit:read

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": [
        { "name": "job_id", "type": "string" },
        { "name": "status", "type": "string" }
      ],
      "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"
}

writable says whether the rows can be changed through /tables/{id}/rows. type is null on a column whose type was never inferred.

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.

Dashboards

Metadata and view counts. The generated code behind a dashboard is not exposed, and neither are raw analytics rows.

GET /dashboards

Every current dashboard in the space, newest first. A session token needs ?teamId=.

{
  "dashboards": [
    {
      "id": "…",
      "name": "Fleet overview",
      "description": null,
      "published": true,
      "publishedUrl": "https://view.flitch.io/fleet-overview",
      "version": 7,
      "createdAt": "2026-07-02T09:14:00.000Z",
      "updatedAt": "2026-08-11T22:03:11.000Z",
      "publishedAt": "2026-08-11T22:05:40.000Z"
    }
  ]
}

id is the dashboard's identity across versions, which is what the published URL and the views endpoint key off. Only the current version of each dashboard is listed, so the count matches what you see in the app.

GET /dashboards/{id}/views

View counts for a published dashboard. Accepts days (1 to 365, default 30) and by (day or viewer, default day).

{
  "dashboardId": "…",
  "name": "Fleet overview",
  "days": 30,
  "totals": { "views": 1284, "uniqueVisitorsByDay": 402 },
  "series": [
    { "day": "2026-08-10", "views": 61, "uniqueVisitors": 24 }
  ]
}

Editor sessions are excluded, so these are real viewers rather than your own work. uniqueVisitorsByDay sums the daily figures, so somebody returning on three days counts three times: it is the number a chart plots, not a distinct count across the whole window.

by=viewer

Adds who, one row per viewer per day:

{
  "by": "viewer",
  "series": [
    { "day": "2026-08-10", "viewerId": "…", "viewer": "alex@flitch.io", "views": 4 },
    { "day": "2026-08-10", "viewerId": null, "viewer": "Anonymous", "views": 17 }
  ]
}

People signed in to Flitch are named. Everyone arriving through a public link is one Anonymous row per day, because there is no person to name, only a browser cookie.

Aggregates only, at both grains. The rows behind them carry IP addresses, user agents, visitor ids and cities belonging to your viewers, and are never served. Naming a signed-in team member shows nothing the Dashboard Usage settings page does not already show you.

Your space

GET /space/{object}

What Flitch knows about the space itself, rather than about the data in it. Accepts days (1 to 365, default 90), and a session token needs ?teamId=.

ObjectAnswers
dashboard-usageWho is reading what you publish
refresh-historyWhich data refreshes are failing, and how long they take
credit-usageWhat your credits are being spent on
api-usageWhich endpoints your integrations call, and where they error
dataset-usageWhat you hold, how fresh it is, and what reads it

All aggregated. dataset-usage reports current state rather than history, so days does not apply to it.

curl "https://app.flitch.io/api/v1/space/refresh-history?days=30" \
  -H "Authorization: Bearer flk_live_..."
{
  "object": "refresh-history",
  "days": 30,
  "columns": [{ "name": "dataset", "type": "string" }],
  "rows": [
    {
      "dataset_id": "…",
      "dataset": "WA localities",
      "connection": "services.slip.wa.gov.au…",
      "day": "2026-08-13",
      "runs": 24,
      "failures": 2,
      "rows_processed": 9936,
      "avg_duration_ms": 1945,
      "last_error": "HTTP 429: Too Many Requests"
    }
  ]
}

Aggregated by day, never per run. The refresh log alone holds millions of rows, so a row-per-run response would be unusable as well as enormous, and the day grain answers "what is failing" better than the raw log does.

The same objects are available inside Flitch as the Flitch activity source in Add Source, reading the same service, so the two cannot disagree.

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.

Audit log

GET /audit

The business's audit log as a feed, for a SIEM to poll. Requires the audit:read scope, the Business plan, and a key whose creator is a business admin. It covers every team in the business, not only the space the key belongs to.

The download in Settings answers "give me a file for the auditor". This answers the other question: "keep my index current". Same events, different contract.

GET /api/v1/audit?cursor=MjAyNi0wOC0yOVQwNjoyODo0Ny4xNzJafGF1ZGl0Xw
{
  "events": [ ... ],
  "cursor": "MjAyNi0wOC0yOVQwNjoyODo0Ny4xNzJafGF1ZGl0Xw",
  "hasMore": false,
  "lagSeconds": 5
}
ParameterDefaultMeaning
cursornoneWhere the last read finished. Omit to start at the beginning.
limit200Up to 1000.
formatocsfocsf for OCSF Application Activity (class 6003), or raw for the stored shape.
categoryaudit eventsRepeatable. Add data_access or error to include data reads and dashboard errors.
sincenoneISO 8601. Honoured only on a first read, since after that the cursor is the position.
teamIdthe key's spaceRequired for a session token, which does not carry one.

Send cursor back on the next call and you get the next events, exactly once each. Keep polling while hasMore is true, then poll on your own cadence. An empty page returns the cursor you sent, so a reader that has caught up does not restart.

The feed is deliberately a few seconds behind the present, reported as lagSeconds. An event carries the timestamp of the process that wrote it, so a row can land with a timestamp earlier than one already returned; reading up to a horizon rather than to the clock is what stops the cursor stepping over it.

Polling is not itself written to the audit log. A minute's cadence would add over a thousand rows a day and bury the events the log exists to hold. Who polled, and how often, is in the API key usage in Settings.

On this page