Node.js
Node 20 ou mais recente já traz fetch embutido, então um cliente completo cabe
em umas trinta linhas, sem axios nem node-fetch. Esta página monta esse
cliente e usa ele em todos os exemplos seguintes.
Configuração
Seção intitulada “Configuração”export LIGGA_API_KEY='ligga_live_…'export LIGGA_BASE='https://api.ligga.app/functions/v1/api'O módulo abaixo concentra autenticação, montagem de URL e tradução do corpo de erro em exceção.
import { randomUUID } from 'node:crypto';
const API_KEY = process.env.LIGGA_API_KEY;const BASE = process.env.LIGGA_BASE ?? 'https://api.ligga.app/functions/v1/api';
export class LiggaError extends Error { constructor(status, problem) { super(`HTTP ${status}: ${problem?.title ?? 'erro sem corpo'}`); this.name = 'LiggaError'; this.status = status; this.problem = problem ?? {}; }}
export async function api(path, { method = 'GET', body, idempotencyKey } = {}) { const headers = { Authorization: `Bearer ${API_KEY}` }; if (body !== undefined) headers['Content-Type'] = 'application/json'; if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
const res = await fetch(`${BASE}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), });
if (res.status === 204) return null;
const data = await res.json().catch(() => null); if (!res.ok) throw new LiggaError(res.status, data); return data;}
export const novaChaveIdempotente = () => randomUUID();Primeira requisição
Seção intitulada “Primeira requisição”GET /v1/me descreve a chave que fez a chamada e confirma que a configuração
está correta.
import { api } from './ligga.mjs';
const me = await api('/v1/me');
console.log(me.data.team_name, me.data.scopes);{ "data": { "team_id": "bca09e2f-41ab-4417-b84b-f2ff50f991ca", "team_name": "Pet Shop Aurora", "scopes": ["*:read", "sales:write"], "plan_code": "pro" }, "meta": { "request_id": "req_01JZXQ8V2K5M7N9P0R1S2T3U4V" }}Listar com paginação
Seção intitulada “Listar com paginação”Uma listagem devolve os itens em data e o estado da paginação em
pagination. O cursor da próxima página está em pagination.next_cursor e vem
null quando acabou.
import { api } from './ligga.mjs';
export async function* listarClientes(filtros = {}) { let cursor = null;
do { const query = new URLSearchParams({ limit: '100', ...filtros, ...(cursor ? { cursor } : {}), });
const pagina = await api(`/v1/customers?${query}`); yield* pagina.data;
cursor = pagina.pagination.next_cursor; } while (cursor);}
for await (const cliente of listarClientes({ 'filter[is_active][eq]': 'true' })) { console.log(cliente.id, cliente.full_name);}O limit vai de 1 a 100, com padrão 20. pagination.has_more responde a mesma
pergunta que o cursor nulo, e serve quando você quer só saber se existe mais
alguma coisa.
Escrita idempotente
Seção intitulada “Escrita idempotente”Gere a chave antes da primeira tentativa e reaproveite o mesmo valor em todas as repetições. Assim, uma requisição perdida no caminho não vira dois registros.
import { api, novaChaveIdempotente, LiggaError } from './ligga.mjs';
const RETENTAVEIS = new Set([408, 429, 500, 502, 503, 504]);
export async function criarVenda(payload, tentativas = 3) { const chave = novaChaveIdempotente();
for (let tentativa = 1; tentativa <= tentativas; tentativa++) { try { return await api('/v1/sales', { method: 'POST', body: payload, idempotencyKey: chave, }); } catch (err) { const retentavel = err instanceof LiggaError && RETENTAVEIS.has(err.status); if (!retentavel || tentativa === tentativas) throw err;
await new Promise((r) => setTimeout(r, 250 * 2 ** (tentativa - 1))); } }}
const venda = await criarVenda({ customer_id: '7c4b0a19-2f36-4d1e-9b58-1f0a6c3e5d24', transaction_type: 'sale', sale_date: '2026-05-18', items: [ { catalog_type: 'product', catalog_item_id: '3d61f8ba-9c04-4a77-8e12-5b7a0d9f2c68', quantity: 2, unit_price: 29.90, }, ],});
console.log(venda.data.id);Reusar a mesma chave com um corpo diferente devolve 409 idempotency_conflict:
é sinal de que a chave vazou para outra operação.
Tratamento de erros
Seção intitulada “Tratamento de erros”Respostas de erro vêm como application/problem+json. O cliente acima já as
converte em LiggaError, com o corpo inteiro em err.problem.
import { api, LiggaError } from './ligga.mjs';
try { await api('/v1/customers', { method: 'POST', body: {} });} catch (err) { if (!(err instanceof LiggaError)) throw err;
console.error(err.problem.status, err.problem.type); console.error(err.problem.detail ?? err.problem.title); console.error('request_id:', err.problem.request_id);
for (const campo of err.problem.errors ?? []) { console.error(' -', campo.field, campo.code, campo.message); }}{ "type": "https://api.ligga.app/errors/validation", "title": "Validation failed", "status": 422, "detail": "full_name is required", "request_id": "req_01JZXQ8V2K5M7N9P0R1S2T3U4V"}type, title, status e request_id estão sempre presentes; detail,
instance e errors aparecem quando fazem sentido. Guarde o request_id no
seu log: é o que o time da Ligga pede para investigar uma chamada.
| HTTP | type | O que fazer |
|---|---|---|
| 401 | invalid_token | Confira LIGGA_API_KEY e o cabeçalho Authorization |
| 403 | insufficient_scope | A chave não carrega o escopo do recurso |
| 403 | module_not_enabled | O módulo do recurso não está no contrato |
| 409 | idempotency_conflict | A mesma Idempotency-Key foi reusada com outro corpo |
| 422 | validation | Campo obrigatório ausente ou com tipo errado |
| 429 | rate-limit-exceeded | Espere o tempo do cabeçalho Retry-After |
Tipos a partir do OpenAPI
Seção intitulada “Tipos a partir do OpenAPI”A especificação publicada gera tipos prontos para TypeScript:
npx openapi-typescript https://docs.ligga.app/openapi.json -o ./ligga-types.tsimport type { paths } from './ligga-types';
type ListaDeClientes = paths['/v1/customers']['get']['responses']['200']['content']['application/json'];
const pagina: ListaDeClientes = await api('/v1/customers?limit=20');const proximo = pagina.pagination.next_cursor;Veja também
Seção intitulada “Veja também”- Início rápido — a primeira chamada, passo a passo
- Paginação, filtros e ordenação — operadores e
sort - Idempotência — o ciclo de vida da
Idempotency-Key - Erros — o catálogo completo de
type