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.

#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).
  • Se o limite de contatos do plano for atingido, a importação para, marca limit_reached e explica em error.
  • Com webhooks de contact.created ou contact.updated, cada contato importado gera um evento.
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.
    • unsubscribedstring
      Coluna que indica descadastro (valores como sim/não, true/false).
    • propertiesobject
      Até 100 pares chave da propriedade → coluna. As propriedades precisam existir.
  • segment_idstring
    Segmento onde colocar todos os contatos importados.
  • topicsobject[] (JSON)
    Escolhas de tópicos para todos os contatos importados: [{"id": "…", "subscription": "opt_in"}].

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.
  • 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 "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('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"},
            }),
            "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' => '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,
  "errors": [],
  "limit_reached": false,
  "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,
      "errors": [
        {
          "row": 37,
          "email": "joao@exemplo",
          "message": "The email is not valid."
        }
      ],
      "limit_reached": false,
      "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), 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,
  "errors": [
    {
      "row": 37,
      "email": "joao@exemplo",
      "message": "The email is not valid."
    }
  ],
  "limit_reached": false,
  "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"
}