---
title: "Create Customer Profile"
method: POST
path: "/api/v1/report-ai/customer-profiles"
tags: ["report-ai", "customer-profiles"]
---

# Create Customer Profile

`POST /api/v1/report-ai/customer-profiles`

Create a contact manually, requiring at least an email or phone number.

## Request body

- CustomerProfileCreate — Body for POST /customer-profiles — manual contact creation.
  - `customer_email` string, email, nullable
  - `customer_name` string, nullable
  - `phone_number` string, nullable
  - `country` string, nullable

## Response `201`

Successful Response

- CustomerProfileRead — Schema for reading a CustomerProfile. requires_attention and attention_reason are derived from complaints_count at serialization time (the DB columns were removed).
  - `id` string, uuid, required
  - `tenant_id` string, uuid, required
  - `customer_email` string, nullable, required
  - `customer_name` string, nullable, required
  - `phone_number` string, nullable, required
  - `total_investigations` integer, required
  - `complaints_count` integer, required
  - `last_sentiment` string, nullable, required
  - `total_revenue_cents` integer, nullable, required
  - `average_booking_value_cents` integer, nullable, required
  - `loyalty_tier` string, nullable, required
  - `country` string, nullable
  - `identity_kind` 'person' | 'shared_inbox' | 'unclassified' | 'stay_guest' — Whether a customer profile stands for one person. ``SHARED_INBOX`` marks an address that books on behalf of many different guests (tour operator, bed bank, travel agent). Such a profile is a real counterparty but NOT a person: it must never receive guest-directed messaging and must not appear in the contacts list as a customer. Classified by ``app.bookings.private.identity_durability``; see ``app.bookings.management.classify_shared_inboxes``. ``STAY_GUEST`` marks the guest of one reservation, known by their stay rather than by their identity — used when a reservation carries no durable identifier and a person cannot honestly be inferred from a name. Keyed on ``('stay', 'episode_id', <uuid>)``, and holds NULL email and NULL phone **always**. Like ``SHARED_INBOX`` it is a real record and not a contact, and is held out of the roster, campaigns, segments and duplicate detection by the same mechanism.
  - `requires_attention` boolean
  - `attention_reason` string, nullable
  - `business_id` string, uuid, nullable
  - `first_seen_at` string, date-time, required
  - `last_seen_at` string, date-time, required
  - `sources` SourceBadgeRead[]
    - `source_key` string, required
    - `label` string, required
    - `badge_hue` string, required
    - `external_id` string, nullable
    - `first_seen_at` string, date-time, required
    - `last_seen_at` string, date-time, required
  - `attention` AttentionFlag[]
    - `kind` 'open_duplicate' | 'field_conflict' | 'relay_identity' | 'no_source', required
    - `detail` string, required

## Other responses

- `409` — Email already exists for another profile
- `422` — Validation Error

---

[API](https://skmtc.dev/getanana/apis/cleon-api.md) · [All operations](https://skmtc.dev/getanana/apis/cleon-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/getanana/cleon-api/revisions/3ad158624c53/schema)
