> ## Documentation Index
> Fetch the complete documentation index at: https://docs.folksbase.joselito.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Contact Management

> How to create, search, filter, tag, and manage contacts in folksbase.

## Overview

Contacts are the core entity in folksbase. Each contact belongs to a workspace and has standard fields (email, name, phone, company, notes) plus a flexible `custom_fields` JSONB column for anything else. Contacts can be tagged, searched, filtered, and exported.

All contact operations go through the layered architecture: Route → Service → Repository → Drizzle. The API is fully documented via OpenAPI at `/api/docs`.

## Creating Contacts

Contacts can be created in two ways:

* **Manually** via the UI or `POST /api/contacts`
* **Bulk** via CSV import (see [CSV Import](/features/csv-import))

When creating a contact, the system automatically:

* Normalizes the email address (lowercase, trimmed)
* Invalidates the workspace stats cache

```json theme={"dark"}
// POST /api/contacts
{
  "email": "[email]",
  "first_name": "[name]",
  "last_name": "[name]",
  "phone": "[phone_number]",
  "company": "Acme Corp",
  "notes": "Met at conference"
}
```

Email is the only required field. Duplicate emails within a workspace are handled via upsert during CSV import, but the manual create endpoint will insert a new record.

## Listing & Pagination

The `GET /api/contacts` endpoint returns a paginated list using cursor-based pagination (never OFFSET). The response shape:

```json theme={"dark"}
{
  "data": [{ "id": "...", "email": "...", "tags": [...] }],
  "nextCursor": "uuid-of-last-item",
  "total": 1234
}
```

Pass `nextCursor` as the `cursor` query parameter to fetch the next page. The default page size is 50, configurable up to 100 via the `limit` parameter.

## Search

The `search` query parameter performs a case-insensitive `ILIKE` search across three fields simultaneously:

* `email`
* `first_name`
* `last_name`

The frontend debounces search input by 300ms to avoid excessive API calls on every keystroke.

```
GET /api/contacts?search=jane
```

## Filtering

### By Tags

Pass one or more tag IDs as a comma-separated `tag_ids` query parameter. The filter uses an `ANY` match — contacts with at least one of the specified tags are returned.

```
GET /api/contacts?tag_ids=uuid-1,uuid-2
```

When tag filters are active, the `total` count in the response reflects the filtered count (using `COUNT(DISTINCT contact.id)` to avoid double-counting contacts with multiple matching tags).

### By Unsubscribe Status

```
GET /api/contacts?is_unsubscribed=true
```

## Updating Contacts

`PATCH /api/contacts/:id` accepts partial updates. Only the fields you send are modified.

If the email changes, the system automatically re-fetches the Gravatar avatar for the new address.

## Deleting Contacts

Two options:

* **Single delete** — `DELETE /api/contacts/:id` returns 204
* **Bulk delete** — `POST /api/contacts/bulk-delete` accepts up to 500 IDs per request

Both operations invalidate the count cache and stats cache.

## Sending Emails

You can send a one-off email to any contact via `POST /api/contacts/:id/send-email`:

```json theme={"dark"}
{
  "subject": "Follow up",
  "body": "Thanks for chatting at the conference."
}
```

The email is sent through Resend using the workspace's configured API key (if set) or the default key. The sender name is derived from the authenticated user's profile, and the reply-to address is set to the user's email.

## Tagging

Contacts support a many-to-many relationship with tags through the `contact_tags` junction table. See the [Tags](/features/tags) guide for details on creating and assigning tags.

When listing contacts, tags are fetched in a single batch query (not N+1) and included in the response as a `tags` array on each contact.

## Custom Fields

Any CSV column that doesn't map to a standard field is stored in the `custom_fields` JSONB column. These fields are:

* Preserved during upserts
* Included in CSV exports
* Indexed with a GIN index for query performance

See the [Database](/architecture/database) docs for more on the schema design.
