---
title: "List contacts"
method: GET
path: "/api/v3/contacts"
tags: ["V3 Contacts"]
---

# List contacts

`GET /api/v3/contacts`

## Request

- Base URL: `https://app.formbricks.com`
- URL: `https://app.formbricks.com/api/v3/contacts`
- Auth: one of:
  - API key in cookie `next-auth.session-token`
  - API key in header `x-api-key`
  - HTTP bearer

Returns a workspace's contacts, filtered, searched, sorted and counted on the server. Requires the
Contacts entitlement (**403** without it). Session cookie, `x-api-key`, or a Bearer token.

**Attribute filters** use `filter[attribute.<key>][<operator>]=<value>`, where `<key>` is a
`ContactAttributeKeyResource.key` of the workspace (an unknown key → **400**). The operators depend
on the key's `dataType`; an operator the type does not support → **400**:

| dataType | operators |
| -------- | --------- |
| `string` | `in`, `eq`, `ne`, `contains`, `notContains`, `startsWith`, `endsWith`, `isSet`, `isNotSet` |
| `number` | `in`, `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `isSet`, `isNotSet` |
| `date`   | `gte`, `lte` (ISO 8601 date-time with offset, inclusive), `isSet`, `isNotSet` |

Text comparisons are case-insensitive. `in` matches any of its values: repeat the key once per
value (`filter[attribute.plan][in]=Pro&filter[attribute.plan][in]=Team`). String values are taken
verbatim — never split on commas, since a value can contain one; a `number` `in` also accepts
comma-separated values. `ne`, `contains`, `notContains`, `startsWith` and `endsWith` may
also be repeated, and then every value must hold (AND). Every other operator takes exactly one
value (repeating it → **400**).
`isSet` / `isNotSet` ignore their value (send `true`). A value is *set* when the attribute row
exists and carries a value (non-empty text, or a non-null number/date). `ne` and
`notContains` match only contacts whose value is set and differs — use `isNotSet` for contacts
without one. All filters combine with AND. At most 50 distinct filter parameters per request and
at most 100 values per `in`.

The cursor is bound to `sortBy` and to the search and filter set; changing any of them while
paging → **400** naming `cursor`. Unknown query parameters → **400**.

## Query parameters

- `workspaceId` string, cuid2, required
- `limit` integer
- `cursor` string
- `includeTotalCount` boolean
- `search` string
- `sortBy` '-createdAt' | 'createdAt'
- `filter[createdAt][gte]` string, date-time
- `filter[createdAt][lte]` string, date-time

## Response `200`

Contacts retrieved successfully

- object
  - `data` ContactResource[], required
    - `id` string, cuid2, required
    - `createdAt` string, date-time, required
    - `updatedAt` string, date-time, required
    - `userId` string, nullable, required
    - `email` string, nullable, required
    - `firstName` string, nullable, required
    - `lastName` string, nullable, required
    - `attributes` object[], required
      - `key` string, required: The attribute key (`ContactAttributeKeyResource.key`).
      - `name` string, nullable, required: The key's display name.
      - `dataType` 'string' | 'number' | 'date', required
      - `value` string, required: The value as text: a number attribute as its decimal string, a date attribute as an ISO 8601 date-time.
  - `meta` CountedListMeta, required: A list `meta` carrying pagination and the filtered total. `totalCount` is `required` but nullable, gated on the operation's `includeTotalCount` parameter.
    - `limit` integer, required: The page size actually applied. Carries no `maximum`: it echoes what the server used, it does not bound what may be asked for. The request-side bound is the `limit` query parameter on the operation itself and differs per collection — read it there. A shared description cannot state it without being wrong for some of the operations that compose this base.
    - `nextCursor` string, nullable, required: Opaque cursor for the next page. `null` when there are no more results — the only reliable end-of-collection signal, since the server may return fewer items than `limit` without the collection being exhausted. Pass it back verbatim: the encoding is deliberately unspecified and may change without notice, so a client that decodes or constructs one is relying on something this contract does not promise.
    - `totalCount` integer, nullable, required: Total number of items matching the current filters (and search) across all pages. `null` when the request did not ask for it — see the operation's `includeTotalCount` parameter, whose default differs per collection.

## Other responses

- `400`: Bad Request — malformed JSON, invalid query/body/params, duplicate name, or unsupported field.
- `401`: Not authenticated (no valid session or API key).
- `403`: Forbidden — no workspace access, or resource does not exist (404 not used; avoids existence leak).
- `429`: Rate limit exceeded.
- `500`: Internal Server Error.

## Changes

- **2026-10-29** `82c0afd2df39`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/formbricks/apis/formbricks-api-v3/changes/api/v3/contacts/get.md)

---

[API](https://skmtc.dev/formbricks/apis/formbricks-api-v3.md) - [All operations](https://skmtc.dev/formbricks/apis/formbricks-api-v3/llms.txt) - [OpenAPI document](https://skmtc.dev/formbricks/apis/formbricks-api-v3/revisions/25335c959a13?raw)
