---
title: "List Contacts"
method: GET
path: "/v1/contacts"
tags: ["contacts"]
---

# List Contacts

`GET /v1/contacts`

## Request

- Base URL: `https://api.elaan.io` (the document declares no server; this is the origin it was published from)
- URL: `https://api.elaan.io/v1/contacts`
- Auth: none declared

## Query parameters

- `limit` integer
- `offset` integer
- `search` string, nullable

## Headers

- `authorization` string, nullable

## Response `200`

Successful Response

- ContactPage: A page of contacts plus the total matching the filter, for pagination.
  - `items` ContactResponse[], required
    - `id` string, required
    - `external_id` string, nullable, required
    - `language` string, nullable, required: The contact's preferred language tag (e.g. "es"), or null for the language-less default. Set at create or via PUT /v1/contacts/{id}/language — deliberately NOT part of the whole-resource PUT, so a partial-shaped update can't silently clear it.
    - `emails` string[], required
    - `phones` string[], required
    - `push_subscriptions` PushSubscriptionDTO[], required
      - `value` string, required
      - `provider` 'fcm' | 'apns' | 'expo' | 'onesignal' | 'webpush', required: The service a push subscription is delivered *through* — the routing key that ties a contact's `PushSubscription` to the tenant's configured `PushTransportConfig` and selects which sender adapter delivers it. Cross-cutting vocabulary (like `Channel`): it lives in `shared` because both `contact` (the destination side) and `push_transport` / `outbound_push` (the sending side) reference the same members, and they must not drift apart. Covers direct transports (FCM / APNs / Web Push) and aggregators that fan out to the native platforms themselves (Expo, OneSignal). Subclassing `str` keeps JSON and dict keys clean.
      - `platform` 'ios' | 'android' | 'web': The device platform a subscription targets. Optional metadata — used for payload shaping once the push channel lands (APNs, Android and Web bodies differ), never for routing (that is `PushProvider`'s job).
      - `auth` string, nullable
      - `p256dh` string, nullable
    - `attributes` object, required
    - `preferences` PreferenceDTO[], required
      - `notification_type_key` string, required
      - `channel` 'email' | 'inbox' | 'push' | 'web_push', required: A delivery channel a notification can be sent over. This is an enum (not a bare string) so adding SMS / WHATSAPP later is a one-line change and every per-channel mapping (notification-type defaults, contact preferences) extends automatically. Crucially, a newly-added channel is **opted out by default** everywhere: `NotificationType.default_for` returns False for a channel absent from `channel_defaults`, and a contact has no preference until they set one — so an existing install gains a channel that delivers to nobody until a tenant explicitly enables it. That's why adding a member needs no data migration. Subclassing `str` makes members compare/serialize as their value ("email"), which keeps JSON and dict keys clean.
      - `enabled` boolean, required
  - `total` integer, required
  - `limit` integer, required
  - `offset` integer, required

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/elaan/apis/elaan-api.md) · [All operations](https://skmtc.dev/elaan/apis/elaan-api/llms.txt) · [OpenAPI document](https://skmtc.dev/elaan/apis/elaan-api/revisions/c96a7e6399aa?raw)
