Pular para o conteúdo

SDK de Node

@flouviahq/node: a API v1 completa e a verificação de webhooks a partir do seu servidor, sem dependências.

SDKs e ferramentas oficiais

Pacote Para quê Status
@flouviahq/node API v1 a partir do seu servidor, webhooks, proxy do Elements Publicado (1.1.0)
@flouviahq/elements Gerador de orçamentos, fatura e formulário fiscal no navegador Publicado (2.0.2). Veja Cord Elements.
@flouviahq/cli Login, simuladores e webhooks no seu localhost Publicado (1.2.1). Veja CLI.
Python e PHP — Ainda não publicados. Não há pacote no PyPI nem no Packagist.

Enquanto os SDKs de Python e PHP não existirem, chame a API REST diretamente. Para verificar webhooks em Laravel, Django, Flask ou FastAPI, o cord init deixa no seu projeto um verificador de assinatura sem dependências; você também pode copiar o algoritmo de verificação.

Instalação

npm install @flouviahq/node

Roda em Node 20 ou superior, Bun, Deno, Cloudflare Workers e Vercel. É publicado como ESM e CommonJS, com tipos.

import { Cord } from '@flouviahq/node';

const cord = new Cord(process.env.CORD_SECRET_KEY!);

O SDK aceita uma chave secreta (sk_live_, sk_test_) ou restrita (rk_live_, rk_test_). Ele rejeita uma chave publicável (pk_) e se recusa a rodar em um navegador: uma chave secreta em uma página fica exposta.

Configuração

const cord = new Cord(process.env.CORD_SECRET_KEY!, {
  apiVersion: '2026-10-01',
  timeoutMs: 15000,
  maxRetries: 3,
  appInfo: { name: 'my-erp', version: '4.2.0' },
});
Opção Padrão O que faz
apiVersion 2026-10-01 (CORD_API_VERSION) É enviada no header Cord-Version de cada requisição. Veja Versões.
baseUrl https://cordhq.app Origem do Cord. O SDK acrescenta /api/v1.
timeoutMs 30000 Interrompe cada tentativa.
maxRetries 2 (máximo 5) Novas tentativas diante de falha de rede, 408, 425, 429, 5xx e 409 idempotency_in_progress.
fetch fetch global Injete o seu próprio fetch (testes, proxies).
appInfo — Adiciona o seu app ao User-Agent (cord-node/1.1.0 my-erp/4.2.0). Útil ao pedir suporte.

cord.mode vale 'test' ou 'live' conforme o prefixo da chave.

Recursos e métodos

Cada método corresponde a uma rota da API v1 e mantém os nomes de campo do servidor. Os métodos que leem ou escrevem um objeto devolvem o data da resposta; os list() devolvem { data, meta }.

Recurso Métodos Rota
cord.me() — GET /me
cord.quotes list, listAll, retrieve, create, send, resend, approve, reject, markPaid, del, draftFromText /cotizaciones, /cotizaciones/{id}, /cotizaciones/ia
cord.clients list, listAll, retrieve, create, update /clientes, /clientes/{id}
cord.products list, listAll, create /productos
cord.invoices list, listAll, retrieve, create, finalize, send, void, recordPayment, creditNote /facturas, /facturas/{id}
cord.collections retrieve GET /cobranza (plano Profissional ou superior)
cord.events list, listAll GET /events
cord.tasks create POST /tareas
cord.webhookEndpoints list, create, del /webhooks, /webhooks/{id}
cord.elements config GET /elements/config
cord.testHelpers fiscal.setNextOutcome, quotes.view, quotes.expire, webhooks.trigger /test_helpers/* (somente sk_test_)
cord.webhooks constructEvent, signPayload Verificação local, sem rede
const quote = await cord.quotes.create({
  cliente: { empresa: 'Acme', email: 'purchasing@acme.com' },
  items: [{ descripcion: 'Installation', cantidad: 1, precio_unitario: 12500 }],
  base_currency: 'MXN',
  send: true,
});
// quote: { id, token, folio, link_publico, needs_approval, motivo, email }

await cord.quotes.markPaid(quote.id, { payment_method: 'transferencia' });

const factura = await cord.invoices.create({
  cliente_id: '0e6f2b8a-91c4-4d7e-b3a5-6c2d8f1e4a90',
  items: [{ descripcion: 'Monthly maintenance', cantidad: 1, precio_unitario: 4500 }],
  currency: 'MXN',
  due_date: '2026-11-06',
});
await cord.invoices.finalize(factura.id); // emite a fatura: irreversível
await cord.invoices.recordPayment(factura.id, { monto: 5220, moneda: 'MXN', metodo: 'transferencia' });

quotes.del() só exclui rascunhos. invoices.create() cria um rascunho; finalize() o emite. recordPayment() exige monto (valor) e moneda (moeda) juntos.

Listas e paginação automática

const page = await cord.quotes.list({ status: 'sent', limit: 50 });
console.log(page.meta.total, page.meta.next_cursor);

for await (const invoice of cord.invoices.listAll({ estado: 'open' })) {
  console.log(invoice.numero, invoice.saldo, invoice.moneda);
}

listAll() pede páginas de 200 e segue next_cursor até que ele chegue como null: não pula nem repete registros, mesmo que outros sejam criados enquanto você percorre a lista. Faturas e eventos paginam apenas por cursor.

Se você ainda está na 1.0.0

Na 1.0.0, quotes.listAll(), clients.listAll() e products.listAll() percorrem por offset, e list() desses três recursos não aceita cursor. A partir da 1.1.0 eles percorrem por cursor e clients.list() aceita q e email. Atualize com npm install @flouviahq/node@latest.

Idempotência e novas tentativas

Cada operação de escrita (POST, PATCH, DELETE) leva uma Idempotency-Key. Se você não passar uma, o SDK gera um UUID por chamada e o repete em cada nova tentativa dessa chamada: uma queda de rede nunca cria dois orçamentos nem registra dois pagamentos. Para vincular a operação a um identificador seu que sobreviva a um reinício do seu processo:

await cord.invoices.recordPayment(
  facturaId,
  { monto: 5000, moneda: 'MXN', referencia: 'SPEI-88213' },
  { idempotencyKey: `payment-${paymentId}` },
);

As opções por chamada são idempotencyKey, timeoutMs e signal (um AbortSignal). As novas tentativas esperam o que Retry-After indicar (até 60 s) ou um backoff exponencial com jitter.

Erros tipados

Toda resposta de erro é convertida em um CordError:

Propriedade Conteúdo
status Código HTTP. 0 se não houve resposta (rede fora do ar ou timeout).
code O code estável da API (invalid_request, insufficient_permissions, invalid_state…). network_error se não houve resposta.
message A mensagem do campo error.
requestId O Cord-Request-Id (req_…). Procure-o em Desenvolvedores › Logs.
docUrl Link para a explicação do código.
raw O corpo completo, com campos extras como limit ou plan_required.
import { Cord, CordError } from '@flouviahq/node';

try {
  await cord.quotes.approve(quoteId);
} catch (err) {
  if (err instanceof CordError && err.code === 'invalid_state') {
    // O orçamento não estava enviado nem visualizado: nada foi aplicado.
  } else {
    throw err;
  }
}

A lista completa de códigos está em Tratamento de erros.

Webhooks: constructEvent

import { constructEvent, CordWebhookSignatureError } from '@flouviahq/node';

export async function POST(req: Request) {
  let event;
  try {
    event = await constructEvent(await req.text(), req.headers, process.env.CORD_WEBHOOK_SECRET!);
  } catch (err) {
    if (err instanceof CordWebhookSignatureError) return new Response(err.code, { status: 400 });
    throw err;
  }
  switch (event.event) {
    case 'invoice.paid':
      console.log(event.data.numero, event.data.saldo, event.data.moneda);
      break;
    case 'payment.partial':
      console.log(event.data.tipo, event.data.monto, event.data.saldo_pendiente);
      break;
  }
  return new Response('ok');
}

constructEvent(payload, headers, secret, options?) usa WebCrypto, por isso é a mesma função em todos os ambientes:

  • payload é o corpo bruto (string, Uint8Array ou ArrayBuffer). Um objeto já parseado e serializado de novo não confere com a assinatura.
  • headers aceita um objeto Headers ou um objeto simples como o req.headers do Node.
  • Exige X-Cord-Signature-V1 e rejeita uma entrega com mais de 300 segundos de diferença (tolerance, em segundos). Durante uma rotação de segredo chegam duas assinaturas: basta que uma confira.
  • A assinatura legada X-Cord-Signature, sem carimbo de data e hora, só é aceita com { allowLegacySignature: true }.
  • Devolve CordWebhookEvent, uma união discriminada por event com o data exato dos 41 eventos e de ping.
CordWebhookSignatureError.code Causa
missing_signature Não chegou assinatura, ou falta o segredo.
invalid_signature_header O header não tem o formato t=<unix>,v1=<hex>.
timestamp_out_of_tolerance A assinatura é mais antiga (ou mais nova) que a tolerância: possível reenvio malicioso.
signature_mismatch A assinatura não corresponde a esse segredo.
legacy_signature_rejected Só chegou a assinatura legada e você não a aceitou.
invalid_payload O corpo não é um JSON com o formato { id, event, created_at, data }.

signPayload(body, secret, timestamp?) assina um corpo como o Cord faz, para os seus testes:

import { signPayload, constructEvent } from '@flouviahq/node';

const body = JSON.stringify({ id: 'evt_test', event: 'quote.sent', created_at: new Date().toISOString(), data: {} });
const header = await signPayload(body, 'whsec_test');
const event = await constructEvent(body, { 'x-cord-signature-v1': header }, 'whsec_test');

Também exporta WEBHOOK_EVENT_TYPES, WEBHOOK_EVENT_OBJECTS e isWebhookEventType() com o catálogo de eventos.

Modo de teste

Com uma chave sk_test_, cord.testHelpers provoca o que em produção um terceiro faz, pelo mesmo código de produção. Com uma chave de produção, lança um erro antes de chamar o Cord.

await cord.testHelpers.fiscal.setNextOutcome('pac_caido');
await cord.testHelpers.quotes.view(quoteId);
await cord.testHelpers.quotes.expire(quoteId);
await cord.testHelpers.webhooks.trigger('invoice.paid');

Para receber os webhooks na sua máquina, use cord listen da CLI.

Outros utilitários

  • createElementsProxy(): o backend do modo proxyUrl do Cord Elements. Veja Proxy de servidor.
  • validateFiscalReceptor(), validRfc(), validSpainTaxId() e validEin(): a mesma validação fiscal que o Cord aplica ao salvar, para validar no seu servidor antes de chamar a API. Veja Formulário fiscal.
  • newIdempotencyKey() e CORD_API_VERSION.

Alterações por versão

O histórico completo fica no CHANGELOG.md do pacote. Uma versão publicada não é reescrita: cada alteração sobe o número.