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

# Criar contato (ou reaproveitar o existente)

> Cria um contato ou, se a empresa já tem um contato com algum dos identificadores enviados, reaproveita esse contato em vez de criar outro.

- **Identificadores:** `phone_number` (com `country_code`), `email` e `linkedin_identifier`. Além do `name`, é obrigatório pelo menos um deles; sem nenhum, 400 `"Informe pelo menos um identificador: telefone, e-mail ou LinkedIn."`. O contato pode nascer sem telefone.
- **Ordem de procura:** e-mail, LinkedIn e telefone. E-mail só conta com `@` e é reconhecido na forma normalizada (sem espaços nas pontas, em minúsculas), que é como passa a ser gravado; e-mails antigos gravados com maiúscula só são reconhecidos com a mesma grafia, até a normalização da base. LinkedIn aceita URL do perfil ou só o nome. Telefone é normalizado (DDD, 9º dígito).
- **201:** contato criado. O corpo é o contato com `created: true`, `matched_by: null`, `matches_count: 0` e `ignored_fields: []`.
- **200:** contato existente reaproveitado. O corpo é o contato com `created: false`, `matched_by` (`email`, `linkedin` ou `phone`) e `matches_count` (quantos contatos da empresa casaram por essa chave; acima de 1, há duplicados e o mais antigo foi usado). Achado pelo telefone, os campos enviados são atualizados, como o POST sempre fez; achado por e-mail ou LinkedIn, só os campos que estavam vazios são preenchidos, e `ignored_fields` lista os enviados que não foram gravados porque o contato já tinha outro valor (ex.: `email`, `phone_number`, `job_title`, `additional_attributes.<slug>`). `name` e o dono nunca mudam. Para alterar qualquer campo de um contato existente, use `PUT /v1/contacts/{id}`.
- **409 `CONTACT_IDENTITY_CONFLICT`:** os identificadores enviados pertencem a contatos diferentes (ex.: o telefone é de um contato e o e-mail, de outro). Nada é gravado; `conflicts` traz o contato de cada identificador.
- **Campos de perfil:** `job_title`, `seniority`, `department`, `country`, `region`, `city` e `experience` são gravados na criação; num contato existente, seguem a regra do 200 (atualizados se achado pelo telefone, só preenchidos se vazios quando achado por e-mail ou LinkedIn).
- **Telefone:** só dígitos, DDD + número, sem o DDI (o DDI vai em `country_code`, obrigatório junto com o telefone). Telefone inválido retorna 400.
- **additional_attributes:** `{ "slug": valor }`, com valor primitivo (string, number, boolean ou null). Slug inexistente, ou de custom field com contexto diferente de CONTACT, é ignorado em silêncio. Num contato existente, merge por slug: achado pelo telefone, os slugs enviados são atualizados; achado por e-mail ou LinkedIn, só os vazios são preenchidos.
- **Conferir antes:** `POST /v1/contacts/identity-check` diz o que esta rota faria, sem gravar.



## OpenAPI

````yaml https://api.nuvia.ai/api/docs/user-json post /v1/contacts
openapi: 3.0.0
info:
  title: Nuvia API - Empresa
  description: Documentação da 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: Criar contato (ou reaproveitar o existente)
      description: >-
        Cria um contato ou, se a empresa já tem um contato com algum dos
        identificadores enviados, reaproveita esse contato em vez de criar
        outro.


        - **Identificadores:** `phone_number` (com `country_code`), `email` e
        `linkedin_identifier`. Além do `name`, é obrigatório pelo menos um
        deles; sem nenhum, 400 `"Informe pelo menos um identificador: telefone,
        e-mail ou LinkedIn."`. O contato pode nascer sem telefone.

        - **Ordem de procura:** e-mail, LinkedIn e telefone. E-mail só conta com
        `@` e é reconhecido na forma normalizada (sem espaços nas pontas, em
        minúsculas), que é como passa a ser gravado; e-mails antigos gravados
        com maiúscula só são reconhecidos com a mesma grafia, até a normalização
        da base. LinkedIn aceita URL do perfil ou só o nome. Telefone é
        normalizado (DDD, 9º dígito).

        - **201:** contato criado. O corpo é o contato com `created: true`,
        `matched_by: null`, `matches_count: 0` e `ignored_fields: []`.

        - **200:** contato existente reaproveitado. O corpo é o contato com
        `created: false`, `matched_by` (`email`, `linkedin` ou `phone`) e
        `matches_count` (quantos contatos da empresa casaram por essa chave;
        acima de 1, há duplicados e o mais antigo foi usado). Achado pelo
        telefone, os campos enviados são atualizados, como o POST sempre fez;
        achado por e-mail ou LinkedIn, só os campos que estavam vazios são
        preenchidos, e `ignored_fields` lista os enviados que não foram gravados
        porque o contato já tinha outro valor (ex.: `email`, `phone_number`,
        `job_title`, `additional_attributes.<slug>`). `name` e o dono nunca
        mudam. Para alterar qualquer campo de um contato existente, use `PUT
        /v1/contacts/{id}`.

        - **409 `CONTACT_IDENTITY_CONFLICT`:** os identificadores enviados
        pertencem a contatos diferentes (ex.: o telefone é de um contato e o
        e-mail, de outro). Nada é gravado; `conflicts` traz o contato de cada
        identificador.

        - **Campos de perfil:** `job_title`, `seniority`, `department`,
        `country`, `region`, `city` e `experience` são gravados na criação; num
        contato existente, seguem a regra do 200 (atualizados se achado pelo
        telefone, só preenchidos se vazios quando achado por e-mail ou
        LinkedIn).

        - **Telefone:** só dígitos, DDD + número, sem o DDI (o DDI vai em
        `country_code`, obrigatório junto com o telefone). Telefone inválido
        retorna 400.

        - **additional_attributes:** `{ "slug": valor }`, com valor primitivo
        (string, number, boolean ou null). Slug inexistente, ou de custom field
        com contexto diferente de CONTACT, é ignorado em silêncio. Num contato
        existente, merge por slug: achado pelo telefone, os slugs enviados são
        atualizados; achado por e-mail ou LinkedIn, só os vazios são
        preenchidos.

        - **Conferir antes:** `POST /v1/contacts/identity-check` diz o que esta
        rota faria, sem gravar.
      operationId: ContactController_createContact
      parameters:
        - name: x-company-id
          in: header
          description: >-
            Identificador da empresa-alvo. Obrigatório apenas para API Keys
            globais (type=global). Ignorado para API Keys de empresa e usuários
            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: >-
            Contato existente reaproveitado (`created: false`). Achado pelo
            telefone, os campos enviados foram atualizados; por e-mail ou
            LinkedIn, só os vazios foram preenchidos e `ignored_fields` lista o
            que não foi gravado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateContactResponseDto'
        '201':
          description: 'Contato criado (`created: true`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateContactResponseDto'
        '400':
          description: >-
            Sem identificador (`"Informe pelo menos um identificador: telefone,
            e-mail ou LinkedIn."`), telefone sem `country_code` ou telefone
            inválido
        '401':
          description: Token inválido ou expirado
        '403':
          description: Sem permissão para acessar este recurso
        '409':
          description: >-
            Conflito (`CONTACT_IDENTITY_CONFLICT`): os identificadores
            informados pertencem a contatos diferentes. Nada é gravado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactIdentityErrorResponseDto'
      security:
        - JWT-auth: []
components:
  schemas:
    CreateContactDto:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          description: >-
            Nome completo do contato. Campo obrigatório. Se o contato já existe,
            o nome dele é preservado.
        country_code:
          type: string
          description: >-
            Código DDI do país (1-3 dígitos). Exemplos: 55 (Brasil), 1 (EUA),
            351 (Portugal). Obrigatório quando phone_number é enviado; sem
            telefone, não envie.
          minLength: 1
          maxLength: 3
          pattern: ^\d{1,3}$
        email:
          type: string
          description: >-
            Endereço de e-mail do contato. Identifica o contato pela forma
            normalizada (sem espaços nas pontas, em minúsculas), que é como é
            gravado. E-mails antigos gravados com maiúscula só são reconhecidos
            com a mesma grafia, até a normalização da base. Só conta como
            identificador se tiver "@". Num contato existente, é atualizado
            quando o contato é achado pelo telefone; achado por e-mail ou
            LinkedIn, só é preenchido se estiver vazio.
        linkedin_identifier:
          type: string
          description: >-
            Identificador do LinkedIn do contato: URL do perfil ou só o nome do
            perfil. Exemplo: joao-silva-123. Identifica o contato. Num contato
            existente, é atualizado quando o contato é achado pelo telefone;
            achado por e-mail ou LinkedIn, só é preenchido se estiver vazio.
        phone_number:
          type: string
          description: >-
            Número de telefone sem formatação, apenas dígitos (DDD + número, sem
            o DDI). Exemplo: 11999998888. Identifica o contato. Telefone que não
            forma um número válido com o country_code é recusado com 400.
            Opcional: o contato precisa de pelo menos um identificador entre
            telefone, e-mail e LinkedIn.
          pattern: ^\d*$
        job_title:
          type: string
          description: >-
            Cargo do contato. Exemplo: Gerente de Vendas. Gravado na criação;
            num contato existente, é atualizado quando o contato é achado pelo
            telefone; achado por e-mail ou LinkedIn, só é preenchido se estiver
            vazio.
        seniority:
          type: string
          description: >-
            Nível de senioridade do contato. Exemplo: Senior, Junior, Pleno.
            Gravado na criação; num contato existente, é atualizado quando o
            contato é achado pelo telefone; achado por e-mail ou LinkedIn, só é
            preenchido se estiver vazio.
        department:
          type: string
          description: >-
            Departamento do contato. Exemplo: Comercial, TI, Marketing. Gravado
            na criação; num contato existente, é atualizado quando o contato é
            achado pelo telefone; achado por e-mail ou LinkedIn, só é preenchido
            se estiver vazio.
        country:
          type: string
          description: >-
            País do contato. Exemplo: Brasil. Gravado na criação; num contato
            existente, é atualizado quando o contato é achado pelo telefone;
            achado por e-mail ou LinkedIn, só é preenchido se estiver vazio.
        region:
          type: string
          description: >-
            Estado ou região do contato. Exemplo: São Paulo. Gravado na criação;
            num contato existente, é atualizado quando o contato é achado pelo
            telefone; achado por e-mail ou LinkedIn, só é preenchido se estiver
            vazio.
        city:
          type: string
          description: >-
            Cidade do contato. Exemplo: São Paulo. Gravado na criação; num
            contato existente, é atualizado quando o contato é achado pelo
            telefone; achado por e-mail ou LinkedIn, só é preenchido se estiver
            vazio.
        experience:
          type: string
          description: >-
            Experiência profissional do contato. Gravado na criação; num contato
            existente, é atualizado quando o contato é achado pelo telefone;
            achado por e-mail ou LinkedIn, só é preenchido se estiver vazio.
        additional_attributes:
          type: object
          description: >-
            Campos personalizados do contato. Formato: { "slug_do_campo": valor
            }. Os slugs devem corresponder a custom fields cadastrados com
            contexto CONTACT. Slugs inválidos ou inexistentes são ignorados
            silenciosamente. Os valores devem ser primitivos (string, number,
            boolean ou null). Num contato existente achado pelo telefone, os
            slugs enviados são atualizados (merge); achado por e-mail ou
            LinkedIn, só os slugs vazios são preenchidos. Slugs não enviados
            ficam como estão. Exemplo: { "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 do contato
          type: string
        id:
          description: ID do contato (mesmo valor de `_id`)
          type: string
        name:
          type: string
          description: Nome do contato
        country_code:
          description: >-
            Código do país do número. Ausente junto com o telefone quando o
            contato só tem identificador do WhatsApp.
          type: string
        email:
          description: E-mail do contato
          type: string
        last_activity_at:
          description: Data da última atividade do contato
        phone_number:
          description: >-
            Número de telefone do contato. Ausente quando o contato só tem
            identificador do WhatsApp.
          type: string
        linkedin_identifier:
          description: Identificador do perfil do LinkedIn
          type: string
        job_title:
          description: Cargo do contato
          type: string
        seniority:
          description: Nível de senioridade do contato
          type: string
        department:
          description: Departamento do contato
          type: string
        country:
          description: País do contato
          type: string
        region:
          description: Estado ou região do contato
          type: string
        city:
          description: Cidade do contato
          type: string
        experience:
          description: Experiência profissional do contato
          type: string
        skills:
          description: Habilidades do contato
          type: string
        additional_attributes:
          description: >-
            Valores dos campos personalizados do contato, por slug. Gravado via
            `additional_attributes`, o valor vem no envelope `{ id, value, type,
            updatedAt }`. Gravado via `custom_fields` (PUT), vem cru: o
            option_id (select), a lista de option_id (multi_select) ou o texto
            (text).
          type: object
          additionalProperties:
            anyOf:
              - type: object
                properties:
                  id:
                    type: string
                    description: ID do custom field
                  value:
                    anyOf:
                      - type: string
                        nullable: true
                      - type: number
                        nullable: true
                      - type: boolean
                        nullable: true
                    description: Valor gravado
                  type:
                    type: string
                    description: 'Tipo do custom field (ex.: text, number, date)'
                  updatedAt:
                    description: Data da última gravação do valor
                required:
                  - id
                  - value
                  - type
                  - updatedAt
                nullable: true
              - type: string
                nullable: true
              - type: array
                items:
                  type: string
                nullable: true
        business:
          description: ID do Business ao qual o contato pertence
          type: string
          nullable: true
        notes:
          description: Notas registradas no contato
          type: array
          items:
            type: object
            properties:
              _id:
                type: string
                description: ID da nota
              content:
                type: string
                description: Texto da nota
              user:
                type: string
                description: ID do usuário que registrou a nota
              createdAt:
                description: Data de criação da nota
              updatedAt:
                description: Data de atualização da nota
            required:
              - _id
              - content
              - user
        marketing_opt_out:
          description: >-
            Opt-out de marketing por meio de comunicação (`whatsapp`, `email`,
            `linkedin`). Ausente ou vazio quando o contato não pediu para sair
            de nenhum meio.
          type: object
          additionalProperties:
            type: object
            properties:
              value:
                type: boolean
                description: true quando o contato saiu do marketing nesse meio
              source:
                type: string
                description: Origem do registro de opt-out
              at:
                description: Data do registro
            required:
              - value
              - source
              - at
        whatsapp_user_id:
          description: >-
            Identificador do contato no WhatsApp (BSUID), atribuído pela Meta.
            String opaca.
          type: string
        whatsapp_username:
          description: '@username do contato no WhatsApp, como a Meta envia'
          type: string
        whatsapp_parent_user_id:
          description: parent_user_id do contato no WhatsApp, como a Meta envia
          type: string
        company:
          description: Empresa do contato
        createdAt:
          description: Data de criação do contato
        updatedAt:
          description: Data de atualização do contato
        created:
          type: boolean
          description: >-
            true quando o contato foi criado (201); false quando foi
            reaproveitado (200).
        matched_by:
          description: Identificador que casou
          type: string
          enum:
            - email
            - linkedin
            - phone
          nullable: true
        matches_count:
          type: number
          description: >-
            Quantos contatos da empresa casaram pela chave `matched_by` (0 ao
            criar). Acima de 1, a empresa tem contatos duplicados e o mais
            antigo foi reaproveitado.
        ignored_fields:
          type: array
          items:
            type: string
          description: >-
            Campos enviados que NÃO foram gravados porque o contato existente já
            tinha outro valor (só quando reaproveitado por e-mail ou LinkedIn).
            Ex.: `email`, `phone_number`, `job_title`,
            `additional_attributes.<slug>`. Vazio ao criar e quando achado pelo
            telefone.
      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` na criação; `CONTACT_IDENTIFIER_TAKEN`
            na edição.
        message:
          type: string
          description: Mensagem para exibir ao usuário
        conflicts:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
                enum:
                  - email
                  - linkedin
                  - phone
                description: Identificador que casou
              contact:
                type: object
                properties:
                  _id:
                    description: ID do contato
                    type: string
                  name:
                    type: string
                    description: Nome do contato
                  email:
                    description: E-mail do contato
                    type: string
                  phone_number:
                    description: Telefone do contato
                    type: string
                  country_code:
                    description: DDI do telefone
                    type: string
                  linkedin_identifier:
                    description: Identificador do LinkedIn do contato
                    type: string
                required:
                  - _id
            required:
              - field
              - contact
          description: Contato que já tem cada identificador em conflito
      required:
        - statusCode
        - code
        - message
        - conflicts
  securitySchemes:
    JWT-auth:
      scheme: bearer
      bearerFormat: JWT
      type: http
      description: Token JWT de autenticação

````

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