# Views

Manage views within a list. Each view belongs to a list and defines its own filters, display type, and column configuration.

#### View types

* `grid` — flat table layout. Columns are defined in `settings.columns`.
* `board` — kanban-style layout. Requires `settings.groupBy` to be set to the field to group by (e.g. `"stage"` or `"custom.<column-id>"`). To create a board view, create it as `grid` first, then PATCH with `{"type": "board"}`.

#### Settings object

```json
{
  "columns": [
    { "key": "&" },
    { "key": "value" },
    { "key": "closeDate" },
    { "key": "custom.<column-id>" }
  ],
  "groupBy": "stage",
  "isDefault": true
}
```

* `columns` — ordered list of visible columns. `"&"` is always the primary name column and should be first.
* `groupBy` — field key to group rows by (board views require this; grid views support it optionally).
* `isDefault` — marks this as the default view opened when navigating to the list. Only one view per list should have `isDefault: true`.

#### Default columns

The API has **no concept of default columns**. When a view is created without a `settings.columns` array, it defaults to `settings: {}` with no columns visible. The UI populates sensible defaults client-side, but these are not enforced or provided by the API. Always include a `columns` array when creating views via the API.

#### Filters

Each view has its own `filters` object. For dynamic lists, this is what determines which records appear. Filters use the same operator syntax as the `where` query parameter. Field keys map directly to entity field names.

```json
{
  "stage": { "$in": ["<stage-uuid-1>", "<stage-uuid-2>"] },
  "lastActivity.time": { "$lt": "-7d" },
  "custom.<column-id>": "some-value"
}
```

Patching `filters` replaces the entire filters object — always include all desired filters, not just the changed ones.

## List views

> Returns views. Filter by list using \`{"listId": "\<LIST\_UUID>"}\` or by workspace using \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`.\
> \
> Each view has its own \`filters\` object that determines which records are shown. For dynamic lists, this is the primary mechanism for filtering records.\
> \
> \### Filter syntax\
> \
> View filters use the same operator syntax as the \`where\` parameter across the API. Field keys map to entity field names:\
> \
> \`\`\`json\
> {\
> &#x20; "name": { "$contains": "acme" },\
> &#x20; "lastActivity.time": { "$lt": "-7d" },\
> &#x20; "title": { "$contains": "Engineer" },\
> &#x20; "custom.\<column-id>": true\
> }\
> \`\`\`<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.9.5"},"tags":[{"name":"Views","description":"Manage views within a list. Each view belongs to a list and defines its own filters, display type, and column configuration.\n\n### View types\n- `grid` — flat table layout. Columns are defined in `settings.columns`.\n- `board` — kanban-style layout. Requires `settings.groupBy` to be set to the field to group by (e.g. `\"stage\"` or `\"custom.<column-id>\"`). To create a board view, create it as `grid` first, then PATCH with `{\"type\": \"board\"}`.\n\n### Settings object\n```json\n{\n  \"columns\": [\n    { \"key\": \"&\" },\n    { \"key\": \"value\" },\n    { \"key\": \"closeDate\" },\n    { \"key\": \"custom.<column-id>\" }\n  ],\n  \"groupBy\": \"stage\",\n  \"isDefault\": true\n}\n```\n- `columns` — ordered list of visible columns. `\"&\"` is always the primary name column and should be first.\n- `groupBy` — field key to group rows by (board views require this; grid views support it optionally).\n- `isDefault` — marks this as the default view opened when navigating to the list. Only one view per list should have `isDefault: true`.\n\n### Default columns\nThe API has **no concept of default columns**. When a view is created without a `settings.columns` array, it defaults to `settings: {}` with no columns visible. The UI populates sensible defaults client-side, but these are not enforced or provided by the API. Always include a `columns` array when creating views via the API.\n\n### Filters\nEach view has its own `filters` object. For dynamic lists, this is what determines which records appear. Filters use the same operator syntax as the `where` query parameter. Field keys map directly to entity field names.\n\n```json\n{\n  \"stage\": { \"$in\": [\"<stage-uuid-1>\", \"<stage-uuid-2>\"] },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"custom.<column-id>\": \"some-value\"\n}\n```\n\nPatching `filters` replaces the entire filters object — always include all desired filters, not just the changed ones.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Contact the Zero team to obtain your API token."}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"View":{"type":"object","description":"A view belongs to a list and defines filters and display configuration. A list can have multiple views, each with its own independent set of filters.\n\nFor dynamic lists, the view filters determine which workspace records are shown.\nFor regular lists, filters narrow down the explicitly-added records.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"listId":{"type":"string","format":"uuid"},"name":{"type":"string"},"entity":{"type":"string","enum":["contacts","companies","deals"]},"type":{"type":"string","enum":["grid","board"],"description":"View display type.\n- `grid` — flat table layout.\n- `board` — kanban-style layout. `settings.groupBy` should be set for meaningful grouping (the API does not enforce this but the UI needs it to display correctly).\nTo convert a `grid` view to `board`, PATCH `{\"type\": \"board\"}`.\n"},"filters":{"type":"object","description":"Filters that determine which records are shown in this view. Uses the same operator syntax as the `where` query parameter.\n\nFor dynamic lists, this is the primary (and only) mechanism for filtering records — do not set filters on the list object itself.\n\nExample:\n```json\n{\n  \"stage\": { \"$in\": [\"<uuid>\", \"<uuid>\"] },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"custom.<column-id>\": \"some-value\"\n}\n```\n"},"settings":{"type":"object","description":"Display settings for the view.\n\n```json\n{\n  \"columns\": [\n    { \"key\": \"&\" },\n    { \"key\": \"value\" },\n    { \"key\": \"closeDate\" },\n    { \"key\": \"custom.<column-id>\" }\n  ],\n  \"groupBy\": \"stage\",\n  \"isDefault\": true\n}\n```\n\n- `columns` — ordered list of visible columns. `\"&\"` is the primary name column and should always be first.\n- `groupBy` — field key to group records by. Required for `board` views. Accepts standard field names (e.g. `\"stage\"`) or `\"custom.<column-id>\"`.\n- `isDefault` — if `true`, this view is opened by default when navigating to the list. Only one view per list should have this set.\n\nWhen PATCHing `settings`, always include the full object — partial updates will overwrite existing column configuration.\n"},"highlights":{"type":"array","items":{},"description":"Highlight rules for the view"},"order":{"type":"integer"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/views":{"get":{"tags":["Views"],"summary":"List views","description":"Returns views. Filter by list using `{\"listId\": \"<LIST_UUID>\"}` or by workspace using `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`.\n\nEach view has its own `filters` object that determines which records are shown. For dynamic lists, this is the primary mechanism for filtering records.\n\n### Filter syntax\n\nView filters use the same operator syntax as the `where` parameter across the API. Field keys map to entity field names:\n\n```json\n{\n  \"name\": { \"$contains\": \"acme\" },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"title\": { \"$contains\": \"Engineer\" },\n  \"custom.<column-id>\": true\n}\n```\n","operationId":"listViews","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/View"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Create a view

> Create a new view within a list. Both \`entity\` and \`type\` are required.\
> \
> Supported \`type\` values: \`grid\`, \`board\`.\
> \
> \> \*\*Board views:\*\* Create as \`grid\` first, then PATCH \`{"type": "board"}\`. Board views should have \`settings.groupBy\` set — the API does not enforce it but the UI needs it to display columns correctly.\
> \
> \> \*\*Default view:\*\* Set \`settings.isDefault: true\` on the first view, or apply it via a follow-up PATCH. Only one view per list should be the default.\
> \
> Filters can be set at creation or added later via PATCH. For dynamic lists, filters on the view determine which records are shown.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.9.5"},"tags":[{"name":"Views","description":"Manage views within a list. Each view belongs to a list and defines its own filters, display type, and column configuration.\n\n### View types\n- `grid` — flat table layout. Columns are defined in `settings.columns`.\n- `board` — kanban-style layout. Requires `settings.groupBy` to be set to the field to group by (e.g. `\"stage\"` or `\"custom.<column-id>\"`). To create a board view, create it as `grid` first, then PATCH with `{\"type\": \"board\"}`.\n\n### Settings object\n```json\n{\n  \"columns\": [\n    { \"key\": \"&\" },\n    { \"key\": \"value\" },\n    { \"key\": \"closeDate\" },\n    { \"key\": \"custom.<column-id>\" }\n  ],\n  \"groupBy\": \"stage\",\n  \"isDefault\": true\n}\n```\n- `columns` — ordered list of visible columns. `\"&\"` is always the primary name column and should be first.\n- `groupBy` — field key to group rows by (board views require this; grid views support it optionally).\n- `isDefault` — marks this as the default view opened when navigating to the list. Only one view per list should have `isDefault: true`.\n\n### Default columns\nThe API has **no concept of default columns**. When a view is created without a `settings.columns` array, it defaults to `settings: {}` with no columns visible. The UI populates sensible defaults client-side, but these are not enforced or provided by the API. Always include a `columns` array when creating views via the API.\n\n### Filters\nEach view has its own `filters` object. For dynamic lists, this is what determines which records appear. Filters use the same operator syntax as the `where` query parameter. Field keys map directly to entity field names.\n\n```json\n{\n  \"stage\": { \"$in\": [\"<stage-uuid-1>\", \"<stage-uuid-2>\"] },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"custom.<column-id>\": \"some-value\"\n}\n```\n\nPatching `filters` replaces the entire filters object — always include all desired filters, not just the changed ones.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Contact the Zero team to obtain your API token."}},"schemas":{"ViewCreate":{"type":"object","required":["workspaceId","listId","name","entity","type"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"listId":{"type":"string","format":"uuid"},"name":{"type":"string"},"entity":{"type":"string","enum":["contacts","companies","deals"]},"type":{"type":"string","enum":["grid","board"],"description":"View type. Use `grid` for table layout, `board` for kanban.\nNote: you can also POST directly with `type: \"board\"` — the API accepts it. The create-as-grid-then-PATCH approach also works.\n"},"filters":{"type":"object","description":"Initial filters for the view. For dynamic lists, this controls which records are shown."},"settings":{"type":"object","description":"Initial display settings. If omitted, defaults to `{}` (no columns configured).\nShould include `columns` array and optionally `groupBy` and `isDefault`.\n```json\n{\n  \"columns\": [{ \"key\": \"&\" }, { \"key\": \"value\" }],\n  \"groupBy\": null,\n  \"isDefault\": true\n}\n```\n"},"order":{"type":"integer","description":"Display order of this view within the list. Use `0` for the first/default view."}}},"View":{"type":"object","description":"A view belongs to a list and defines filters and display configuration. A list can have multiple views, each with its own independent set of filters.\n\nFor dynamic lists, the view filters determine which workspace records are shown.\nFor regular lists, filters narrow down the explicitly-added records.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"listId":{"type":"string","format":"uuid"},"name":{"type":"string"},"entity":{"type":"string","enum":["contacts","companies","deals"]},"type":{"type":"string","enum":["grid","board"],"description":"View display type.\n- `grid` — flat table layout.\n- `board` — kanban-style layout. `settings.groupBy` should be set for meaningful grouping (the API does not enforce this but the UI needs it to display correctly).\nTo convert a `grid` view to `board`, PATCH `{\"type\": \"board\"}`.\n"},"filters":{"type":"object","description":"Filters that determine which records are shown in this view. Uses the same operator syntax as the `where` query parameter.\n\nFor dynamic lists, this is the primary (and only) mechanism for filtering records — do not set filters on the list object itself.\n\nExample:\n```json\n{\n  \"stage\": { \"$in\": [\"<uuid>\", \"<uuid>\"] },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"custom.<column-id>\": \"some-value\"\n}\n```\n"},"settings":{"type":"object","description":"Display settings for the view.\n\n```json\n{\n  \"columns\": [\n    { \"key\": \"&\" },\n    { \"key\": \"value\" },\n    { \"key\": \"closeDate\" },\n    { \"key\": \"custom.<column-id>\" }\n  ],\n  \"groupBy\": \"stage\",\n  \"isDefault\": true\n}\n```\n\n- `columns` — ordered list of visible columns. `\"&\"` is the primary name column and should always be first.\n- `groupBy` — field key to group records by. Required for `board` views. Accepts standard field names (e.g. `\"stage\"`) or `\"custom.<column-id>\"`.\n- `isDefault` — if `true`, this view is opened by default when navigating to the list. Only one view per list should have this set.\n\nWhen PATCHing `settings`, always include the full object — partial updates will overwrite existing column configuration.\n"},"highlights":{"type":"array","items":{},"description":"Highlight rules for the view"},"order":{"type":"integer"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/views":{"post":{"tags":["Views"],"summary":"Create a view","description":"Create a new view within a list. Both `entity` and `type` are required.\n\nSupported `type` values: `grid`, `board`.\n\n> **Board views:** Create as `grid` first, then PATCH `{\"type\": \"board\"}`. Board views should have `settings.groupBy` set — the API does not enforce it but the UI needs it to display columns correctly.\n\n> **Default view:** Set `settings.isDefault: true` on the first view, or apply it via a follow-up PATCH. Only one view per list should be the default.\n\nFilters can be set at creation or added later via PATCH. For dynamic lists, filters on the view determine which records are shown.\n","operationId":"createView","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ViewCreate"}}}},"responses":{"200":{"description":"View created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/View"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Get a view

> Retrieve a single view by ID.

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.9.5"},"tags":[{"name":"Views","description":"Manage views within a list. Each view belongs to a list and defines its own filters, display type, and column configuration.\n\n### View types\n- `grid` — flat table layout. Columns are defined in `settings.columns`.\n- `board` — kanban-style layout. Requires `settings.groupBy` to be set to the field to group by (e.g. `\"stage\"` or `\"custom.<column-id>\"`). To create a board view, create it as `grid` first, then PATCH with `{\"type\": \"board\"}`.\n\n### Settings object\n```json\n{\n  \"columns\": [\n    { \"key\": \"&\" },\n    { \"key\": \"value\" },\n    { \"key\": \"closeDate\" },\n    { \"key\": \"custom.<column-id>\" }\n  ],\n  \"groupBy\": \"stage\",\n  \"isDefault\": true\n}\n```\n- `columns` — ordered list of visible columns. `\"&\"` is always the primary name column and should be first.\n- `groupBy` — field key to group rows by (board views require this; grid views support it optionally).\n- `isDefault` — marks this as the default view opened when navigating to the list. Only one view per list should have `isDefault: true`.\n\n### Default columns\nThe API has **no concept of default columns**. When a view is created without a `settings.columns` array, it defaults to `settings: {}` with no columns visible. The UI populates sensible defaults client-side, but these are not enforced or provided by the API. Always include a `columns` array when creating views via the API.\n\n### Filters\nEach view has its own `filters` object. For dynamic lists, this is what determines which records appear. Filters use the same operator syntax as the `where` query parameter. Field keys map directly to entity field names.\n\n```json\n{\n  \"stage\": { \"$in\": [\"<stage-uuid-1>\", \"<stage-uuid-2>\"] },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"custom.<column-id>\": \"some-value\"\n}\n```\n\nPatching `filters` replaces the entire filters object — always include all desired filters, not just the changed ones.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Contact the Zero team to obtain your API token."}},"schemas":{"View":{"type":"object","description":"A view belongs to a list and defines filters and display configuration. A list can have multiple views, each with its own independent set of filters.\n\nFor dynamic lists, the view filters determine which workspace records are shown.\nFor regular lists, filters narrow down the explicitly-added records.\n","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"listId":{"type":"string","format":"uuid"},"name":{"type":"string"},"entity":{"type":"string","enum":["contacts","companies","deals"]},"type":{"type":"string","enum":["grid","board"],"description":"View display type.\n- `grid` — flat table layout.\n- `board` — kanban-style layout. `settings.groupBy` should be set for meaningful grouping (the API does not enforce this but the UI needs it to display correctly).\nTo convert a `grid` view to `board`, PATCH `{\"type\": \"board\"}`.\n"},"filters":{"type":"object","description":"Filters that determine which records are shown in this view. Uses the same operator syntax as the `where` query parameter.\n\nFor dynamic lists, this is the primary (and only) mechanism for filtering records — do not set filters on the list object itself.\n\nExample:\n```json\n{\n  \"stage\": { \"$in\": [\"<uuid>\", \"<uuid>\"] },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"custom.<column-id>\": \"some-value\"\n}\n```\n"},"settings":{"type":"object","description":"Display settings for the view.\n\n```json\n{\n  \"columns\": [\n    { \"key\": \"&\" },\n    { \"key\": \"value\" },\n    { \"key\": \"closeDate\" },\n    { \"key\": \"custom.<column-id>\" }\n  ],\n  \"groupBy\": \"stage\",\n  \"isDefault\": true\n}\n```\n\n- `columns` — ordered list of visible columns. `\"&\"` is the primary name column and should always be first.\n- `groupBy` — field key to group records by. Required for `board` views. Accepts standard field names (e.g. `\"stage\"`) or `\"custom.<column-id>\"`.\n- `isDefault` — if `true`, this view is opened by default when navigating to the list. Only one view per list should have this set.\n\nWhen PATCHing `settings`, always include the full object — partial updates will overwrite existing column configuration.\n"},"highlights":{"type":"array","items":{},"description":"Highlight rules for the view"},"order":{"type":"integer"},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/views/{viewId}":{"get":{"tags":["Views"],"summary":"Get a view","description":"Retrieve a single view by ID.","operationId":"getView","parameters":[{"name":"viewId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/View"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Delete a view

> Permanently delete a view. Returns \`{"data": 1}\` on success, \`{"data": 0}\` if not found.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.9.5"},"tags":[{"name":"Views","description":"Manage views within a list. Each view belongs to a list and defines its own filters, display type, and column configuration.\n\n### View types\n- `grid` — flat table layout. Columns are defined in `settings.columns`.\n- `board` — kanban-style layout. Requires `settings.groupBy` to be set to the field to group by (e.g. `\"stage\"` or `\"custom.<column-id>\"`). To create a board view, create it as `grid` first, then PATCH with `{\"type\": \"board\"}`.\n\n### Settings object\n```json\n{\n  \"columns\": [\n    { \"key\": \"&\" },\n    { \"key\": \"value\" },\n    { \"key\": \"closeDate\" },\n    { \"key\": \"custom.<column-id>\" }\n  ],\n  \"groupBy\": \"stage\",\n  \"isDefault\": true\n}\n```\n- `columns` — ordered list of visible columns. `\"&\"` is always the primary name column and should be first.\n- `groupBy` — field key to group rows by (board views require this; grid views support it optionally).\n- `isDefault` — marks this as the default view opened when navigating to the list. Only one view per list should have `isDefault: true`.\n\n### Default columns\nThe API has **no concept of default columns**. When a view is created without a `settings.columns` array, it defaults to `settings: {}` with no columns visible. The UI populates sensible defaults client-side, but these are not enforced or provided by the API. Always include a `columns` array when creating views via the API.\n\n### Filters\nEach view has its own `filters` object. For dynamic lists, this is what determines which records appear. Filters use the same operator syntax as the `where` query parameter. Field keys map directly to entity field names.\n\n```json\n{\n  \"stage\": { \"$in\": [\"<stage-uuid-1>\", \"<stage-uuid-2>\"] },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"custom.<column-id>\": \"some-value\"\n}\n```\n\nPatching `filters` replaces the entire filters object — always include all desired filters, not just the changed ones.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Contact the Zero team to obtain your API token."}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/views/{viewId}":{"delete":{"tags":["Views"],"summary":"Delete a view","description":"Permanently delete a view. Returns `{\"data\": 1}` on success, `{\"data\": 0}` if not found.\n","operationId":"deleteView","parameters":[{"name":"viewId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deletion result","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"integer","description":"`1` if deleted, `0` if not found"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Update a view

> Update a view's name, type, filters, or display settings. Returns only changed fields.\
> \
> \`entity\` is immutable and cannot be changed after creation.\
> \
> Patching \`filters\` replaces the entire filters object — include all desired filters, not just the changed ones.\
> \
> Patching \`settings\` replaces the entire settings object — always include the full \`columns\` array and \`groupBy\` value, not just the changed fields.\
> \
> To convert a grid view to a board view: \`PATCH {"type": "board"}\`. Board views should have \`settings.groupBy\` set — the API does not enforce it but the UI needs it to display columns correctly.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.9.5"},"tags":[{"name":"Views","description":"Manage views within a list. Each view belongs to a list and defines its own filters, display type, and column configuration.\n\n### View types\n- `grid` — flat table layout. Columns are defined in `settings.columns`.\n- `board` — kanban-style layout. Requires `settings.groupBy` to be set to the field to group by (e.g. `\"stage\"` or `\"custom.<column-id>\"`). To create a board view, create it as `grid` first, then PATCH with `{\"type\": \"board\"}`.\n\n### Settings object\n```json\n{\n  \"columns\": [\n    { \"key\": \"&\" },\n    { \"key\": \"value\" },\n    { \"key\": \"closeDate\" },\n    { \"key\": \"custom.<column-id>\" }\n  ],\n  \"groupBy\": \"stage\",\n  \"isDefault\": true\n}\n```\n- `columns` — ordered list of visible columns. `\"&\"` is always the primary name column and should be first.\n- `groupBy` — field key to group rows by (board views require this; grid views support it optionally).\n- `isDefault` — marks this as the default view opened when navigating to the list. Only one view per list should have `isDefault: true`.\n\n### Default columns\nThe API has **no concept of default columns**. When a view is created without a `settings.columns` array, it defaults to `settings: {}` with no columns visible. The UI populates sensible defaults client-side, but these are not enforced or provided by the API. Always include a `columns` array when creating views via the API.\n\n### Filters\nEach view has its own `filters` object. For dynamic lists, this is what determines which records appear. Filters use the same operator syntax as the `where` query parameter. Field keys map directly to entity field names.\n\n```json\n{\n  \"stage\": { \"$in\": [\"<stage-uuid-1>\", \"<stage-uuid-2>\"] },\n  \"lastActivity.time\": { \"$lt\": \"-7d\" },\n  \"custom.<column-id>\": \"some-value\"\n}\n```\n\nPatching `filters` replaces the entire filters object — always include all desired filters, not just the changed ones.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Contact the Zero team to obtain your API token."}},"schemas":{"ViewUpdate":{"type":"object","description":"All fields are optional. Multiple fields can be updated in a single PATCH request.","properties":{"name":{"type":"string","description":"Display name of the view."},"type":{"type":"string","enum":["grid","board"],"description":"Change the view layout. Use `\"board\"` for kanban, `\"grid\"` for table.\nCan be toggled freely — both `grid→board` and `board→grid` work.\nWhen switching to `\"board\"`, ensure `settings.groupBy` is set for meaningful column grouping.\n"},"filters":{"type":"object","description":"Replaces the **entire** filters object — include all desired filters, not just the changed ones.\nFor dynamic lists this controls which records are shown.\nSet to `{}` to clear all filters.\n"},"settings":{"type":"object","description":"Replaces the **entire** settings object — always include the full `columns` array, `groupBy`, and `isDefault` values.\nPartial settings updates will overwrite existing column configuration.\nIf `settings` is omitted on creation, it defaults to `{}`.\n"},"order":{"type":"integer","description":"Display order of this view within the list."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/views/{viewId}":{"patch":{"tags":["Views"],"summary":"Update a view","description":"Update a view's name, type, filters, or display settings. Returns only changed fields.\n\n`entity` is immutable and cannot be changed after creation.\n\nPatching `filters` replaces the entire filters object — include all desired filters, not just the changed ones.\n\nPatching `settings` replaces the entire settings object — always include the full `columns` array and `groupBy` value, not just the changed fields.\n\nTo convert a grid view to a board view: `PATCH {\"type\": \"board\"}`. Board views should have `settings.groupBy` set — the API does not enforce it but the UI needs it to display columns correctly.\n","operationId":"updateView","parameters":[{"name":"viewId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ViewUpdate"}}}},"responses":{"200":{"description":"View updated. Only changed fields are returned in `data`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````
