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

# Contacts & profile

> Manage contacts, consent, and your WhatsApp business profile.

Contacts are the people you message. LazyApp stores them per workspace and mode; broadcasts and marketing sends require a contact row plus granted marketing [consent](https://docs.lazyapp.in/sending/broadcasts.md#recording-consent-from-your-own-opt-in-flow).

There is no `profileId`, `accountId`, or `platform` on contact calls — the API key scopes everything.

## Quick start

```bash theme={null}
# Create or update by phone (upsert)
curl https://lazyapp.in/v1/contacts \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+919812345670",
    "email": "asha@acme.example",
    "username": "Asha",
    "attributes": { "company": "Acme Retail" }
  }'

# Record marketing consent from your opt-in flow
curl -X POST https://lazyapp.in/v1/contacts/{uuid}/consent \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "granted": true,
    "purpose": "marketing",
    "consent_text": "I agree to receive WhatsApp offers",
    "proof_reference": "checkout-1042",
    "captured_by": "website"
  }'
```

## Create and update

`POST /v1/contacts` upserts on the first identity field you provide:

| Identity field | Use when |
| - | - |
| `phone_number` | Normal WhatsApp contacts (E.164) |
| `external_id` | Your CRM primary key |
| `bsuid` | Meta business-scoped user id when you do not have a phone yet |

At least one of `phone_number`, `bsuid`, or `external_id` is required.

Optional fields: `username`, `email`, `preferred_language`, `timezone`, `attributes` (free-form JSON for CRM fields).

`attributes` is merged, not replaced, on both the upsert and `PATCH /v1/contacts/{contact}`: the keys you send are set, a key you set to `null` is removed, and every other key stays. So your CRM sync can write `company` while a lead-scoring job writes `lead_score` without either erasing the other. Send `"attributes": null` to clear them all.

`PATCH /v1/contacts/{contact}` accepts the same body fields. Look up `{contact}` by uuid, phone number, `external_id`, or `bsuid`.

### Service window on read

`GET /v1/contacts/{contact}` includes `service_window` — whether the [24-hour window](https://docs.lazyapp.in/sending/messages.md#the-24-hour-window) is open, when it expires, and the last inbound timestamp. Use this before choosing template vs free-form send.

## List and search

```bash theme={null}
curl "https://lazyapp.in/v1/contacts?limit=25&q=9812" \
  -H "Authorization: Bearer $LAZYAPP_API_KEY"
```

Cursor-paginated (`next_cursor`). `q` matches phone, `external_id`, or email.

## Consent and opt-out

| Endpoint | Purpose |
| - | - |
| `POST /v1/contacts/{contact}/consent` | Record opt-in evidence (`consent_text`, `proof_reference`, `captured_by`) |
| `POST /v1/contacts/{contact}/suppress` | Opt out of `marketing` or `all` sends |

Contacts can also opt out by message. `STOP`, `UNSUBSCRIBE` or `REMOVE` opts out of marketing, `STOP ALL` out of everything, and `START` or `UNSTOP` opts back in to marketing (translations in Hindi, Spanish, Portuguese, Arabic and French work too). The whole message has to be the keyword. A plain "yes" never opts anyone back in: in a conversation it answers whatever you just asked.

Granting marketing consent lifts a matching suppression. See [Broadcasts](https://docs.lazyapp.in/sending/broadcasts.md) for how exclusions work at send time.

## Merge duplicates

When the same person arrives under two identities:

```bash theme={null}
curl -X POST https://lazyapp.in/v1/contacts/{target_uuid}/merge \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "source": "{source_uuid}" }'
```

Messages and attributes consolidate onto the target contact.

## Bulk import

Upsert up to **1000** contacts in one request:

```bash theme={null}
curl -X POST https://lazyapp.in/v1/contacts/bulk \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      { "phone_number": "+919811111111", "attributes": { "first_name": "Ada" } },
      { "phone_number": "+919822222222", "email": "bob@acme.example" }
    ]
  }'
```

Response: `{ "created", "updated", "failed": [{ "index", "message" }] }`.

## Tags

Structured membership (keep free-form CRM fields in `attributes`):

```bash theme={null}
curl -X POST https://lazyapp.in/v1/tags \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "vip", "color": "#111111" }'

curl -X POST https://lazyapp.in/v1/contacts/{uuid}/tags \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "tags": ["vip", "newsletter"] }'
```

`DELETE /v1/contacts/{uuid}/tags` detaches by tag uuid or name. `GET /v1/tags` lists tags.

## Segments

```bash theme={null}
curl -X POST https://lazyapp.in/v1/contact-segments \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "VIP buyers",
    "filters": { "attribute": { "key": "tier", "equals": "vip" } }
  }'
```

Every filter you give must match:

| Filter | Shape | Matches |
| - | - | - |
| `tags` | `{ "mode": "any" \| "all", "names": [...] }` | Contacts with any (or all) of the tags. Name tags by `names` or `uuids`, as [`GET /v1/tags`](#tags) returns them |
| `attribute` | `{ "key": "tier", "equals": "vip" }` | An exact attribute value |
| `consent_status` | `"granted"`, `"revoked"` or `"unknown"` | Marketing consent; `unknown` means none was recorded |
| `suppressed` | `true` or `false` | Contacts who did, or did not, opt out of everything |

For example, everyone you tagged over the API for an intake announcement:

```json theme={null}
{
  "name": "Intake alerts",
  "filters": { "tags": { "mode": "any", "names": ["admissions:intake-alerts"] } }
}
```

A tag name or uuid that does not exist in the workspace is refused with `422`, so a typo cannot silently produce an empty audience. Reads return each tag's `names`, `uuids` and internal `ids`. Use the segment `uuid` as `contact_segment_uuid` on [broadcasts](https://docs.lazyapp.in/sending/broadcasts.md).

## Business profile

Prefer native routes on a connection uuid (also available via the [Meta-compatible proxy](https://docs.lazyapp.in/migrating/from-cloud-api.md)):

```bash theme={null}
curl https://lazyapp.in/v1/connections/{connection_uuid}/business-profile \
  -H "Authorization: Bearer $LAZYAPP_API_KEY"

curl -X PATCH https://lazyapp.in/v1/connections/{connection_uuid}/business-profile \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "about": "Your go-to store for widgets",
    "email": "hello@acme.example",
    "websites": ["https://acme.example"]
  }'
```

## Block users

```bash theme={null}
curl -X POST https://lazyapp.in/v1/connections/{connection_uuid}/block-users \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "users": ["+919812345670"] }'
```

`users` takes phone numbers or BSUIDs. `DELETE` with the same body unblocks, and `GET` lists the block list. Limits, per-person failures and the identity change check are in [Identity, privacy and data](https://docs.lazyapp.in/guides/identity-and-privacy.md#block-people).

## What we skip (Zernio-style)

| Zernio | LazyApp |
| - | - |
| `profileId` + `accountId` + `platformIdentifier` | Workspace scoped; `phone_number` or `bsuid` |
| `isSubscribed` boolean | Explicit `consent` + `suppress` endpoints |

## Related

* [Broadcasts](https://docs.lazyapp.in/sending/broadcasts.md) — audience and consent requirements
* [Messages](https://docs.lazyapp.in/sending/messages.md) — send to a contact's phone or BSUID
* [API reference — contacts](https://docs.lazyapp.in/api-reference/contacts/list-contacts.md)
* [Migrating from Cloud API](https://docs.lazyapp.in/migrating/from-cloud-api.md) — proxy auth and paths


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.