Skip to main content
POST
/
v0
/
query
curl --request POST \
  --url https://api.formo.so/v0/query \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "query": "SELECT toDate(timestamp) as date, uniq(anonymous_id) as dau FROM events WHERE timestamp >= now() - interval 30 day GROUP BY date ORDER BY date"
}
'
{
  "data": [
    {
      "day": "2026-04-21",
      "users": 1284
    },
    {
      "day": "2026-04-22",
      "users": 1352
    },
    {
      "day": "2026-04-23",
      "users": 1411
    }
  ],
  "total": 7,
  "limit": 100,
  "offset": 0,
  "has_more": false
}

Documentation Index

Fetch the complete documentation index at: https://docs.formo.so/llms.txt

Use this file to discover all available pages before exploring further.

Run a read-only SQL query to read your data. See examples on the Explorer page.
Query API architecture diagram showing your service sending SQL queries to the Query API and receiving data from the Data Warehouse

Authentication

Use a Workspace API Key with query:read permission. Send it in the Authorization header:
Authorization: Bearer <your_workspace_api_key>

Request

  • Method: POST
  • Path: /v0/query
  • Body:
{
  "query": "SELECT * FROM events LIMIT 100"
}
Limits:
  • Query must be a single SELECT/WITH statement (read-only).
  • No comments or multiple statements.
  • LIMIT is required and must be <= 1000.

Response

200 OK returns rows plus pagination metadata reflecting the LIMIT / OFFSET you wrote into the SQL.
{
  "data": [
    {
      "event": "page",
      "address": "0x123...",
      "timestamp": "2025-01-20T10:00:00Z"
    }
  ],
  "total": 120,
  "limit": 100,
  "offset": 0,
  "has_more": true
}
FieldTypeDescription
dataarrayResult rows
totalintegerTotal rows before LIMIT was applied
limitintegerLIMIT echoed from the SQL
offsetintegerOFFSET echoed from the SQL
has_morebooleantrue if total > offset + data.length
This endpoint uses limit/offset (not page/size) because the client controls pagination directly through the SQL query - Formo just echoes the values back.

Errors

Branch on error.code; see Errors for the full reference.
  • 400 BAD_REQUEST: missing query, invalid SQL, LIMIT over 1000, or missing project id.
  • 401 UNAUTHORIZED: missing/invalid authorization header or API key.
  • 403 FORBIDDEN: API key lacks query:read scope.
  • 404 NOT_FOUND: project or read token not found.
  • 429 TOO_MANY_REQUESTS: per-workspace rate limit exceeded.
  • 500 INTERNAL_SERVER_ERROR: failure executing the query.

Authorizations

Authorization
string
header
required

Workspace API key (e.g. formo_xxx). Create one in the Formo dashboard under Team Settings > API Keys.

Body

application/json
query
string
required

SQL query to execute

Response

Offset-paginated query results. The server doesn't own pagination here - LIMIT and OFFSET come from your SQL string and are echoed back. total is the row count before LIMIT was applied; has_more is true when there are additional rows beyond the current window.

data
object[]
required

Result rows.

total
integer
required

Total rows before LIMIT was applied.

limit
integer
required

Applied LIMIT (parsed from your SQL; defaults to the server cap if absent).

offset
integer
required

Applied OFFSET (parsed from your SQL; 0 if absent).

has_more
boolean
required

True when offset + data.length < total - i.e. there's another page to fetch by re-running with a higher OFFSET.