> ## Documentation Index
> Fetch the complete documentation index at: https://help.retainful.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Contacts API

> Create, fetch, and list contact profiles.

Contacts (also called profiles) are the people in your audience. The Contacts API lets you create them, update them, and read them back.

All requests require [authentication](/developers/authentication).

## Create or update a contact

`POST /api/v1/customer/create`

Upserts a contact and returns `201 Created`. If a contact with the same email or phone already exists, it's updated; otherwise it's created. This endpoint does **not** add anyone to a list — for that, see the [Lists API](/developers/api/contact-groups).

### Request

You must send **`email` or `phone`** — at least one. Everything else is optional.

<ParamField body="email" type="string">
  The contact's email address. Required unless you send `phone`.
</ParamField>

<ParamField body="phone" type="string">
  Phone number, 7–20 characters. Required unless you send `email`. Needed for WhatsApp messaging.
</ParamField>

<ParamField body="external_id" type="string">
  Your own identifier for this person (your database ID), for reconciling across systems.
</ParamField>

<ParamField body="first_name" type="string">
  Max 100 characters.
</ParamField>

<ParamField body="last_name" type="string">
  Max 100 characters.
</ParamField>

<ParamField body="email_opt_in" type="string">
  Email marketing consent: `SUBSCRIBED`, `NON_SUBSCRIBED`, or `UNSUBSCRIBED`. Defaults to `NON_SUBSCRIBED` when omitted. Only send `SUBSCRIBED` when the person actually opted in.
</ParamField>

<ParamField body="phone_opt_in" type="string">
  SMS/WhatsApp consent, same values. Defaults to `NON_SUBSCRIBED`.
</ParamField>

<ParamField body="is_suppressed" type="boolean">
  Whether the contact is [suppressed](/audience/suppressed-contacts). Defaults to `false`.
</ParamField>

<ParamField body="double_opt_in" type="boolean">
  Whether to require email confirmation before the contact counts as subscribed. Defaults to `false`.
</ParamField>

<ParamField body="source" type="string">
  Where this contact came from, e.g. `booking-platform`. Defaults to `api`.
</ParamField>

<ParamField body="dob" type="string">
  Date of birth as an ISO 8601 date, e.g. `1990-01-15`.
</ParamField>

<ParamField body="gender" type="string">
  `MALE`, `FEMALE`, or `OTHER`. Case-sensitive.
</ParamField>

<ParamField body="country" type="string">
  Max 100 characters. Normalized to a country code on write.
</ParamField>

<ParamField body="state" type="string">
  Max 100 characters.
</ParamField>

<ParamField body="city" type="string">
  Max 100 characters.
</ParamField>

<ParamField body="address1" type="string">
  Max 255 characters.
</ParamField>

<ParamField body="address2" type="string">
  Max 255 characters.
</ParamField>

<ParamField body="postal_code" type="string">
  3–20 characters.
</ParamField>

<ParamField body="custom_properties" type="object">
  Key–value pairs stored as [custom fields](/audience/custom-fields) — usable in segments and personalization. Maximum 30 keys per request, string values.
</ParamField>

<Warning>
  Omitting `email_opt_in` creates the contact as `NON_SUBSCRIBED`, and they will receive no marketing email. Set it explicitly whenever you have consent — this is the single most common reason API-created contacts appear to be ignored by campaigns.
</Warning>

<Note>
  Unrecognized fields are rejected with `400`. Send `first_name`, not `firstName`.
</Note>

```bash Example request theme={null}
curl -X POST "https://api.retainful.net/api/v1/customer/create" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $RETAINFUL_API_KEY" \
  -d '{
    "email": "jane@example.com",
    "first_name": "Jane",
    "last_name": "Smith",
    "phone": "+14155550123",
    "external_id": "cust_8841",
    "email_opt_in": "SUBSCRIBED",
    "source": "api",
    "custom_properties": {
      "loyalty_tier": "gold",
      "signup_channel": "mobile-app"
    }
  }'
```

### Response

```json 201 Created theme={null}
{
  "data": {
    "type": "profile",
    "id": "01K8J5M02QYPEJE5YN3V944ZY9",
    "attributes": {
      "email": "jane@example.com",
      "phone": "+14155550123",
      "first_name": "Jane",
      "email_opt_in": "SUBSCRIBED",
      "source": "api",
      "created": "2026-06-11T06:27:50.000Z",
      "updated": "2026-06-11T06:27:50.000Z"
    },
    "relationships": {
      "lists": {
        "links": { "related": ".../profiles/01K8J5M02QYPEJE5YN3V944ZY9/lists" }
      }
    }
  },
  "links": { "self": ".../api/v1/customer/01K8J5M02QYPEJE5YN3V944ZY9" }
}
```

Keep the `id` — it's the stable identifier for fetching this contact later, and for sending events with `contactUUID`.

### Custom properties

* Maximum **30 keys per request**; more returns `400`.
* Values are stored as strings. Numbers and booleans are converted; **nested objects are not** — they store as unusable text, so flatten them before sending.
* Empty and whitespace-only values are dropped rather than stored.

## Get a contact

`GET /api/v1/customer/{id}`

```bash theme={null}
curl "https://api.retainful.net/api/v1/customer/01K8J5M02QYPEJE5YN3V944ZY9" \
  -H "X-API-Key: $RETAINFUL_API_KEY"
```

Returns the same profile envelope as above, or `404` if no contact has that ID.

## List contacts

`GET /api/v1/customer`

Returns contacts in pages.

| Parameter | Default | Notes                                                  |
| --------- | ------- | ------------------------------------------------------ |
| `page`    | `1`     | Page number.                                           |
| `limit`   | `10`    | Contacts per page. Values above 100 are capped at 100. |

```bash theme={null}
curl "https://api.retainful.net/api/v1/customer?page=1&limit=50" \
  -H "X-API-Key: $RETAINFUL_API_KEY"
```

<Tip>
  For a one-time bulk load of an existing audience, the dashboard's [CSV import](/audience/import-contacts) is faster and friendlier to [rate limits](/developers/rate-limits) than looping over this API.
</Tip>
