Entrar Criar conta
Audiência

Importação de contatos

Importe contatos de um CSV, com mapeamento de colunas e progresso.

Traga contatos de uma planilha: envie um arquivo CSV e diga qual coluna corresponde a cada campo. A importação roda em segundo plano, em lotes, e você acompanha o progresso. Cada time pode ter até 3 importações em andamento.

Antes de enviar para uma lista nova, vale conferir os endereços com a validação de e-mails: ela aceita o mesmo CSV (source=csv) e, depois da importação, também o segmento ou todos os contatos.

#O arquivo

  • CSV de até 20 MB, com uma linha de cabeçalho. O separador (vírgula, ponto e vírgula ou tabulação) é detectado sozinho, e o arquivo pode estar em UTF-8 (com ou sem BOM) ou Windows-1252, como os exportados pelo Excel.
  • Os contatos são encontrados pelo e-mail: os que já existem são atualizados, os novos são criados. Células vazias nunca apagam um valor que já existe.
  • Um e-mail repetido no arquivo é ignorado depois da primeira vez. Linhas com problemas (e-mail inválido, valor que não combina com o tipo da propriedade) são puladas e aparecem em errors (até 20).
  • Endereços que estão na lista de supressão do time não viram contatos: a linha é ignorada (nem cria nem atualiza), conta em skipped e em skipped_suppressed, aparece em errors com reason: "suppressed" e não usa o limite de contatos do plano.
  • Se o limite de contatos do time for atingido (o do plano mais o dos pacotes do Adicional de contatos, quando há), a importação para, marca limit_reached e explica em error. Com Continuar adicionando contatos acima do limite do plano ligado em Cobrança (plano que permite excedente), a importação segue e os contatos extras são cobrados por pacote de 1.000.
  • Com webhooks de contact.created ou contact.updated, cada contato importado gera um evento.
  • Uma coluna mapeada em tags acrescenta etiquetas (nomes separados por vírgula ou ponto e vírgula; as que não existem são criadas). Uma coluna mapeada em external_id grava o ID da pessoa no seu sistema; a linha cujo external_id já é de outro contato é pulada.
  • As automações não começam para os contatos importados, a menos que a importação peça com trigger_automations: true. Aí cada contato novo inicia as automações de contato criado, quem entrou no segmento da importação inicia as de segmento adicionado, e cada etiqueta nova inicia as de etiqueta adicionada, em lotes espaçados para não inundar a fila. automations_triggered conta quantas foram iniciadas.
customers.csv
email;name;last_name;plan;customer_since
[email protected];Ana;Souza;pro;18/03/2024
[email protected];Bruno;Lima;free;02/11/2025

#Importar um CSV

POST/contacts/imports

Chave de acesso total

Envie como multipart/form-data. A Postal confere na hora se as colunas do mapping existem no cabeçalho, guarda o arquivo e coloca a importação na fila. Acompanhe com GET /contacts/imports/{id}.

Corpo da requisição (multipart/form-data)

  • filearquivoobrigatório
    O CSV, até 20 MB.
  • mappingobject (JSON)obrigatório
    Qual coluna do cabeçalho vai para cada campo. Pode ser enviado como texto JSON.
    • emailstringobrigatório
      Coluna dos e-mails.
    • first_namestring
      Coluna do nome.
    • last_namestring
      Coluna do sobrenome.
    • external_idstring
      Coluna com o ID da pessoa no seu sistema.
    • tagsstring
      Coluna com nomes de etiquetas, separados por vírgula ou ponto e vírgula.
    • unsubscribedstring
      Coluna que indica descadastro (valores como sim/não, true/false).
    • propertiesobject
      Até 100 pares chave da propriedade → coluna. As propriedades precisam existir.
  • consent_sourcestringobrigatório
    De onde vieram os contatos: website_form (formulário do site), product_signup (clientes ou usuários do seu produto), in_person (evento ou atendimento presencial) ou other.
  • consent_notestring
    Conte de onde vieram, até 500 caracteres. Obrigatório quando consent_source é other.
  • consent_confirmedbooleanobrigatório
    Precisa ser true: confirma que os contatos pediram para receber seus e-mails e que a lista não foi comprada, alugada nem coletada da internet. A Postal guarda a confirmação com a data e o IP.
  • segment_idstring
    Segmento estático onde colocar todos os contatos importados.
  • topicsobject[] (JSON)
    Escolhas de tópicos para todos os contatos importados: [{"id": "…", "subscription": "opt_in"}].
  • trigger_automationsboolean
    Inicia as automações que os contatos importados iniciariam se fossem criados pela API. Padrão: false.

Erros

  • 422validation_error
    Arquivo ausente, grande demais ou que não é CSV; arquivo vazio; coluna do mapping que não está no cabeçalho; propriedade desconhecida; consent_source ou consent_confirmed ausente.
  • 422validation_error
    O segmento é dinâmico: só segmentos estáticos recebem contatos importados.
  • 404not_found
    O segmento ou algum tópico não existe.
  • 429rate_limit_exceeded
    Já há 3 importações em andamento.
POST /contacts/imports

cURL

curl -X POST https://api.postal.so/contacts/imports \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -F "[email protected]" \
  -F 'mapping={"email": "email", "first_name": "name", "last_name": "last_name", "properties": {"plan": "plan", "customer_since": "customer_since"}}' \
  -F "consent_source=website_form"   -F "consent_confirmed=true"   -F "segment_id=019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a" \
  -F 'topics=[{"id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b", "subscription": "opt_in"}]'

Node.js

import { readFile } from 'node:fs/promises';

const form = new FormData();
form.append('file', new Blob([await readFile('customers.csv')]), 'customers.csv');
form.append('mapping', JSON.stringify({
  email: 'email',
  first_name: 'name',
  last_name: 'last_name',
  properties: { plan: 'plan', customer_since: 'customer_since' },
}));
form.append('consent_source', 'website_form');
form.append('consent_confirmed', 'true');
form.append('segment_id', '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a');

const response = await fetch('https://api.postal.so/contacts/imports', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.POSTAL_API_KEY}` },
  body: form,
});

const contactImport = await response.json();

Python

import json
import os
import requests

with open("customers.csv", "rb") as csv_file:
    response = requests.post(
        "https://api.postal.so/contacts/imports",
        headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
        files={"file": ("customers.csv", csv_file, "text/csv")},
        data={
            "mapping": json.dumps({
                "email": "email",
                "first_name": "name",
                "last_name": "last_name",
                "properties": {"plan": "plan", "customer_since": "customer_since"},
            }),
            "consent_source": "website_form",
            "consent_confirmed": "true",
            "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
        },
        timeout=60,
    )

contact_import = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->post('contacts/imports', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'multipart' => [
        ['name' => 'file', 'contents' => fopen('customers.csv', 'r'), 'filename' => 'customers.csv'],
        ['name' => 'mapping', 'contents' => json_encode([
            'email' => 'email',
            'first_name' => 'name',
            'last_name' => 'last_name',
            'properties' => ['plan' => 'plan', 'customer_since' => 'customer_since'],
        ])],
        ['name' => 'consent_source', 'contents' => 'website_form'],
        ['name' => 'consent_confirmed', 'contents' => 'true'],
        ['name' => 'segment_id', 'contents' => '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a'],
    ],
]);

$contactImport = json_decode($response->getBody(), true);
201Resposta
{
  "object": "contact_import",
  "id": "019a1e10-6e7f-7a90-9b0c-1d2e3f4a5b6c",
  "filename": "customers.csv",
  "status": "pending",
  "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
  "total_rows": null,
  "rows": 0,
  "created": 0,
  "updated": 0,
  "skipped": 0,
  "skipped_suppressed": 0,
  "errors": [],
  "limit_reached": false,
  "trigger_automations": false,
  "automations_triggered": 0,
  "consent_source": "website_form",
  "consent_note": null,
  "consent_confirmed_at": "2026-10-24T14:10:00.000000Z",
  "error": null,
  "created_at": "2026-10-24T14:10:00.000000Z",
  "started_at": null,
  "completed_at": null
}

#Listar importações

GET/contacts/imports

Chave de acesso total

Parâmetros de consulta

  • limitinteger
    De 1 a 100. Padrão: 20.
  • afterstring
    Cursor da próxima página.
  • beforestring
    Cursor da página anterior.
GET /contacts/imports

cURL

curl https://api.postal.so/contacts/imports \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/contacts/imports', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/contacts/imports",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('contacts/imports', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "contact_import",
      "id": "019a1e10-6e7f-7a90-9b0c-1d2e3f4a5b6c",
      "filename": "customers.csv",
      "status": "completed",
      "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
      "total_rows": 1500,
      "rows": 1500,
      "created": 1320,
      "updated": 172,
      "skipped": 8,
      "skipped_suppressed": 3,
      "errors": [
        {
          "row": 37,
          "email": "joao@exemplo",
          "message": "The email is not valid."
        },
        {
          "row": 52,
          "email": "[email protected]",
          "message": "The address is in your suppression list.",
          "reason": "suppressed"
        }
      ],
      "limit_reached": false,
      "trigger_automations": false,
      "automations_triggered": 0,
      "consent_source": "website_form",
      "consent_note": null,
      "consent_confirmed_at": "2026-10-24T14:10:00.000000Z",
      "error": null,
      "created_at": "2026-10-24T14:10:00.000000Z",
      "started_at": "2026-10-24T14:10:01.000000Z",
      "completed_at": "2026-10-24T14:10:19.000000Z"
    }
  ]
}

#Obter uma importação

GET/contacts/imports/{id}

Chave de acesso total

Mostra o progresso: rows linhas lidas de total_rows, quantos contatos foram criados (created), atualizados (updated) e pulados (skipped, dos quais skipped_suppressed são endereços da lista de supressão), e os primeiros erros por linha. status: pending, processing, completed ou failed (com error).

Parâmetros de caminho

  • idstringobrigatório
    ID da importação.

Erros

  • 404not_found
    O recurso não existe ou é de outro time.
GET /contacts/imports/{id}

cURL

curl https://api.postal.so/contacts/imports/019a1e10-6e7f-7a90-9b0c-1d2e3f4a5b6c \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/contacts/imports/019a1e10-6e7f-7a90-9b0c-1d2e3f4a5b6c', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/contacts/imports/019a1e10-6e7f-7a90-9b0c-1d2e3f4a5b6c",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('contacts/imports/019a1e10-6e7f-7a90-9b0c-1d2e3f4a5b6c', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "contact_import",
  "id": "019a1e10-6e7f-7a90-9b0c-1d2e3f4a5b6c",
  "filename": "customers.csv",
  "status": "completed",
  "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
  "total_rows": 1500,
  "rows": 1500,
  "created": 1320,
  "updated": 172,
  "skipped": 8,
  "skipped_suppressed": 3,
  "errors": [
    {
      "row": 37,
      "email": "joao@exemplo",
      "message": "The email is not valid."
    },
    {
      "row": 52,
      "email": "[email protected]",
      "message": "The address is in your suppression list.",
      "reason": "suppressed"
    }
  ],
  "limit_reached": false,
  "trigger_automations": false,
  "automations_triggered": 0,
  "consent_source": "website_form",
  "consent_note": null,
  "consent_confirmed_at": "2026-10-24T14:10:00.000000Z",
  "error": null,
  "created_at": "2026-10-24T14:10:00.000000Z",
  "started_at": "2026-10-24T14:10:01.000000Z",
  "completed_at": "2026-10-24T14:10:19.000000Z"
}