Pular para o conteúdo

Importar clientes de um CSV

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.

  • Node.js 18 ou mais novo (o script usa fetch nativo e await no topo do módulo).
  • Uma chave de API com o escopo customers:write e o módulo clientes habilitado no contrato. Confira os dois em GET /v1/me.
  • Um CSV com uma coluna full_name e, em cada linha, pelo menos email ou phone preenchido. A criação de cliente exige o nome e um dos dois contatos.
Terminal window
export LIGGA_API_KEY='ligga_live_…'
export LIGGA_BASE='https://api.ligga.app/functions/v1/api'
  1. 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,notes
    Maria Silva,maria@exemplo.com,+5541999998888,Cliente desde 2021
    João Souza,joao@exemplo.com,+5541988887777,
  2. Confira a chave. Se scopes não trouxer customers:write (ou *:write) e modules não trouxer clientes, o import falha em todas as linhas:

    Terminal window
    curl "$LIGGA_BASE/v1/me" \
    -H "Authorization: Bearer $LIGGA_API_KEY"
  3. Salve o script como import.mjs, na mesma pasta do CSV. O código completo está na seção seguinte.

  4. 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.csv
    node import.mjs amostra.csv
  5. Rode o arquivo inteiro. Ao final, o script imprime quantos clientes entraram e lista as linhas que falharam, com o detail devolvido pela API:

    Terminal window
    node import.mjs customers.csv
  6. 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.

import.mjs
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}`));
}

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.

  • 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 use PATCH /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.