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_reachede explica emerror. - Com webhooks de
contact.createdoucontact.updated, cada contato importado gera um evento.
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
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órioO CSV, até 20 MB.mappingobject (JSON)obrigatórioQual coluna do cabeçalho vai para cada campo. Pode ser enviado como texto JSON.emailstringobrigatórioColuna dos e-mails.first_namestringColuna do nome.last_namestringColuna do sobrenome.unsubscribedstringColuna que indica descadastro (valores comosim/não,true/false).propertiesobjectAté 100 pareschave da propriedade → coluna. As propriedades precisam existir.
segment_idstringSegmento onde colocar todos os contatos importados.topicsobject[] (JSON)Escolhas de tópicos para todos os contatos importados:[{"id": "…", "subscription": "opt_in"}].
Erros
- 422
validation_errorArquivo ausente, grande demais ou que não é CSV; arquivo vazio; coluna domappingque não está no cabeçalho; propriedade desconhecida. - 404
not_foundO segmento ou algum tópico não existe. - 429
rate_limit_exceededJá há 3 importações em andamento.
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);
{
"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
Parâmetros de consulta
limitintegerDe 1 a 100. Padrão: 20.afterstringCursor da próxima página.beforestringCursor da página anterior.
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);
{
"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}
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órioID da importação.
Erros
- 404
not_foundO recurso não existe ou é de outro time.
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);
{
"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"
}