> ## 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.

# Crear contacto (o reutilizar el existente)

> Crea un contacto o, si la empresa ya tiene un contacto con alguno de los identificadores enviados, reutiliza ese contacto en lugar de crear otro.

- **Identificadores:** `phone_number` (con `country_code`), `email` y `linkedin_identifier`. Además del `name`, se exige al menos uno de ellos; sin ninguno, 400 `"Informe pelo menos um identificador: telefone, e-mail ou LinkedIn."`. El contacto puede crearse sin teléfono.
- **Orden de búsqueda:** correo electrónico, LinkedIn y teléfono. El correo solo cuenta con `@` y se reconoce en la forma normalizada (sin espacios en los extremos, en minúsculas), que es como pasa a guardarse; los correos antiguos guardados con mayúsculas solo se reconocen con la misma grafía, hasta la normalización de la base. LinkedIn acepta la URL del perfil o solo el nombre. El teléfono se normaliza (código de área, 9.º dígito).
- **201:** contacto creado. El cuerpo es el contacto con `created: true`, `matched_by: null`, `matches_count: 0` e `ignored_fields: []`.
- **200:** contacto existente reutilizado. El cuerpo es el contacto con `created: false`, `matched_by` (`email`, `linkedin` o `phone`) y `matches_count` (cuántos contactos de la empresa coincidieron por esa clave; por encima de 1, hay duplicados y se usó el más antiguo). Encontrado por teléfono, los campos enviados se actualizan, como el POST siempre hizo; encontrado por correo electrónico o LinkedIn, solo se completan los campos que estaban vacíos, e `ignored_fields` indica los enviados que no se guardaron porque el contacto ya tenía otro valor (ej.: `email`, `phone_number`, `job_title`, `additional_attributes.<slug>`). El `name` y el propietario nunca cambian. Para modificar cualquier campo de un contacto existente, use `PUT /v1/contacts/{id}`.
- **409 `CONTACT_IDENTITY_CONFLICT`:** los identificadores enviados pertenecen a contactos distintos (ej.: el teléfono es de un contacto y el correo, de otro). No se guarda nada; `conflicts` devuelve el contacto de cada identificador.
- **Campos de perfil:** `job_title`, `seniority`, `department`, `country`, `region`, `city` y `experience` se guardan en la creación; en un contacto existente, siguen la regla del 200 (se actualizan si se encuentra por teléfono, solo se completan si están vacíos cuando se encuentra por correo electrónico o LinkedIn).
- **Teléfono:** solo dígitos, código de área + número, sin el código de país (el código de país va en `country_code`, obligatorio junto con el teléfono). Un teléfono inválido devuelve 400.
- **additional_attributes:** `{ "slug": valor }`, con valor primitivo (string, number, boolean o null). Un slug inexistente, o de un custom field con contexto distinto de CONTACT, se ignora en silencio. En un contacto existente, merge por slug: encontrado por teléfono, los slugs enviados se actualizan; encontrado por correo electrónico o LinkedIn, solo se completan los vacíos.
- **Verificar antes:** `POST /v1/contacts/identity-check` indica qué haría esta ruta, sin guardar.



## OpenAPI

````yaml /es/openapi.json post /v1/contacts
openapi: 3.0.0
info:
  title: Nuvia API - Empresa
  description: Documentación de la API para empresas
  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: Crear contacto (o reutilizar el existente)
      description: >-
        Crea un contacto o, si la empresa ya tiene un contacto con alguno de los
        identificadores enviados, reutiliza ese contacto en lugar de crear otro.


        - **Identificadores:** `phone_number` (con `country_code`), `email` y
        `linkedin_identifier`. Además del `name`, se exige al menos uno de
        ellos; sin ninguno, 400 `"Informe pelo menos um identificador: telefone,
        e-mail ou LinkedIn."`. El contacto puede crearse sin teléfono.

        - **Orden de búsqueda:** correo electrónico, LinkedIn y teléfono. El
        correo solo cuenta con `@` y se reconoce en la forma normalizada (sin
        espacios en los extremos, en minúsculas), que es como pasa a guardarse;
        los correos antiguos guardados con mayúsculas solo se reconocen con la
        misma grafía, hasta la normalización de la base. LinkedIn acepta la URL
        del perfil o solo el nombre. El teléfono se normaliza (código de área,
        9.º dígito).

        - **201:** contacto creado. El cuerpo es el contacto con `created:
        true`, `matched_by: null`, `matches_count: 0` e `ignored_fields: []`.

        - **200:** contacto existente reutilizado. El cuerpo es el contacto con
        `created: false`, `matched_by` (`email`, `linkedin` o `phone`) y
        `matches_count` (cuántos contactos de la empresa coincidieron por esa
        clave; por encima de 1, hay duplicados y se usó el más antiguo).
        Encontrado por teléfono, los campos enviados se actualizan, como el POST
        siempre hizo; encontrado por correo electrónico o LinkedIn, solo se
        completan los campos que estaban vacíos, e `ignored_fields` indica los
        enviados que no se guardaron porque el contacto ya tenía otro valor
        (ej.: `email`, `phone_number`, `job_title`,
        `additional_attributes.<slug>`). El `name` y el propietario nunca
        cambian. Para modificar cualquier campo de un contacto existente, use
        `PUT /v1/contacts/{id}`.

        - **409 `CONTACT_IDENTITY_CONFLICT`:** los identificadores enviados
        pertenecen a contactos distintos (ej.: el teléfono es de un contacto y
        el correo, de otro). No se guarda nada; `conflicts` devuelve el contacto
        de cada identificador.

        - **Campos de perfil:** `job_title`, `seniority`, `department`,
        `country`, `region`, `city` y `experience` se guardan en la creación; en
        un contacto existente, siguen la regla del 200 (se actualizan si se
        encuentra por teléfono, solo se completan si están vacíos cuando se
        encuentra por correo electrónico o LinkedIn).

        - **Teléfono:** solo dígitos, código de área + número, sin el código de
        país (el código de país va en `country_code`, obligatorio junto con el
        teléfono). Un teléfono inválido devuelve 400.

        - **additional_attributes:** `{ "slug": valor }`, con valor primitivo
        (string, number, boolean o null). Un slug inexistente, o de un custom
        field con contexto distinto de CONTACT, se ignora en silencio. En un
        contacto existente, merge por slug: encontrado por teléfono, los slugs
        enviados se actualizan; encontrado por correo electrónico o LinkedIn,
        solo se completan los vacíos.

        - **Verificar antes:** `POST /v1/contacts/identity-check` indica qué
        haría esta ruta, sin guardar.
      operationId: ContactController_createContact
      parameters:
        - name: x-company-id
          in: header
          description: >-
            Identificador de la empresa objetivo. Obligatorio solo para API keys
            globales (type=global). Se ignora para API keys de empresa y
            usuarios humanos.
          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: >-
            Contacto existente reutilizado (`created: false`). Encontrado por
            teléfono, los campos enviados se actualizaron; por correo
            electrónico o LinkedIn, solo se completaron los vacíos e
            `ignored_fields` indica lo que no se guardó.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateContactResponseDto'
        '201':
          description: 'Contacto creado (`created: true`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateContactResponseDto'
        '400':
          description: >-
            Sin identificador (`"Informe pelo menos um identificador: telefone,
            e-mail ou LinkedIn."`), teléfono sin `country_code` o teléfono
            inválido
        '401':
          description: Token inválido o expirado
        '403':
          description: Sin permiso para acceder a este recurso
        '409':
          description: >-
            Conflicto (`CONTACT_IDENTITY_CONFLICT`): los identificadores
            informados pertenecen a contactos distintos. No se guarda nada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactIdentityErrorResponseDto'
      security:
        - JWT-auth: []
components:
  schemas:
    CreateContactDto:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          description: >-
            Nombre completo del contacto. Obligatorio. Si el contacto ya existe,
            su nombre se conserva.
        country_code:
          type: string
          description: >-
            Código de país para llamadas (1-3 dígitos). Ejemplos: 55 (Brasil), 1
            (EE. UU.), 351 (Portugal). Obligatorio cuando se envía phone_number;
            sin teléfono, no lo envíe.
          minLength: 1
          maxLength: 3
          pattern: ^\d{1,3}$
        email:
          type: string
          description: >-
            Dirección de correo electrónico del contacto. Identifica el contacto
            por la forma normalizada (sin espacios en los extremos, en
            minúsculas), que es como se guarda. Los correos antiguos guardados
            con mayúsculas solo se reconocen con la misma grafía, hasta la
            normalización de la base. Solo cuenta como identificador si tiene
            "@". En un contacto existente, se actualiza cuando el contacto se
            encuentra por teléfono; encontrado por correo electrónico o
            LinkedIn, solo se completa si está vacío.
        linkedin_identifier:
          type: string
          description: >-
            Identificador de LinkedIn del contacto: URL del perfil o solo el
            nombre del perfil. Ejemplo: joao-silva-123. Identifica el contacto.
            En un contacto existente, se actualiza cuando el contacto se
            encuentra por teléfono; encontrado por correo electrónico o
            LinkedIn, solo se completa si está vacío.
        phone_number:
          type: string
          description: >-
            Número de teléfono sin formato, solo dígitos (código de área +
            número, sin el código de país). Ejemplo: 11999998888. Identifica el
            contacto. Un teléfono que no forma un número válido con country_code
            se rechaza con 400. Opcional: el contacto necesita al menos un
            identificador entre teléfono, correo electrónico y LinkedIn.
          pattern: ^\d*$
        job_title:
          type: string
          description: >-
            Cargo del contacto. Ejemplo: Gerente de Ventas. Se guarda en la
            creación; en un contacto existente, se actualiza cuando el contacto
            se encuentra por teléfono; encontrado por correo electrónico o
            LinkedIn, solo se completa si está vacío.
        seniority:
          type: string
          description: >-
            Nivel de seniority del contacto. Ejemplo: Senior, Junior,
            Semi-senior. Se guarda en la creación; en un contacto existente, se
            actualiza cuando el contacto se encuentra por teléfono; encontrado
            por correo electrónico o LinkedIn, solo se completa si está vacío.
        department:
          type: string
          description: >-
            Departamento del contacto. Ejemplo: Comercial, TI, Marketing. Se
            guarda en la creación; en un contacto existente, se actualiza cuando
            el contacto se encuentra por teléfono; encontrado por correo
            electrónico o LinkedIn, solo se completa si está vacío.
        country:
          type: string
          description: >-
            País del contacto. Ejemplo: Brasil. Se guarda en la creación; en un
            contacto existente, se actualiza cuando el contacto se encuentra por
            teléfono; encontrado por correo electrónico o LinkedIn, solo se
            completa si está vacío.
        region:
          type: string
          description: >-
            Estado o región del contacto. Ejemplo: São Paulo. Se guarda en la
            creación; en un contacto existente, se actualiza cuando el contacto
            se encuentra por teléfono; encontrado por correo electrónico o
            LinkedIn, solo se completa si está vacío.
        city:
          type: string
          description: >-
            Ciudad del contacto. Ejemplo: São Paulo. Se guarda en la creación;
            en un contacto existente, se actualiza cuando el contacto se
            encuentra por teléfono; encontrado por correo electrónico o
            LinkedIn, solo se completa si está vacío.
        experience:
          type: string
          description: >-
            Experiencia profesional del contacto. Se guarda en la creación; en
            un contacto existente, se actualiza cuando el contacto se encuentra
            por teléfono; encontrado por correo electrónico o LinkedIn, solo se
            completa si está vacío.
        additional_attributes:
          type: object
          description: >-
            Campos personalizados del contacto. Formato: { "slug_del_campo":
            valor }. Los slugs deben corresponder a custom fields registrados
            con contexto CONTACT. Los slugs inválidos o inexistentes se ignoran
            en silencio. Los valores deben ser primitivos (string, number,
            boolean o null). En un contacto existente encontrado por teléfono,
            los slugs enviados se actualizan (merge); encontrado por correo
            electrónico o LinkedIn, solo se completan los slugs vacíos. Los
            slugs no enviados se mantienen. Ejemplo: { "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: ID del contacto
          type: string
        id:
          description: ID del contacto (mismo valor que `_id`)
          type: string
        name:
          type: string
          description: Nombre del contacto
        country_code:
          description: >-
            Código de país del número. Ausente junto con el teléfono cuando el
            contacto solo tiene un identificador de WhatsApp.
          type: string
        email:
          description: Email del contacto
          type: string
        last_activity_at:
          description: Fecha de la última actividad del contacto
        phone_number:
          description: >-
            Número de teléfono del contacto. Ausente cuando el contacto solo
            tiene un identificador de WhatsApp.
          type: string
        linkedin_identifier:
          description: Identificador del perfil de LinkedIn
          type: string
        job_title:
          description: Cargo del contacto
          type: string
        seniority:
          description: Nivel de seniority del contacto
          type: string
        department:
          description: Departamento del contacto
          type: string
        country:
          description: País del contacto
          type: string
        region:
          description: Estado o región del contacto
          type: string
        city:
          description: Ciudad del contacto
          type: string
        experience:
          description: Experiencia profesional del contacto
          type: string
        skills:
          description: Habilidades del contacto
          type: string
        additional_attributes:
          description: >-
            Valores de los campos personalizados del contacto, por slug. Si se
            guardó vía `additional_attributes`, el valor viene en el envoltorio
            `{ id, value, type, updatedAt }`. Si se guardó vía `custom_fields`
            (PUT), viene sin envoltorio: el option_id (select), la lista de
            option_id (multi_select) o el texto (text).
          type: object
          additionalProperties:
            anyOf:
              - type: object
                properties:
                  id:
                    type: string
                    description: ID del campo personalizado
                  value:
                    anyOf:
                      - type: string
                        nullable: true
                      - type: number
                        nullable: true
                      - type: boolean
                        nullable: true
                    description: Valor guardado
                  type:
                    type: string
                    description: 'Tipo del custom field (ej.: text, number, date)'
                  updatedAt:
                    description: Fecha del último guardado del valor
                required:
                  - id
                  - value
                  - type
                  - updatedAt
                nullable: true
              - type: string
                nullable: true
              - type: array
                items:
                  type: string
                nullable: true
        business:
          description: ID del Business al que pertenece el contacto
          type: string
          nullable: true
        notes:
          description: Notas registradas en el contacto
          type: array
          items:
            type: object
            properties:
              _id:
                type: string
                description: ID de la nota
              content:
                type: string
                description: Texto de la nota
              user:
                type: string
                description: ID del usuario que registró la nota
              createdAt:
                description: Fecha de creación de la nota
              updatedAt:
                description: Fecha de actualización de la nota
            required:
              - _id
              - content
              - user
        marketing_opt_out:
          description: >-
            Opt-out de marketing por medio de comunicación (`whatsapp`, `email`,
            `linkedin`). Ausente o vacío cuando el contacto no pidió salir de
            ningún medio.
          type: object
          additionalProperties:
            type: object
            properties:
              value:
                type: boolean
                description: true cuando el contacto salió del marketing en este medio
              source:
                type: string
                description: Origen del registro de opt-out
              at:
                description: Fecha del registro
            required:
              - value
              - source
              - at
        whatsapp_user_id:
          description: >-
            Identificador del contacto en WhatsApp (BSUID), asignado por Meta.
            String opaco.
          type: string
        whatsapp_username:
          description: '@username del contacto en WhatsApp, tal como lo envía Meta'
          type: string
        whatsapp_parent_user_id:
          description: parent_user_id del contacto en WhatsApp, tal como lo envía Meta
          type: string
        company:
          description: Empresa del contacto
        createdAt:
          description: Fecha de creación del contacto
        updatedAt:
          description: Fecha de actualización del contacto
        created:
          type: boolean
          description: >-
            true cuando el contacto se creó (201); false cuando se reutilizó
            (200).
        matched_by:
          description: Identificador que coincidió
          type: string
          enum:
            - email
            - linkedin
            - phone
          nullable: true
        matches_count:
          type: number
          description: >-
            Cuántos contactos de la empresa coincidieron por la clave
            `matched_by` (0 al crear). Por encima de 1, la empresa tiene
            contactos duplicados y se reutilizó el más antiguo.
        ignored_fields:
          type: array
          items:
            type: string
          description: >-
            Campos enviados que NO se guardaron porque el contacto existente ya
            tenía otro valor (solo cuando se reutiliza por correo electrónico o
            LinkedIn). Ej.: `email`, `phone_number`, `job_title`,
            `additional_attributes.<slug>`. Vacío al crear y cuando se encuentra
            por teléfono.
      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` en la creación;
            `CONTACT_IDENTIFIER_TAKEN` en la edición.
        message:
          type: string
          description: Mensaje para mostrar al usuario
        conflicts:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
                enum:
                  - email
                  - linkedin
                  - phone
                description: Identificador que coincidió
              contact:
                type: object
                properties:
                  _id:
                    description: ID del contacto
                    type: string
                  name:
                    type: string
                    description: Nombre del contacto
                  email:
                    description: Email del contacto
                    type: string
                  phone_number:
                    description: Teléfono del contacto
                    type: string
                  country_code:
                    description: Código de país del teléfono
                    type: string
                  linkedin_identifier:
                    description: Identificador de LinkedIn del contacto
                    type: string
                required:
                  - _id
            required:
              - field
              - contact
          description: Contacto que ya tiene cada identificador en conflicto
      required:
        - statusCode
        - code
        - message
        - conflicts
  securitySchemes:
    JWT-auth:
      scheme: bearer
      bearerFormat: JWT
      type: http
      description: Token JWT de autenticación

````

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