> ## 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.

# Search Profiles

> Search and filter wallet profiles by address, socials, labels, net worth, chain activity, and more. Returns paginated results with full profile data.

Search and filter wallet profiles across your project with advanced filtering capabilities. You can look up an exact wallet address, run a free-text search across addresses and social fields, and combine that with structured filters in the request body.

## Authentication

This endpoint requires authentication using a **Workspace API Key** with `profiles:read` permission. Include the API key in your request headers:

```bash theme={null}
Authorization: Bearer <your_workspace_api_key>
```

<Note>
  The API key needs to have the **read permission for profiles** in the API settings. You can configure this in your workspace API settings.
</Note>

## Query Parameters

### `address`

Filter by a specific wallet address (exact match). Accepts an EVM address, a Solana address, or an ENS name (e.g. `vitalik.eth`) which is resolved to an address before filtering. An unresolvable ENS name or an otherwise invalid value returns a `400 BAD_REQUEST`.

```bash theme={null}
curl -sS -X GET \
  -H "Authorization: Bearer <your_workspace_api_key>" \
  "https://api.formo.so/v0/profiles?address=0x1234..."

# ENS names are resolved automatically
curl -sS -X GET \
  -H "Authorization: Bearer <your_workspace_api_key>" \
  "https://api.formo.so/v0/profiles?address=vitalik.eth"
```

### `search`

Case-insensitive free-text search across wallet addresses and all supported social fields. This includes values from both global wallet profiles and project-scoped identify overrides.

```bash theme={null}
curl -sS -X GET \
  -H "Authorization: Bearer <your_workspace_api_key>" \
  "https://api.formo.so/v0/profiles?search=bob_override"
```

### `expand`

Comma-separated list of optional sections to include in each profile response.

Supported values:

* `apps` - DeFi app interactions and balances
* `chains` - Per-chain activity metrics
* `tokens` - Token holdings
* `labels` - Wallet labels

```bash theme={null}
curl -sS -X GET \
  -H "Authorization: Bearer <your_workspace_api_key>" \
  "https://api.formo.so/v0/profiles?expand=apps,chains,labels"
```

<Note>
  Expanding fields like `chains`, `tokens`, or `apps` increases response size and latency. The collections are capped at **50 items** each.
</Note>

### `order_by`

Field to sort results by. Supported values:

* `last_onchain` (default) - Last on-chain activity timestamp
* `first_onchain` - First on-chain activity timestamp
* `net_worth_usd` - Total net worth
* `updated_at` - Last profile update timestamp
* `tx_count` - Total transaction count
* `first_seen` - First seen timestamp in your app
* `last_seen` - Last seen timestamp in your app
* `num_sessions` - Number of sessions
* `revenue` - Total revenue
* `volume` - Total volume
* `points` - Total points

### `order_dir`

Sort direction:

* `desc` (default) - Descending order
* `asc` - Ascending order

### `page`

1-indexed page number to return.

* Default: `1`

### `size`

Page size - number of profiles per page.

* Default: `100`
* Maximum: `1000`

## Request Body (Filters)

The search endpoint supports rich filtering capabilities through a JSON request body. The body contains a filter object with `filters` and `logic`.

### Filter Schema

```json theme={null}
{
  "filters": [
    {
      "field": "users.net_worth_usd",
      "op": "gt",
      "value": 1000
    }
  ],
  "logic": "and"
}
```

| Field | Type | Description |
| - | - | - |
| `filters` | array | Array of filter conditions |
| `logic` | string | How to combine conditions: `and` (all must match) or `or` (any must match). Default: `and` |

### Filter Condition

Each filter in the `filters` array carries the canonical `{field, op, value}` core, plus named **qualifier** properties for resource filters:

| Field | Type | Description |
| - | - | - |
| `field` | string | The stable field path to filter on (see Field Reference below). User/profile/social fields use `users.{attribute}`. Resource metrics use the fixed paths `chains.balance`, `apps.balance`, `tokens.balance`, or `labels.value`. Identifiers belong in the qualifier properties below and must **not** be embedded in the field path. |
| `op` | string | The comparison operator |
| `value` | string \| number \| boolean \| array | The value to compare against. Balance fields (`chains.balance`, `apps.balance`, `tokens.balance`) require a JSON number; numeric strings and empty strings are rejected. `labels.value` rejects an empty string. |
| `chain_id` | string | (Optional) Chain qualifier for `chains.balance`, `apps.balance`, `tokens.balance`, and `labels.value`. Omit to compare across all chains. |
| `app_id` | string | Required for `apps.balance`, and for `tokens.balance` with `scope: "protocol"` (e.g., `aave-v3`, `compound-v3`). |
| `token_address` | string | Required token address for `tokens.balance`. |
| `scope` | string | Required for `tokens.balance`: `any` (wallet + all protocols) or `protocol` (a specific protocol; requires `app_id`). |
| `tag_id` | string | Required label tag for `labels.value` (e.g., `coinbase.verified_account`). |

Unknown properties and qualifiers that don't apply to the targeted `field` are rejected with a `400`.

### Operators

| Operator | Description | Example |
| - | - | - |
| `eq` | Equals | `{"field": "users.device", "op": "eq", "value": "Desktop"}` |
| `neq` | Not equals | `{"field": "users.os", "op": "neq", "value": "Windows"}` |
| `gt` | Greater than | `{"field": "users.net_worth_usd", "op": "gt", "value": 1000}` |
| `gte` | Greater than or equal | `{"field": "users.volume", "op": "gte", "value": 100}` |
| `lt` | Less than | `{"field": "users.net_worth_usd", "op": "lt", "value": 50000}` |
| `lte` | Less than or equal | `{"field": "chains.balance", "op": "lte", "value": 10000, "chain_id": "1"}` |
| `in` | In array | `{"field": "users.lifecycle", "op": "in", "value": ["New", "Power user"]}` |
| `nin` | Not in array | `{"field": "users.location", "op": "nin", "value": ["US", "UK"]}` |
| `contains` | Case-insensitive substring match for social fields and `labels.value` | `{"field": "users.twitter", "op": "contains", "value": "vitalik"}` |
| `notEmpty` | Field is set (non-empty). String fields only; value is ignored | `{"field": "users.email", "op": "notEmpty"}` |
| `isEmpty` | Field is not set (empty). String user attributes only; value is ignored | `{"field": "users.utm_source", "op": "isEmpty"}` |

Behavioural labels such as `defi.lender`, `defi.borrower`, `defi.staker` and `defi.perp_trader` store a USD amount as their value, so the comparison operators work on them. For example, wallets that supply at least \$10,000 to lending markets: `{"field": "labels.value", "op": "gte", "value": "10000", "tag_id": "defi.lender"}`. See [Wallet labels](/features/wallet-intelligence/wallet-labels#behavioural-labels) for the full list and each label's rule.

<Note>
  **Legacy spellings are retired.** The long-form spellings `equals`, `notEquals`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `notIn`, and `includes` (and symbol forms like `=` / `!=` / `like`) are no longer accepted on this endpoint. A request carrying one is rejected with a `400` whose message names the offending token. Send only the canonical operators above. `notEmpty` / `isEmpty` are value-less existence checks (any `value` is ignored) and are supported on string user/profile attributes only, not on numeric metrics (`net_worth_usd`, `volume`, `revenue`, `points`), the `lifecycle` enum, or chain/app/token/label fields.
</Note>

<Note>
  **Dynamic field paths are retired.** Identifier-in-path spellings such as `chains.1.balance`, `apps.aave-v3.balance`, `tokens.0x….balance`, and `labels.coinbase.verified_account` are no longer accepted and return a `400`. Use the stable field (`chains.balance`, `apps.balance`, `tokens.balance`, `labels.value`) with the matching qualifier property (`chain_id`, `app_id`, `token_address`, `tag_id`) instead. Balance fields support only the comparison operators (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`); `labels.value` supports comparisons plus case-insensitive `contains`.
</Note>

***

## Field Reference

### User Fields (`users.*`)

Filter by user engagement and profile data. Use the format `users.{attribute}`.

#### Profile Fields

| Field | Type | Description |
| - | - | - |
| `users.net_worth_usd` | number | Total net worth in USD |
| `users.volume` | number | Total trading volume |
| `users.revenue` | number | Total revenue |
| `users.points` | number | Total points |

**Example - Find users with net worth > \$10,000:**

```json theme={null}
{
  "filters": [
    { "field": "users.net_worth_usd", "op": "gt", "value": 10000 }
  ],
  "logic": "and"
}
```

**Example - Find high-volume users:**

```json theme={null}
{
  "filters": [
    { "field": "users.volume", "op": "gt", "value": 5000 }
  ],
  "logic": "and"
}
```

#### Engagement Fields

| Field | Type | Description |
| - | - | - |
| `users.device` | string | Device type (e.g., `Desktop`, `Mobile`) |
| `users.browser` | string | Browser name (e.g., `Chrome`, `Safari`) |
| `users.os` | string | Operating system (e.g., `macOS`, `Windows`) |
| `users.location` | string | Country code (e.g., `US`, `NG`) |

**Example - Find mobile users from the US:**

```json theme={null}
{
  "filters": [
    { "field": "users.device", "op": "eq", "value": "Mobile" },
    { "field": "users.location", "op": "eq", "value": "US" }
  ],
  "logic": "and"
}
```

#### UTM & Referral Fields

| Field | Type | Description |
| - | - | - |
| `users.first_utm_source` | string | First UTM source |
| `users.last_utm_source` | string | Last UTM source |
| `users.first_utm_medium` | string | First UTM medium |
| `users.last_utm_medium` | string | Last UTM medium |
| `users.first_utm_campaign` | string | First UTM campaign |
| `users.last_utm_campaign` | string | Last UTM campaign |
| `users.first_utm_content` | string | First UTM content |
| `users.last_utm_content` | string | Last UTM content |
| `users.first_utm_term` | string | First UTM term |
| `users.last_utm_term` | string | Last UTM term |
| `users.first_referrer` | string | First referrer domain |
| `users.last_referrer` | string | Last referrer domain |
| `users.first_referrer_url` | string | First referrer full URL |
| `users.last_referrer_url` | string | Last referrer full URL |
| `users.first_ref` | string | First referral code |
| `users.last_ref` | string | Last referral code |

**Example - Find users from Google Ads campaign:**

```json theme={null}
{
  "filters": [
    { "field": "users.first_utm_source", "op": "eq", "value": "google" },
    { "field": "users.first_utm_medium", "op": "eq", "value": "cpc" }
  ],
  "logic": "and"
}
```

#### Lifecycle Filter

Filter by user lifecycle stage. Valid values: `New`, `Returning`, `Power user`, `At Risk`, `Churned`, `Resurrected`.

| Field | Type | Description |
| - | - | - |
| `users.lifecycle` | string | User lifecycle stage |

**Example - Find new and power users:**

```json theme={null}
{
  "filters": [
    { "field": "users.lifecycle", "op": "in", "value": ["New", "Power user"] }
  ],
  "logic": "and"
}
```

#### Social Fields

Social fields support two modes:

* Presence checks: use `notEmpty` to match profiles where the field is set (or `isEmpty` for the opposite). The value is ignored.
* Value matching: use `eq`, `neq`, or `contains` (case-insensitive substring) with a non-empty string to match the actual social value.

| Field | Description |
| - | - |
| `users.ens` | ENS name |
| `users.farcaster` | Farcaster username |
| `users.lens` | Lens handle |
| `users.basenames` | Base names |
| `users.linea` | Linea identifier |
| `users.discord` | Discord username |
| `users.telegram` | Telegram username |
| `users.website` | Website URL |
| `users.twitter` | Twitter/X handle |
| `users.github` | GitHub username |
| `users.linkedin` | LinkedIn profile |
| `users.email` | Email address |
| `users.instagram` | Instagram handle |
| `users.facebook` | Facebook profile |
| `users.tiktok` | TikTok handle |
| `users.youtube` | YouTube channel or handle |
| `users.reddit` | Reddit username |

**Example - Find users with any email present:**

```json theme={null}
{
  "filters": [
    { "field": "users.email", "op": "notEmpty" }
  ],
  "logic": "and"
}
```

**Example - Find users with both ENS and Farcaster present:**

```json theme={null}
{
  "filters": [
    { "field": "users.ens", "op": "notEmpty" },
    { "field": "users.farcaster", "op": "notEmpty" }
  ],
  "logic": "and"
}
```

**Example - Find users whose Twitter contains `bob`:**

```json theme={null}
{
  "filters": [
    { "field": "users.twitter", "op": "contains", "value": "bob" }
  ],
  "logic": "and"
}
```

**Example - Find an exact email match:**

```json theme={null}
{
  "filters": [
    { "field": "users.email", "op": "eq", "value": "alice@formo.so" }
  ],
  "logic": "and"
}
```

***

### Chain Filters (`chains.balance`)

Filter by per-chain net worth using the stable field `chains.balance` and an optional `chain_id` qualifier. This is distinct from `users.net_worth_usd`, which compares the profile's **total** net worth.

#### All Chains

Omit `chain_id` to filter across all chains (returns profiles where **any** chain matches).

**Example - Find users with >\$1,000 on any chain:**

```json theme={null}
{
  "filters": [
    { "field": "chains.balance", "op": "gt", "value": 1000 }
  ],
  "logic": "and"
}
```

#### Specific Chain

Set the `chain_id` qualifier to filter on a specific chain.

Common chain IDs:

* `1` - Ethereum Mainnet
* `137` - Polygon
* `42161` - Arbitrum One
* `10` - Optimism
* `8453` - Base
* `56` - BNB Chain
* `43114` - Avalanche

**Example - Find users with >\$5,000 on Ethereum:**

```json theme={null}
{
  "filters": [
    { "field": "chains.balance", "op": "gt", "value": 5000, "chain_id": "1" }
  ],
  "logic": "and"
}
```

**Example - Find users with >\$1,000 on both Ethereum and Polygon:**

```json theme={null}
{
  "filters": [
    { "field": "chains.balance", "op": "gt", "value": 1000, "chain_id": "1" },
    { "field": "chains.balance", "op": "gt", "value": 1000, "chain_id": "137" }
  ],
  "logic": "and"
}
```

***

### App Filters (`apps.balance`)

Filter by DeFi app balances using the stable field `apps.balance` with a required `app_id` qualifier and an optional `chain_id` qualifier.

#### All Chains

Omit `chain_id` to filter by app balance across all chains.

**Example - Find users with >\$1,000 in Uniswap:**

```json theme={null}
{
  "filters": [
    { "field": "apps.balance", "op": "gt", "value": 1000, "app_id": "uniswap" }
  ],
  "logic": "and"
}
```

#### Specific Chain

Set the `chain_id` qualifier to filter by app balance on a specific chain.

**Example - Find users with >\$500 in Aave on Ethereum:**

```json theme={null}
{
  "filters": [
    { "field": "apps.balance", "op": "gt", "value": 500, "app_id": "aave", "chain_id": "1" }
  ],
  "logic": "and"
}
```

***

### Token Filters (`tokens.balance`)

Filter by token holdings using the stable field `tokens.balance` with required `token_address` and `scope` qualifiers, plus an optional `chain_id` and, for protocol scope, a required `app_id`.

#### Token Filter Qualifiers

| Qualifier | Type | Description |
| - | - | - |
| `token_address` | string | Required. The token's contract address. |
| `scope` | string | Required. `any` = wallet + all protocols, `protocol` = a specific protocol only (requires `app_id`) |
| `app_id` | string | Required for `scope: "protocol"`. The DeFi protocol ID (e.g., `aave-v3`, `compound-v3`, `uniswap-v3`) |
| `chain_id` | string | Optional. Omit to compare across all chains. |

#### All Chains

Omit `chain_id` to filter by token balance across all chains.

**Example - Find users holding >1000 USDC (any chain):**

```json theme={null}
{
  "filters": [
    {
      "field": "tokens.balance",
      "op": "gt",
      "value": 1000,
      "token_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "scope": "any"
    }
  ],
  "logic": "and"
}
```

#### Specific Chain

Set the `chain_id` qualifier to filter by token balance on a specific chain.

**Example - Find users with >500 USDC on Ethereum:**

```json theme={null}
{
  "filters": [
    {
      "field": "tokens.balance",
      "op": "gt",
      "value": 500,
      "token_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "scope": "any",
      "chain_id": "1"
    }
  ],
  "logic": "and"
}
```

#### Protocol-Specific Token Filters

Use `scope: "protocol"` with `app_id` to filter for tokens deposited in a specific DeFi protocol.

**Example - Find users with USDC deposited in Aave V3:**

```json theme={null}
{
  "filters": [
    {
      "field": "tokens.balance",
      "op": "gt",
      "value": 1000,
      "token_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "scope": "protocol",
      "app_id": "aave-v3"
    }
  ],
  "logic": "and"
}
```

**Example - Find users with ETH staked in Lido:**

```json theme={null}
{
  "filters": [
    {
      "field": "tokens.balance",
      "op": "gt",
      "value": 1,
      "token_address": "0x0000000000000000000000000000000000000000",
      "scope": "protocol",
      "app_id": "lido"
    }
  ],
  "logic": "and"
}
```

***

### Label Filters (`labels.value`)

Filter by wallet labels using the stable field `labels.value` with a required `tag_id` qualifier (and an optional `chain_id`). Supported operators are the comparisons (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`) and case-insensitive `contains`; the `value` must be non-empty.

Common label tags:

* `coinbase.verified_account` - Coinbase verified account (boolean)
* `coinbase.verified_country` - Coinbase verified country code
* `coinbase.verified_coinbase_one` - Coinbase One membership (boolean)
* `sanctions.designated` - Sanctioned address (boolean)
* `passport.models_aggregate_score` - Passport aggregate score (0-100)
* `passport.unique_humanity_score` - Passport uniqueness score

**Example - Find Coinbase verified users:**

```json theme={null}
{
  "filters": [
    { "field": "labels.value", "op": "eq", "value": "true", "tag_id": "coinbase.verified_account" }
  ],
  "logic": "and"
}
```

**Example - Find US-verified Coinbase users:**

```json theme={null}
{
  "filters": [
    { "field": "labels.value", "op": "eq", "value": "US", "tag_id": "coinbase.verified_country" }
  ],
  "logic": "and"
}
```

**Example - Find users with high Passport score:**

```json theme={null}
{
  "filters": [
    { "field": "labels.value", "op": "gte", "value": 50, "tag_id": "passport.models_aggregate_score" }
  ],
  "logic": "and"
}
```

***

## Combined Filter Examples

### High-Value Web3 Users

Find users with >\$10,000 net worth, ENS name, and activity on Ethereum:

```bash theme={null}
curl -sS -X GET \
  -H "Authorization: Bearer <your_workspace_api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "filters": [
      { "field": "users.net_worth_usd", "op": "gt", "value": 10000 },
      { "field": "users.ens", "op": "notEmpty" },
      { "field": "chains.balance", "op": "gt", "value": 0, "chain_id": "1" }
    ],
    "logic": "and"
  }' \
  "https://api.formo.so/v0/profiles?expand=chains,labels"
```

### Active DeFi Users

Find users with activity in Uniswap or Aave:

```bash theme={null}
curl -sS -X GET \
  -H "Authorization: Bearer <your_workspace_api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "filters": [
      { "field": "apps.balance", "op": "gt", "value": 0, "app_id": "uniswap" },
      { "field": "apps.balance", "op": "gt", "value": 0, "app_id": "aave" }
    ],
    "logic": "or"
  }' \
  "https://api.formo.so/v0/profiles?expand=apps"
```

### Verified Power Users

Find Coinbase-verified power users from specific countries:

```bash theme={null}
curl -sS -X GET \
  -H "Authorization: Bearer <your_workspace_api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "filters": [
      { "field": "labels.value", "op": "eq", "value": "true", "tag_id": "coinbase.verified_account" },
      { "field": "labels.value", "op": "eq", "value": "US", "tag_id": "coinbase.verified_country" },
      { "field": "users.lifecycle", "op": "eq", "value": "Power user" }
    ],
    "logic": "and"
  }' \
  "https://api.formo.so/v0/profiles"
```

### Multi-Chain Whales

Find users with significant holdings across multiple chains:

```bash theme={null}
curl -sS -X GET \
  -H "Authorization: Bearer <your_workspace_api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "filters": [
      { "field": "chains.balance", "op": "gt", "value": 10000, "chain_id": "1" },
      { "field": "chains.balance", "op": "gt", "value": 5000, "chain_id": "137" },
      { "field": "chains.balance", "op": "gt", "value": 5000, "chain_id": "42161" }
    ],
    "logic": "and"
  }' \
  "https://api.formo.so/v0/profiles?expand=chains&order_by=net_worth_usd&order_dir=desc"
```

***

## Response

The response is a [paginated envelope](/api/overview#pagination) of wallet profiles.

```json theme={null}
{
  "data": [
    {
      "address": "0x9CC3cB28cd94eB4423B15cdA73346e204f59a407",
      "net_worth_usd": 125000.5,
      "ens": "example.eth",
      "tx_count": 1234,
      "first_onchain": "2024-01-15T10:30:00Z",
      "last_onchain": "2025-01-20T14:22:00Z",
      "lifecycle": "Returning"
    }
  ],
  "total": 150,
  "page": 1,
  "size": 100,
  "has_more": true
}
```

### Response Fields

| Field | Type | Description |
| - | - | - |
| `data` | array | Array of wallet profiles matching the search filters |
| `total` | integer | Total number of profiles matching the filters across all pages |
| `page` | integer | 1-indexed page number echoed from the request |
| `size` | integer | Page size echoed from the request |
| `has_more` | boolean | `true` if there are pages after the current one |

### Profile Fields

Each profile in the `data` array includes:

| Field | Type | Description |
| - | - | - |
| `address` | string | Wallet address |
| `net_worth_usd` | number | Total net worth in USD |
| `tx_count` | integer | Total transaction count |
| `first_onchain` | string | First on-chain activity timestamp (ISO 8601) |
| `last_onchain` | string | Last on-chain activity timestamp (ISO 8601) |
| `lifecycle` | string | User lifecycle stage: `New`, `Returning`, `Power user`, `At Risk`, `Churned`, `Resurrected` |
| `ens` | string \| null | ENS name |
| `farcaster` | string \| null | Farcaster username |
| `twitter` | string \| null | Twitter/X handle |
| `discord` | string \| null | Discord username |
| `telegram` | string \| null | Telegram username |
| `email` | string \| null | Email address |
| `first_seen` | string \| null | First seen timestamp in your app |
| `last_seen` | string \| null | Last seen timestamp in your app |
| `num_sessions` | integer \| null | Total number of sessions |
| `revenue` | number \| null | Total revenue |
| `volume` | number \| null | Total volume |
| `points` | number \| null | Total points |
| `chains` | array \| null | Per-chain data (when expanded) |
| `apps` | array \| null | DeFi app data (when expanded) |
| `tokens` | array \| null | Token holdings (when expanded) |
| `labels` | array \| null | Wallet labels (when expanded) |

## Error Responses

| Status | Code | Description |
| - | - | - |
| 400 | `BAD_REQUEST` | Invalid query parameters or malformed JSON in request body |
| 401 | `UNAUTHORIZED` | Missing or invalid API key |
| 403 | `FORBIDDEN` | API key does not have `profiles:read` permission |
| 500 | `INTERNAL_SERVER_ERROR` | Failed to fetch wallet profile data |


## OpenAPI

````yaml GET /v0/profiles
openapi: 3.1.0
info:
  title: Formo Public API
  description: >-
    REST API for managing Formo projects, analytics, alerts, boards, charts,
    contracts, segments, and AI chat.


    **Auth.** Every endpoint on `api.formo.so` requires a workspace API key with
    the appropriate scopes (see `x-api-scopes`). The one exception is `POST
    /v0/raw_events`, which runs on `events.formo.so` and authenticates with the
    project SDK write key instead.


    **Response shape.** Successful responses return the resource directly (or `{
    data: [...], total, page, size, has_more }` for paginated lists). HTTP
    status carries success/failure; there is no envelope wrapping success
    bodies.


    **Errors.** Non-2xx responses from `api.formo.so` use the `Error` envelope,
    with two exceptions: some rate-limit rejections reply in plain text, and
    `POST /v0/raw_events` returns a plain `{ "error": "..." }` object. Treat the
    HTTP status as authoritative and parse defensively: `{ error: { code,
    message, doc_url, param?, details? } }`. Branch on the machine-readable
    `code` (see `ErrorCode` enum) and follow `doc_url` to the matching section
    of the [errors reference](https://docs.formo.so/api/errors).


    **Idempotency.** Pass an `Idempotency-Key` header on write requests to
    alerts, boards, charts, contracts, segments, and import to make retries
    safe; the response is cached for 24 h and replayed on duplicate keys.
    Profile writes and `POST /v0/query` do not support it.
  version: 0.1.0
  contact:
    name: Formo
    url: https://formo.so
servers:
  - url: https://api.formo.so
    description: API Server (boards, alerts, contracts, segments, profiles, query, import)
  - url: https://events.formo.so
    description: Events Server (event ingestion)
security:
  - WorkspaceApiKey: []
tags:
  - name: Alerts
    description: Manage project alerts and notifications
  - name: Boards
    description: Manage dashboard boards
  - name: Charts
    description: Manage charts within boards
  - name: Contracts
    description: Manage blockchain contract monitoring
  - name: Segments
    description: Manage user segments
  - name: Profiles
    description: Wallet profiles and import
  - name: Query
    description: >-
      Execute SQL queries and call pre-built analytics endpoints (KPIs, top
      pages, lifecycle, retention, revenue). Requires the query:read scope.
  - name: Events
    description: Event ingestion API (events.formo.so)
paths:
  /v0/profiles:
    get:
      tags:
        - Profiles
      summary: Search wallet profiles
      operationId: searchProfiles
      parameters:
        - name: address
          in: query
          schema:
            type: string
          description: >-
            Filter by wallet address. Accepts an EVM (0x...) or Solana address,
            or an ENS name (e.g. vitalik.eth) which is resolved to an address.
        - name: expand
          in: query
          schema:
            type: string
          description: 'Comma-separated: apps, chains, tokens, labels'
        - name: timestamp
          in: query
          schema:
            type: string
            format: date-time
          description: >-
            Optional point in time for wallet-enrichment data. Requires the
            address query parameter; unbounded historical collection scans are
            rejected. Returns the wallet's stored base snapshot closest to this
            instant instead of its latest snapshot; an exact-distance tie
            chooses the later snapshot. Expanded chains, apps, and tokens come
            from the selected snapshot's profiling batch. Project engagement
            fields, project-defined identity overrides, and labels remain
            current. Must be ISO-8601 with a timezone.
        - name: order_by
          in: query
          schema:
            type: string
            enum:
              - net_worth_usd
              - tx_count
              - first_onchain
              - last_onchain
              - updated_at
              - first_seen
              - last_seen
              - num_sessions
              - revenue
              - volume
              - points
          description: Sort field
        - name: order_dir
          in: query
          schema:
            type: string
            enum:
              - asc
              - desc
          description: Sort direction
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
          description: 1-indexed page number (default 1).
        - name: size
          in: query
          schema:
            type: integer
            default: 100
            minimum: 1
            maximum: 1000
          description: Page size (default 100, max 1000).
      requestBody:
        required: false
        description: Optional filter conditions
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProfileFilter'
            examples:
              highNetWorth:
                summary: Users with >$10k net worth
                value:
                  logic: and
                  filters:
                    - field: users.net_worth_usd
                      op: gt
                      value: 10000
              chainFilter:
                summary: Users active on Ethereum with >$1k balance
                value:
                  logic: and
                  filters:
                    - field: chains.balance
                      op: gt
                      value: 1000
                      chain_id: '1'
              labelFilter:
                summary: Coinbase verified users
                value:
                  logic: and
                  filters:
                    - field: labels.value
                      op: eq
                      value: 'true'
                      tag_id: coinbase.verified_account
              tokenFilter:
                summary: Users holding USDC in any protocol
                value:
                  logic: and
                  filters:
                    - field: tokens.balance
                      op: gt
                      value: 0
                      scope: any
                      token_address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
      responses:
        '200':
          description: Paginated wallet profile search results.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedListMeta'
                  - type: object
                    required:
                      - data
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Profile'
              example:
                data:
                  - address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
                    net_worth_usd: 12345.67
                page: 1
                size: 100
                total: 1
                has_more: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'
components:
  schemas:
    ProfileFilter:
      type: object
      description: >-
        A profile-search filter group. `filters` contains canonical `{field, op,
        value}` leaves and `logic` combines them at this level.
      properties:
        logic:
          type: string
          enum:
            - and
            - or
          default: and
        filters:
          type: array
          items:
            $ref: '#/components/schemas/FilterCondition'
      required:
        - filters
    PaginatedListMeta:
      type: object
      description: >-
        Pagination cursor returned alongside `data` on every paginated list
        endpoint. Use these to walk pages: `has_more` is true while `page * size
        < total`. Combine with the matching `Page` and `Size` query parameters
        to request the next page.
      required:
        - page
        - size
        - total
        - has_more
      properties:
        page:
          type: integer
          description: 1-indexed page number echoed from the request.
        size:
          type: integer
          description: Page size echoed from the request.
        total:
          type: integer
          description: Total row count across all pages.
        has_more:
          type: boolean
          description: True when more pages remain (`page * size < total`).
    Profile:
      type: object
      description: Comprehensive wallet profile with onchain and offchain data
      properties:
        address:
          type: string
        net_worth_usd:
          type: number
        tx_count:
          type: integer
        first_onchain:
          type: string
          description: First onchain activity date
        last_onchain:
          type: string
          description: Last onchain activity date
        updated_at:
          type: string
          format: date-time
        ens:
          type: string
          nullable: true
        farcaster:
          type: string
          nullable: true
        lens:
          type: string
          nullable: true
        basenames:
          type: string
          nullable: true
        linea:
          type: string
          nullable: true
        avatar:
          type: string
          nullable: true
        display_name:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        discord:
          type: string
          nullable: true
        telegram:
          type: string
          nullable: true
        twitter:
          type: string
          nullable: true
        github:
          type: string
          nullable: true
        linkedin:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
        instagram:
          type: string
          nullable: true
        facebook:
          type: string
          nullable: true
        website:
          type: string
          nullable: true
        reddit:
          type: string
          nullable: true
        youtube:
          type: string
          nullable: true
        tiktok:
          type: string
          nullable: true
        first_seen:
          type: string
          nullable: true
          description: First seen in project
        last_seen:
          type: string
          nullable: true
          description: Last seen in project
        lifecycle:
          type: string
          nullable: true
          enum:
            - New
            - Returning
            - Power user
            - At Risk
            - Churned
            - Resurrected
        num_sessions:
          type: integer
          nullable: true
        revenue:
          type: number
          nullable: true
        volume:
          type: number
          nullable: true
        points:
          type: number
          nullable: true
        device:
          type: string
          nullable: true
        browser:
          type: string
          nullable: true
        os:
          type: string
          nullable: true
        location:
          type: string
          nullable: true
        first_utm_source:
          type: string
          nullable: true
        first_utm_medium:
          type: string
          nullable: true
        first_utm_campaign:
          type: string
          nullable: true
        first_referrer:
          type: string
          nullable: true
        last_utm_source:
          type: string
          nullable: true
        last_utm_medium:
          type: string
          nullable: true
        last_utm_campaign:
          type: string
          nullable: true
        last_referrer:
          type: string
          nullable: true
        first_referrer_url:
          type: string
          nullable: true
          description: First referrer full URL
        last_referrer_url:
          type: string
          nullable: true
          description: Last referrer full URL
        first_ref:
          type: string
          nullable: true
          description: First referral code
        last_ref:
          type: string
          nullable: true
          description: Last referral code
        first_utm_content:
          type: string
          nullable: true
          description: First UTM content
        last_utm_content:
          type: string
          nullable: true
          description: Last UTM content
        first_utm_term:
          type: string
          nullable: true
          description: First UTM term
        last_utm_term:
          type: string
          nullable: true
          description: Last UTM term
        first_paid_source:
          type: string
          nullable: true
          description: >-
            First-touch acquiring ad network (google, meta, microsoft, tiktok,
            twitter, linkedin, reddit, or paid_utm for pricing-medium UTMs with
            no click ID)
        last_paid_source:
          type: string
          nullable: true
          description: >-
            Last-touch acquiring ad network (same value set as
            first_paid_source)
        first_click_id:
          type: string
          nullable: true
          description: >-
            First-touch raw ad-platform click ID (gclid, fbclid, msclkid,
            ttclid, twclid, li_fat_id, or rdt_cid)
        last_click_id:
          type: string
          nullable: true
          description: Last-touch raw ad-platform click ID
        last_type:
          type: string
          nullable: true
          description: Last event type
        last_event:
          type: string
          nullable: true
          description: Last event name
        last_properties:
          type: string
          nullable: true
          description: Last event properties (JSON string)
        activity_dates:
          type: array
          items:
            type: string
            format: date
          nullable: true
          description: Array of activity dates (YYYY-MM-DD format)
        chains:
          type: array
          items:
            $ref: '#/components/schemas/WalletChain'
          description: Requires expand=chains
        apps:
          type: array
          items:
            $ref: '#/components/schemas/WalletApp'
          description: Requires expand=apps
        tokens:
          type: array
          items:
            $ref: '#/components/schemas/WalletToken'
          description: Requires expand=tokens
        labels:
          type: array
          items:
            $ref: '#/components/schemas/WalletLabel'
          description: Requires expand=labels
    FilterCondition:
      type: object
      additionalProperties: false
      required:
        - field
        - op
      properties:
        field:
          type: string
          description: >-
            Stable profile filter field. User/profile/social fields use user.*
            or users.*. Resource metrics use chains.balance, apps.balance,
            tokens.balance, or labels.value; resource identifiers belong in
            named qualifier properties and must not be embedded in the field
            path.
        op:
          type: string
          enum:
            - eq
            - neq
            - gt
            - gte
            - lt
            - lte
            - in
            - nin
            - contains
            - startsWith
            - endsWith
            - notEmpty
            - isEmpty
          description: >-
            Comparison operator. Only the canonical terse tokens (`eq`, `neq`,
            `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains`, `startsWith`,
            `endsWith`, `notEmpty`, `isEmpty`) are accepted; the retired
            long-form spellings (`equals`, `notEquals`, `greater`,
            `greaterOrEqual`, `less`, `lessOrEqual`, `notIn`, `includes`) are
            rejected with a 400 naming the token. **Substring operators:**
            `contains`, `startsWith` and `endsWith` are supported on routable
            string user attributes (`users.device`, `users.os`,
            `users.referrer`, `users.utm_*`, `users.click_id`, and the
            `first_*`/`last_*` attribution variants), where they match
            **case-sensitively**. Social fields (e.g. `users.twitter`,
            `users.email`) additionally support `contains`, which matches
            **case-insensitively** there; `startsWith`/`endsWith` are rejected
            on social fields. `contains` is also supported on `labels.value` and
            matches case-insensitively; `startsWith` and `endsWith` remain
            unsupported on labels. All three substring operators are rejected on
            numeric, chain/app/token, and lifecycle fields. `notEmpty` ("is not
            empty" / "is set") is a string existence check for user string
            attributes (device/os/utm_*/referrer/…) and social fields; `isEmpty`
            ("is empty" / "is not set") is its complement, supported for user
            string attributes only (not social fields). Both ignore `value`.
            `users.lifecycle` supports only `eq` (a single stage) and `in` (a
            list of stages).
        value:
          oneOf:
            - type: string
            - type: number
            - type: boolean
            - type: array
              minItems: 1
              items:
                oneOf:
                  - type: string
                    pattern: ^[^|]*$
                  - type: number
          description: >-
            Filter value (string, number, boolean, or non-empty array). Required
            for every operator except the existence checks `notEmpty` and
            `isEmpty`, which ignore it. Resource balance fields
            (`chains.balance`, `apps.balance`, and `tokens.balance`) require a
            JSON number; empty strings and numeric strings are rejected.
            `labels.value` also rejects an empty string operand.
        scope:
          type: string
          enum:
            - any
            - protocol
          description: >-
            Required for tokens.balance: any includes wallet and protocol
            balances; protocol requires app_id.
        chain_id:
          type: string
          minLength: 1
          description: Optional chain qualifier. Omit to compare across all chains.
        app_id:
          type: string
          minLength: 1
          description: >-
            Required for apps.balance and for tokens.balance with
            scope=protocol.
        token_address:
          type: string
          minLength: 1
          description: Required token address for tokens.balance.
        tag_id:
          type: string
          minLength: 1
          description: Required label tag for labels.value.
    WalletChain:
      type: object
      properties:
        chain_id:
          type: string
        net_worth_usd:
          type: number
        tx_count:
          type: integer
        first_onchain:
          type: string
        last_onchain:
          type: string
    WalletApp:
      type: object
      properties:
        chain_id:
          type: string
        id:
          type: string
        name:
          type: string
        img:
          type: string
          nullable: true
        url:
          type: string
          nullable: true
        balance_usd:
          type: number
    WalletToken:
      type: object
      properties:
        chain_id:
          type: string
        token_address:
          type: string
        app_id:
          type: string
        name:
          type: string
        symbol:
          type: string
        img:
          type: string
          nullable: true
        decimals:
          type: integer
        price:
          type: number
        balance:
          type: string
        balance_usd:
          type: number
    WalletLabel:
      type: object
      properties:
        id:
          type: string
          description: e.g. coinbase.verified_account
        value:
          type: string
        chain_id:
          type: string
        source:
          type: string
    Error:
      type: object
      description: >-
        Standard error envelope returned by every public API endpoint for any
        non-2xx response. The HTTP status code carries success/failure; the body
        provides a machine-readable `code`, a human-readable `message`, and a
        `doc_url` pointing at the matching section of the docs so agents can
        fetch context on the fly.
      properties:
        error:
          type: object
          required:
            - code
            - message
            - doc_url
          properties:
            code:
              $ref: '#/components/schemas/ErrorCode'
            message:
              type: string
              description: >-
                Human-readable error description. Wording may change between
                releases, so branch on `code`, not `message`.
            doc_url:
              type: string
              format: uri
              description: >-
                Link to the matching section of the errors reference at
                https://docs.formo.so/api/errors.
            param:
              type: string
              description: >-
                When the error pertains to a specific request field, the dotted
                path to that field (e.g. `body.trigger_filters.0.value`).
            details:
              type: object
              additionalProperties: true
              description: >-
                Code-specific extra context. For `INVALID_VALIDATION_REQUEST`
                this is a `{ fieldPath: message }` map of every Zod validation
                failure.
      required:
        - error
    ErrorCode:
      type: string
      description: >-
        Stable, enumerated error codes. New codes may be added in any release;
        clients should treat unknown codes as the closest matching HTTP status
        family.
      enum:
        - INTERNAL_SERVER_ERROR
        - INVALID_VALIDATION_REQUEST
        - UNAUTHORIZED
        - BAD_REQUEST
        - FORBIDDEN
        - NOT_FOUND
        - CONFLICT
        - INVALID_CHAIN_ID
        - CONTEXT_LIMIT_EXCEEDED
        - SERVICE_UNAVAILABLE
        - TOO_MANY_REQUESTS
        - IDEMPOTENCY_IN_PROGRESS
        - INVALID_IDEMPOTENCY_KEY
  responses:
    BadRequest:
      description: >-
        The request was rejected. `code` is either `INVALID_VALIDATION_REQUEST`
        (Zod schema mismatch; `details` carries a `{ fieldPath: message }` map)
        or `BAD_REQUEST` (semantic validation failure outside Zod, e.g.
        mismatched IDs, business-rule violations). Branch on `code`, not status,
        to tell the two apart.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            validation:
              summary: Zod schema mismatch
              value:
                error:
                  code: INVALID_VALIDATION_REQUEST
                  message: Invalid request data
                  doc_url: https://docs.formo.so/api/errors#invalid_validation_request
                  details:
                    body.name: String must contain at least 1 character(s)
            semantic:
              summary: Semantic validation failure
              value:
                error:
                  code: BAD_REQUEST
                  message: Target board must be different from the current board
                  doc_url: https://docs.formo.so/api/errors#bad_request
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: UNAUTHORIZED
              message: Invalid API key
              doc_url: https://docs.formo.so/api/errors#unauthorized
    Forbidden:
      description: The API key is valid but lacks the required scope for this endpoint.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: FORBIDDEN
              message: 'API key missing required scope: alerts:write'
              doc_url: https://docs.formo.so/api/errors#forbidden
    TooManyRequests:
      description: >-
        Per-workspace rate limit exceeded. Inspect the `RateLimit-*` response
        headers and back off.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: TOO_MANY_REQUESTS
              message: Too many requests
              doc_url: https://docs.formo.so/api/errors#too_many_requests
    InternalServerError:
      description: >-
        An unexpected error occurred on the server. The error has been logged;
        retry with exponential backoff.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: INTERNAL_SERVER_ERROR
              message: Internal Server Error
              doc_url: https://docs.formo.so/api/errors#internal_server_error
  securitySchemes:
    WorkspaceApiKey:
      type: http
      scheme: bearer
      description: >-
        Workspace API key (e.g. `formo_xxx`). Create one in the Formo dashboard
        under Team Settings > API.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.