Importar clientes de um CSV
O que você vai construir
Seção intitulada “O que você vai construir”Um script em Node.js que lê um arquivo CSV com clientes vindos de um sistema
antigo e cria cada um deles na Ligga com POST /v1/customers.
O script trabalha com algumas requisições em paralelo, isola as falhas linha a linha e deriva a chave de idempotência do e-mail do cliente. Consequência prática: se ele parar no meio, você corrige o que falhou e roda o arquivo inteiro de novo — quem já entrou não entra duas vezes.
Pré-requisitos
Seção intitulada “Pré-requisitos”- Node.js 18 ou mais novo (o script usa
fetchnativo eawaitno topo do módulo). - Uma chave de API com o escopo
customers:writee o móduloclienteshabilitado no contrato. Confira os dois emGET /v1/me. - Um CSV com uma coluna
full_namee, em cada linha, pelo menosemailouphonepreenchido. A criação de cliente exige o nome e um dos dois contatos.
export LIGGA_API_KEY='ligga_live_…'export LIGGA_BASE='https://api.ligga.app/functions/v1/api'Passo a passo
Seção intitulada “Passo a passo”-
Monte o CSV. Os nomes das colunas viram nomes de campo no corpo da requisição, então use exatamente os nomes de campo do recurso Clientes:
customers.csv full_name,email,phone,notesMaria Silva,maria@exemplo.com,+5541999998888,Cliente desde 2021João Souza,joao@exemplo.com,+5541988887777, -
Confira a chave. Se
scopesnão trouxercustomers:write(ou*:write) emodulesnão trouxerclientes, o import falha em todas as linhas:Terminal window curl "$LIGGA_BASE/v1/me" \-H "Authorization: Bearer $LIGGA_API_KEY" -
Salve o script como
import.mjs, na mesma pasta do CSV. O código completo está na seção seguinte. -
Rode com um recorte pequeno primeiro. Separe as três primeiras linhas em um arquivo de teste, aponte o script para ele e confira no app se os clientes chegaram como você esperava:
Terminal window head -n 4 customers.csv > amostra.csvnode import.mjs amostra.csv -
Rode o arquivo inteiro. Ao final, o script imprime quantos clientes entraram e lista as linhas que falharam, com o
detaildevolvido pela API:Terminal window node import.mjs customers.csv -
Corrija e repita. Ajuste no CSV as linhas que falharam e rode o mesmo arquivo de novo. As linhas que já tinham dado certo são respondidas com o resultado guardado da primeira vez, sem criar um segundo cliente.
Código completo
Seção intitulada “Código completo”import { readFile } from 'node:fs/promises';import { createHash } from 'node:crypto';
const API_KEY = process.env.LIGGA_API_KEY;const BASE = process.env.LIGGA_BASE;const CONCURRENCY = 5;const FILE = process.argv[2] ?? './customers.csv';
async function post(path, body, key) { const res = await fetch(`${BASE}${path}`, { method: 'POST', headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': key, }, body: JSON.stringify(body), }); const data = await res.json().catch(() => null); return { ok: res.ok, status: res.status, data };}
function keyFor(customer) { return 'import-' + createHash('sha256').update(customer.email).digest('hex').slice(0, 32);}
function parseCsv(text) { const [header, ...rows] = text.trim().split('\n'); const cols = header.split(','); return rows.map(row => { const vals = row.split(','); return Object.fromEntries(cols.map((c, i) => [c, vals[i]?.trim() || null])); });}
async function importOne(customer) { const result = await post('/v1/customers', customer, keyFor(customer)); if (result.ok) { console.log(`OK ${customer.email} ${result.data.data.id}`); return { customer, ok: true }; } else { console.error(`FALHA ${customer.email}: HTTP ${result.status} ${result.data?.detail ?? ''}`); return { customer, ok: false, error: result.data }; }}
async function runWithConcurrency(items, fn, concurrency) { const queue = [...items]; const workers = Array.from({ length: concurrency }, async () => { const results = []; while (queue.length) results.push(await fn(queue.shift())); return results; }); return (await Promise.all(workers)).flat();}
const csv = await readFile(FILE, 'utf8');const customers = parseCsv(csv);console.log(`Importando ${customers.length} clientes de ${FILE}…`);
const results = await runWithConcurrency(customers, importOne, CONCURRENCY);const ok = results.filter(r => r.ok).length;const failed = results.filter(r => !r.ok);
console.log(`\nFim: ${ok}/${customers.length} criados, ${failed.length} com falha`);if (failed.length) { console.log('Linhas com falha:'); failed.forEach(f => console.log(` - ${f.customer.email}: ${f.error?.detail}`));}Decisões
Seção intitulada “Decisões”A chave de idempotência vem do e-mail, não de um sorteio. Cada linha do CSV
gera sempre o mesmo Idempotency-Key, então repetir o arquivo é seguro: a
segunda chamada com a mesma chave e o mesmo corpo devolve a resposta guardada
da primeira, com o cabeçalho Idempotent-Replay: true, em vez de criar outro
cliente. Se o seu legado não tem e-mail confiável, derive a chave do
identificador que ele usa como único.
Cinco requisições em paralelo. O que limita o import não é a concorrência, é
a cota de escrita da sua chave: no plano pro são 120 escritas por minuto,
além do teto de burst por segundo. Cinco trabalhadores em paralelo mantêm a
fila cheia sem disparar tudo de uma vez. Se aparecerem respostas 429, baixe
CONCURRENCY para 1 e leia o cabeçalho Retry-After. Os limites do seu
plano estão em GET /v1/me e em
Limites de requisições.
Cada linha falha sozinha. Um 422 validation em uma linha não interrompe as
outras. O relatório do fim traz o detail de cada falha, que é onde a API diz
qual campo recusou.
O leitor de CSV é proposital e simplório. Ele quebra o arquivo em vírgulas e
não entende campos entre aspas nem quebras de linha dentro de um valor. Se o
seu arquivo tiver esses casos, troque parseCsv por uma biblioteca de CSV — o
resto do script continua igual.
Quando não usar
Seção intitulada “Quando não usar”- O CSV vem de outro lugar toda semana. Um import pontual é script; uma entrada recorrente é integração. Nesse caso, prefira um fluxo agendado, como o da receita de fornecedores com n8n.
- Você quer atualizar clientes que já existem. Este script só cria. Para
atualizar, busque o cliente por
filter[email][eq]=…e usePATCH /v1/customers/{id}. - As linhas dependem umas das outras. O script trata cada cliente como independente. Se uma linha só faz sentido depois de outra, processe em série.
Veja também
Seção intitulada “Veja também”- Clientes (customers) — campos, filtros e regras de validação
- Idempotência — como a repetição segura funciona
- Limites de requisições — cotas por plano e o que fazer com
429 - Node.js — o mesmo cliente HTTP em outros cenários