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

# Importando contatos

> Traga seus usuários de um CSV — e o que uma importação não finge saber.

Você já tem usuários. Trazê-los para cá é um upload, e esta página fala
principalmente das duas coisas que a importação se recusa a fazer no seu lugar,
porque as duas são coisas que você não desfaz depois.

## O formato

```bash theme={null}
curl -X POST "$API/v1/organization/contact-imports?environment=live&filename=incumbente.csv" \
  -H "Authorization: Bearer $UK_SESSION" \
  -H "X-Organization-Id: $ORG" \
  -H "Content-Type: text/csv" \
  --data-binary @incumbente.csv
```

```json theme={null}
{
  "import": {
    "id": "8c1f…",
    "filename": "incumbente.csv",
    "columns": ["email", "name", "external_id", "customer"],
    "status": "pending",
    "total": 4312,
    "pending": 4312,
    "created": 0,
    "matched": 0,
    "conflicts": 0,
    "failed": 0
  }
}
```

A resposta é **202**, não 201: o arquivo foi aceito e está durável, e nada dele
foi aplicado ainda. Um worker aplica as linhas em lotes; consulte
`GET /v1/organization/contact-imports/{id}` e os números vão se preenchendo.

`environment` é **obrigatório** aqui. Todas as outras leituras desta superfície
assumem `live` por padrão, e esta se recusa a assumir — ler o ambiente errado é
um clique perdido, e dez mil contatos escritos no ambiente errado não se
desescrevem.

O painel faz a mesma coisa em **Contatos → Importar**, e nomeia o ambiente na
confirmação pelo mesmo motivo.

## Colunas

`email` e `external_id` são os identificadores. Seu arquivo precisa de pelo menos
uma das duas colunas; sem nenhuma, toda linha falharia pelo mesmo motivo, então o
arquivo é recusado em vez de virar um relatório de falhas idênticas.

| Coluna                                                                                            | Lida como                                   |
| ------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| `email`, `email_address`, `e-mail`                                                                | O endereço, como identidade                 |
| `external_id`, `id`, `user_id`                                                                    | Sua própria chave primária para essa pessoa |
| `name`, `full_name`, `nome`                                                                       | O nome                                      |
| `customer`, `company`, `team`, `account`                                                          | O time a que a pessoa pertence              |
| `customer_role`                                                                                   | `owner` ou `member`                         |
| `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`, `referrer`, `landing_page` | Atribuição de primeiro toque                |

O casamento ignora maiúsculas e lê espaços, hífens e pontos como underscores, de
modo que `First Name` e `first_name` são a mesma coluna. O que a tabela não nomeia
é ignorado — um export de verdade traz uma dúzia de colunas que aqui não têm onde
morar.

`id` vira `external_id` de propósito: é como a maioria dos provedores chama a
própria chave primária, e daqui é exatamente isso que ela é.

## O que uma linha faz

**Ninguém com essa identidade ainda** — o contato é criado com tudo que a linha
trouxe, atribuição de primeiro toque incluída. Vale notar: este é o único momento
em que um contato nasce muito depois da visita, então o arquivo pode dizer de onde
a pessoa veio em vez de inventarmos "csv" para ela.

**Alguém que já está aqui** — a linha **casa** com essa pessoa e não mexe no
perfil dela. O export do seu provedor anterior não é evidência sobre um contato
que este ambiente já tem: o nome e o endereço dele são para onde vão os avisos de
segurança dele, e uma linha desatualizada não é motivo para redirecioná-los. Se a
linha trouxer um identificador livre — um `external_id` para quem só conhecíamos
por e-mail —, essa aresta é anexada.

**Uma linha que nomeia duas pessoas diferentes** — reportada como `conflict`, e
nada é escrito por ela.

Essa última é a decisão em torno da qual o recurso inteiro foi construído, então
merece um título.

## Uma colisão é reportada, nunca resolvida

Tipos identificadores nunca reapontam. Um `email` ou um `external_id` que já
pertence a outro contato é uma **duplicata para fundir à mão**, e não existe
desfazer fusão — o registro da fusão existe justamente para que uma fusão errada
seja consertada à mão, e não por restore.

Uma importação de dez mil linhas é a maneira mais rápida já inventada de criar
fusões erradas em lote. Então ela não cria nenhuma. Quando o `email` de uma linha
pertence a um contato e o `external_id` pertence a outro, a linha cai no relatório
como `conflict` nomeando **os dois** ids, e nada é escrito:

```bash theme={null}
curl -s "$API/v1/organization/contact-imports/$ID?environment=live&outcome=conflict" \
  -H "Authorization: Bearer $UK_SESSION" -H "X-Organization-Id: $ORG"
```

```json theme={null}
{
  "rows": [
    {
      "line": 812,
      "email": "grace@example.com",
      "external_id": "user_8421",
      "status": "conflict",
      "error_code": "identity_conflict",
      "error": "the email and the external_id on this line belong to two different contacts — merge them by hand and import the line again",
      "contact_id": "3f9a…",
      "conflicting_contact_id": "b711…"
    }
  ]
}
```

Os dois ids estão ali porque o conserto exige dois: passe-os para
`POST /v1/organization/contacts/{id}/merge` — `{id}` é a linha que desaparece,
`target_id` no corpo é a sobrevivente — e importe aquela linha de novo. Um
relatório nomeando só um do par seria um relatório sobre o qual ninguém consegue
agir.

## Uma importação não prova um endereço

`email_verified` num contato quer dizer que alguém provou que lê e-mail naquele
endereço: mandamos algo para lá e a pessoa voltou. Um arquivo não produz isso.

Então **todo contato importado chega com `email_verified: false`**, e não existe
parâmetro, header ou coluna que mude isso. Uma coluna `email_verified` no CSV é
recusada pelo nome, com um `422` explicando o porquê — recusada em vez de
ignorada, porque você a colocou ali por um motivo e o silêncio deixaria você
acreditar que conseguiu.

Isso importa mais do que parece, porque `email_verified` é o que a claim de um
[JWT de sessão](/pt-br/customer-auth/session-tokens) carrega e o que o guard
`<Verified>` do SDK lê. No momento em que uma importação pudesse definir esse
campo, os dois passariam a ser afirmações sobre uma planilha.

O que deixa a pergunta honesta: **o que acontece com as pessoas que você acabou de
importar?**

* Elas estão **identificadas**. O `boot` acha, o login acha, o `<SignedIn>` vê, e
  elas aparecem na sua lista de contatos. Não estar verificado não é ser
  desconhecido.
* O primeiro [link mágico ou código por e-mail](/pt-br/customer-auth/hosted)
  enviado àquele endereço o **prova** e liga o marco — de forma permanente, no
  primeiro uso. Esses dois fluxos existem exatamente para isso, que é também o
  motivo de funcionarem nos dois modos. Um único "entrar com um link" é todo o seu
  backfill de verificação, uma pessoa por vez, pago pelas pessoas que de fato
  voltam.
* No [modo federado](/pt-br/customer-auth/federated), `email_verified` não está no
  caminho: o HMAC prova o `external_id`, e o endereço é um atributo. Se o seu
  produto mantém o próprio login, importar não muda nada sobre quem entra.
* Se hoje o `<Verified>` protege algo que os seus importados precisam no primeiro
  dia, proteja com `<SignedIn>` e coloque a prova de endereço onde ela pertence —
  na frente das telas onde o dado de outra pessoa poderia vazar. É essa a
  fronteira para a qual o `<Verified>` foi desenhado.

Senhas seguem a mesma postura: `password` e `password_hash` são recusadas pelo
nome. Migrar credencial é um projeto próprio, e derrubar a coluna em silêncio é
como uma migração descobre no dia do lançamento que ninguém consegue entrar.

## Nada é enviado por e-mail

Nenhuma mensagem, de nenhum tipo, para ninguém do arquivo. Uma importação é a
maneira mais rápida de mandar dez mil e-mails para endereços que nunca ouviram
falar de você, e um desses acaba com a reputação de um domínio de envio — o seu e
o de todos os outros na plataforma. O relatório é onde o resultado mora.

Se você quer que as pessoas importadas ouçam de você, isso é um ato deliberado que
você faz depois, no ritmo que o seu domínio de envio aguenta.

## Linhas ruins são reportadas, não fatais

Uma linha ilegível não interrompe a importação. Parar na linha 4.312 é inútil para
quem está migrando; ignorar em silêncio é pior, porque você terminaria acreditando
que todo mundo chegou.

```bash theme={null}
curl -s "$API/v1/organization/contact-imports/$ID?environment=live&outcome=failed" \
  -H "Authorization: Bearer $UK_SESSION" -H "X-Organization-Id: $ORG"
```

| `error_code`                     | A linha                                         |
| -------------------------------- | ----------------------------------------------- |
| `no_identifier`                  | Não traz e-mail nem external\_id                |
| `invalid_email`                  | Tem um endereço que não é um endereço           |
| `invalid_customer_role`          | Nomeia um papel que não é `owner` nem `member`  |
| `customer_role_without_customer` | Nomeia um papel em time nenhum                  |
| `malformed_row`                  | Não bate com o próprio cabeçalho                |
| `identity_conflict`              | Nomeia dois contatos existentes diferentes      |
| `apply_failed`                   | Não pôde ser aplicada depois de três tentativas |

Tudo, exceto `identity_conflict` e `apply_failed`, é decidido enquanto o arquivo é
lido — por isso as falhas já estão no relatório quando o upload responde.

## Times

Uma coluna `customer` coloca pessoas em times. O nome é casado dentro do ambiente
— é a única alça que uma planilha tem sobre um time — e um time que não existe é
criado. O mais antigo ganha quando dois times têm o mesmo nome, para que importar
o mesmo arquivo duas vezes convirja num time em vez de bifurcar outro.

A primeira pessoa a entrar num time **recém-criado** vira `owner`, a menos que o
arquivo diga outra coisa: um time precisa ter um, e o arquivo oferece exatamente
um candidato. Quem já está num time mantém o papel que já tem — um arquivo pode
colocar uma pessoa num time, não pode mudar o papel dela.

## Rodar duas vezes é seguro

Nada deduplica pelos bytes do arquivo, então subir o mesmo CSV duas vezes produz
duas importações. E não produz uma segunda audiência: cada linha casa com o
contato que a primeira execução criou, então o segundo relatório lê `matched` de
cima a baixo.

Essa mesma propriedade é o que faz uma importação interrompida ser retomável em
vez de reiniciada. O progresso vive nas linhas, não numa mensagem — um worker que
morre no meio do arquivo deixa as linhas aplicadas terminais e o resto pendente, e
a passada seguinte continua de onde parou.

## Tamanho

Um upload carrega até **50.000 linhas** ou **8 MiB**, o que vier primeiro. Passando
de qualquer um dos dois o arquivo é recusado inteiro com `413`, e este é o único
lugar em que recusar o arquivo em vez da linha está certo: não há nada a relatar
sobre uma linha que nunca foi lida. Divida e importe as partes.

## Eventos

Cada contato que a importação cria publica `contact.identified`, o mesmo fato que
todas as outras portas publicam — então suas automações não precisam saber que
houve uma importação.

Quando a última linha fica terminal, a importação publica
`contact_import.completed` uma vez, com os números. Se você tem um
[endpoint de webhook](/pt-br/guides/webhooks) e quer o tiro de partida sem dez mil
fatos individuais no dia da migração, inscreva-o nesse tipo e não em
`contact.identified`.
