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

# Create contact (or reuse the existing one)

> Creates a contact or, if the company already has a contact with any of the identifiers sent, reuses that contact instead of creating another.

- **Identifiers:** `phone_number` (with `country_code`), `email` and `linkedin_identifier`. Besides `name`, at least one of them is required; with none, 400 `"Informe pelo menos um identificador: telefone, e-mail ou LinkedIn."`. The contact can be created without a phone.
- **Lookup order:** email, LinkedIn and phone. Email only counts with `@` and is recognized in its normalized form (no leading or trailing spaces, lowercase), which is how it's saved from now on; old emails saved with uppercase letters are only recognized with the same spelling, until the database is normalized. LinkedIn accepts the profile URL or just the name. Phone is normalized (area code, 9th digit).
- **201:** contact created. The body is the contact with `created: true`, `matched_by: null`, `matches_count: 0` and `ignored_fields: []`.
- **200:** existing contact reused. The body is the contact with `created: false`, `matched_by` (`email`, `linkedin` or `phone`) and `matches_count` (how many contacts in the company matched by that key; above 1, there are duplicates and the oldest one was used). Found by phone, the fields sent are updated, as the POST always did; found by email or LinkedIn, only the fields that were empty are filled in, and `ignored_fields` lists the ones sent that weren't saved because the contact already had another value (e.g. `email`, `phone_number`, `job_title`, `additional_attributes.<slug>`). `name` and the owner never change. To change any field of an existing contact, use `PUT /v1/contacts/{id}`.
- **409 `CONTACT_IDENTITY_CONFLICT`:** the identifiers sent belong to different contacts (e.g. the phone belongs to one contact and the email to another). Nothing is saved; `conflicts` returns the contact for each identifier.
- **Profile fields:** `job_title`, `seniority`, `department`, `country`, `region`, `city` and `experience` are saved on creation; on an existing contact, they follow the 200 rule (updated if found by phone, only filled in if empty when found by email or LinkedIn).
- **Phone:** digits only, area code + number, without the country code (the country code goes in `country_code`, required together with the phone). An invalid phone returns 400.
- **additional_attributes:** `{ "slug": value }`, with a primitive value (string, number, boolean or null). A slug that doesn't exist, or that belongs to a custom field whose context isn't CONTACT, is silently ignored. On an existing contact, merge by slug: found by phone, the slugs sent are updated; found by email or LinkedIn, only empty ones are filled in.
- **Check first:** `POST /v1/contacts/identity-check` tells you what this route would do, without saving.



## OpenAPI

````yaml /en/openapi.json post /v1/contacts
openapi: 3.0.0
info:
  title: Nuvia API - Company
  description: API documentation for companies
  version: '1.0'
  contact: {}
servers:
  - url: https://api.nuvia.ai
security: []
tags:
  - name: messages
    x-group: Mensagens
  - name: contacts
    x-group: Contatos
  - name: campaigns
    x-group: Campanhas
  - name: tables
    x-group: Tabelas e Listas
  - name: agents
    x-group: Agentes
  - name: conversations
    x-group: Conversas
  - name: inboxes
    x-group: Caixas de Entrada
  - name: knowledge
    x-group: Base de Conhecimento
  - name: Webhooks
    x-group: Webhooks
  - name: campaign-enrollments
    x-group: Inscrições em Campanha
  - name: custom-fields
    x-group: Campos Customizados
  - name: resources
    x-group: Arquivos e Anexos
  - name: hubspot
    x-group: HubSpot
paths:
  /v1/contacts:
    post:
      tags:
        - contacts
      summary: Create contact (or reuse the existing one)
      description: >-
        Creates a contact or, if the company already has a contact with any of
        the identifiers sent, reuses that contact instead of creating another.


        - **Identifiers:** `phone_number` (with `country_code`), `email` and
        `linkedin_identifier`. Besides `name`, at least one of them is required;
        with none, 400 `"Informe pelo menos um identificador: telefone, e-mail
        ou LinkedIn."`. The contact can be created without a phone.

        - **Lookup order:** email, LinkedIn and phone. Email only counts with
        `@` and is recognized in its normalized form (no leading or trailing
        spaces, lowercase), which is how it's saved from now on; old emails
        saved with uppercase letters are only recognized with the same spelling,
        until the database is normalized. LinkedIn accepts the profile URL or
        just the name. Phone is normalized (area code, 9th digit).

        - **201:** contact created. The body is the contact with `created:
        true`, `matched_by: null`, `matches_count: 0` and `ignored_fields: []`.

        - **200:** existing contact reused. The body is the contact with
        `created: false`, `matched_by` (`email`, `linkedin` or `phone`) and
        `matches_count` (how many contacts in the company matched by that key;
        above 1, there are duplicates and the oldest one was used). Found by
        phone, the fields sent are updated, as the POST always did; found by
        email or LinkedIn, only the fields that were empty are filled in, and
        `ignored_fields` lists the ones sent that weren't saved because the
        contact already had another value (e.g. `email`, `phone_number`,
        `job_title`, `additional_attributes.<slug>`). `name` and the owner never
        change. To change any field of an existing contact, use `PUT
        /v1/contacts/{id}`.

        - **409 `CONTACT_IDENTITY_CONFLICT`:** the identifiers sent belong to
        different contacts (e.g. the phone belongs to one contact and the email
        to another). Nothing is saved; `conflicts` returns the contact for each
        identifier.

        - **Profile fields:** `job_title`, `seniority`, `department`, `country`,
        `region`, `city` and `experience` are saved on creation; on an existing
        contact, they follow the 200 rule (updated if found by phone, only
        filled in if empty when found by email or LinkedIn).

        - **Phone:** digits only, area code + number, without the country code
        (the country code goes in `country_code`, required together with the
        phone). An invalid phone returns 400.

        - **additional_attributes:** `{ "slug": value }`, with a primitive value
        (string, number, boolean or null). A slug that doesn't exist, or that
        belongs to a custom field whose context isn't CONTACT, is silently
        ignored. On an existing contact, merge by slug: found by phone, the
        slugs sent are updated; found by email or LinkedIn, only empty ones are
        filled in.

        - **Check first:** `POST /v1/contacts/identity-check` tells you what
        this route would do, without saving.
      operationId: ContactController_createContact
      parameters:
        - name: x-company-id
          in: header
          description: >-
            Target company identifier. Required only for global API keys
            (type=global). Ignored for company API keys and human users.
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateContactDto'
            examples:
              contato:
                summary: Contato com e-mail, cargo e campo personalizado
                value:
                  name: João Silva
                  country_code: '55'
                  phone_number: '11999998888'
                  email: joao.silva@exemplo.com.br
                  job_title: Gerente de Vendas
                  city: São Paulo
                  additional_attributes:
                    cpf: '12345678900'
      responses:
        '200':
          description: >-
            Existing contact reused (`created: false`). Found by phone, the
            fields sent were updated; by email or LinkedIn, only empty ones were
            filled in and `ignored_fields` lists what wasn't saved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateContactResponseDto'
        '201':
          description: 'Contact created (`created: true`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateContactResponseDto'
        '400':
          description: >-
            No identifier (`"Informe pelo menos um identificador: telefone,
            e-mail ou LinkedIn."`), phone without `country_code`, or invalid
            phone
        '401':
          description: Invalid or expired token
        '403':
          description: You do not have permission to access this resource
        '409':
          description: >-
            Conflict (`CONTACT_IDENTITY_CONFLICT`): the identifiers provided
            belong to different contacts. Nothing is saved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactIdentityErrorResponseDto'
      security:
        - JWT-auth: []
components:
  schemas:
    CreateContactDto:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          description: >-
            The contact's full name. Required. If the contact already exists,
            its name is preserved.
        country_code:
          type: string
          description: >-
            Country calling code (1-3 digits). Examples: 55 (Brazil), 1 (USA),
            351 (Portugal). Required when phone_number is sent; without a phone,
            don't send it.
          minLength: 1
          maxLength: 3
          pattern: ^\d{1,3}$
        email:
          type: string
          description: >-
            The contact's email address. Identifies the contact by its
            normalized form (no leading or trailing spaces, lowercase), which is
            how it's saved. Old emails saved with uppercase letters are only
            recognized with the same spelling, until the database is normalized.
            Only counts as an identifier if it contains "@". On an existing
            contact, it's updated when the contact is found by phone; found by
            email or LinkedIn, it's only filled in if empty.
        linkedin_identifier:
          type: string
          description: >-
            The contact's LinkedIn identifier: profile URL or just the profile
            name. Example: joao-silva-123. Identifies the contact. On an
            existing contact, it's updated when the contact is found by phone;
            found by email or LinkedIn, it's only filled in if empty.
        phone_number:
          type: string
          description: >-
            Phone number without formatting, digits only (area code + number,
            without the country code). Example: 11999998888. Identifies the
            contact. A phone that doesn't form a valid number with country_code
            is rejected with 400. Optional: the contact needs at least one
            identifier among phone, email, and LinkedIn.
          pattern: ^\d*$
        job_title:
          type: string
          description: >-
            The contact's job title. Example: Sales Manager. Saved on creation;
            on an existing contact, it's updated when the contact is found by
            phone; found by email or LinkedIn, it's only filled in if empty.
        seniority:
          type: string
          description: >-
            The contact's seniority level. Example: Senior, Junior, Mid-level.
            Saved on creation; on an existing contact, it's updated when the
            contact is found by phone; found by email or LinkedIn, it's only
            filled in if empty.
        department:
          type: string
          description: >-
            The contact's department. Example: Sales, IT, Marketing. Saved on
            creation; on an existing contact, it's updated when the contact is
            found by phone; found by email or LinkedIn, it's only filled in if
            empty.
        country:
          type: string
          description: >-
            The contact's country. Example: Brazil. Saved on creation; on an
            existing contact, it's updated when the contact is found by phone;
            found by email or LinkedIn, it's only filled in if empty.
        region:
          type: string
          description: >-
            The contact's state or region. Example: São Paulo. Saved on
            creation; on an existing contact, it's updated when the contact is
            found by phone; found by email or LinkedIn, it's only filled in if
            empty.
        city:
          type: string
          description: >-
            The contact's city. Example: São Paulo. Saved on creation; on an
            existing contact, it's updated when the contact is found by phone;
            found by email or LinkedIn, it's only filled in if empty.
        experience:
          type: string
          description: >-
            The contact's professional experience. Saved on creation; on an
            existing contact, it's updated when the contact is found by phone;
            found by email or LinkedIn, it's only filled in if empty.
        additional_attributes:
          type: object
          description: >-
            The contact's custom fields. Format: { "field_slug": value }. Slugs
            must match custom fields registered with the CONTACT context.
            Invalid or nonexistent slugs are silently ignored. Values must be
            primitives (string, number, boolean or null). On an existing contact
            found by phone, the slugs sent are updated (merge); found by email
            or LinkedIn, only empty slugs are filled in. Slugs not sent stay as
            they are. Example: { "cpf": "12345678900", "data_nascimento":
            "1990-01-15", "valor_total": 1500.50 }
          additionalProperties:
            anyOf:
              - type: string
                nullable: true
              - type: number
                nullable: true
              - type: boolean
                nullable: true
      required:
        - name
    CreateContactResponseDto:
      type: object
      properties:
        _id:
          description: Contact ID
          type: string
        id:
          description: Contact ID (same value as `_id`)
          type: string
        name:
          type: string
          description: Contact name
        country_code:
          description: >-
            Country code of the number. Absent, along with the phone, when the
            contact only has a WhatsApp identifier.
          type: string
        email:
          description: Contact's email
          type: string
        last_activity_at:
          description: Date of the contact's last activity
        phone_number:
          description: >-
            The contact's phone number. Absent when the contact only has a
            WhatsApp identifier.
          type: string
        linkedin_identifier:
          description: LinkedIn profile identifier
          type: string
        job_title:
          description: Contact's job title
          type: string
        seniority:
          description: Contact's seniority level
          type: string
        department:
          description: Contact's department
          type: string
        country:
          description: Contact's country
          type: string
        region:
          description: Contact's state or region
          type: string
        city:
          description: Contact's city
          type: string
        experience:
          description: Contact's professional experience
          type: string
        skills:
          description: Contact's skills
          type: string
        additional_attributes:
          description: >-
            Values of the contact's custom fields, by slug. When saved via
            `additional_attributes`, the value comes in the envelope `{ id,
            value, type, updatedAt }`. When saved via `custom_fields` (PUT), it
            comes raw: the option_id (select), the list of option_id
            (multi_select) or the text (text).
          type: object
          additionalProperties:
            anyOf:
              - type: object
                properties:
                  id:
                    type: string
                    description: Custom field ID
                  value:
                    anyOf:
                      - type: string
                        nullable: true
                      - type: number
                        nullable: true
                      - type: boolean
                        nullable: true
                    description: Saved value
                  type:
                    type: string
                    description: Custom field type (e.g. text, number, date)
                  updatedAt:
                    description: Date the value was last saved
                required:
                  - id
                  - value
                  - type
                  - updatedAt
                nullable: true
              - type: string
                nullable: true
              - type: array
                items:
                  type: string
                nullable: true
        business:
          description: ID of the Business the contact belongs to
          type: string
          nullable: true
        notes:
          description: Notes recorded on the contact
          type: array
          items:
            type: object
            properties:
              _id:
                type: string
                description: Note ID
              content:
                type: string
                description: Note text
              user:
                type: string
                description: ID of the user who recorded the note
              createdAt:
                description: Date the note was created
              updatedAt:
                description: Date the note was updated
            required:
              - _id
              - content
              - user
        marketing_opt_out:
          description: >-
            Marketing opt-out per communication channel (`whatsapp`, `email`,
            `linkedin`). Absent or empty when the contact hasn't opted out of
            any channel.
          type: object
          additionalProperties:
            type: object
            properties:
              value:
                type: boolean
                description: true when the contact opted out of marketing on this channel
              source:
                type: string
                description: Source of the opt-out record
              at:
                description: Date of the record
            required:
              - value
              - source
              - at
        whatsapp_user_id:
          description: >-
            The contact's WhatsApp identifier (BSUID), assigned by Meta. Opaque
            string.
          type: string
        whatsapp_username:
          description: The contact's WhatsApp @username, as Meta sends it
          type: string
        whatsapp_parent_user_id:
          description: The contact's WhatsApp parent_user_id, as Meta sends it
          type: string
        company:
          description: Contact's company
        createdAt:
          description: Contact creation date
        updatedAt:
          description: Contact update date
        created:
          type: boolean
          description: >-
            true when the contact was created (201); false when it was reused
            (200).
        matched_by:
          description: Identifier that matched
          type: string
          enum:
            - email
            - linkedin
            - phone
          nullable: true
        matches_count:
          type: number
          description: >-
            How many contacts in the company matched by the `matched_by` key (0
            on creation). Above 1, the company has duplicate contacts and the
            oldest one was reused.
        ignored_fields:
          type: array
          items:
            type: string
          description: >-
            Fields sent that were NOT saved because the existing contact already
            had another value (only when reused by email or LinkedIn). E.g.
            `email`, `phone_number`, `job_title`,
            `additional_attributes.<slug>`. Empty on creation and when found by
            phone.
      required:
        - name
        - company
        - created
        - matched_by
        - matches_count
        - ignored_fields
    ContactIdentityErrorResponseDto:
      type: object
      properties:
        statusCode:
          type: number
          description: '409'
        code:
          type: string
          enum:
            - CONTACT_IDENTITY_CONFLICT
            - CONTACT_IDENTIFIER_TAKEN
          description: >-
            `CONTACT_IDENTITY_CONFLICT` on creation; `CONTACT_IDENTIFIER_TAKEN`
            on update.
        message:
          type: string
          description: Message to display to the user
        conflicts:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
                enum:
                  - email
                  - linkedin
                  - phone
                description: Identifier that matched
              contact:
                type: object
                properties:
                  _id:
                    description: Contact ID
                    type: string
                  name:
                    type: string
                    description: Contact name
                  email:
                    description: Contact's email
                    type: string
                  phone_number:
                    description: The contact's phone
                    type: string
                  country_code:
                    description: Phone country calling code
                    type: string
                  linkedin_identifier:
                    description: The contact's LinkedIn identifier
                    type: string
                required:
                  - _id
            required:
              - field
              - contact
          description: Contact that already has each conflicting identifier
      required:
        - statusCode
        - code
        - message
        - conflicts
  securitySchemes:
    JWT-auth:
      scheme: bearer
      bearerFormat: JWT
      type: http
      description: JWT authentication token

````

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