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,Uint8ArrayouArrayBuffer). Um objeto já parseado e serializado de novo não confere com a assinatura.headersaceita um objetoHeadersou um objeto simples como oreq.headersdo Node.- Exige
X-Cord-Signature-V1e 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 poreventcom odataexato dos 41 eventos e deping.
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 modoproxyUrldo Cord Elements. Veja Proxy de servidor.validateFiscalReceptor(),validRfc(),validSpainTaxId()evalidEin(): 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()eCORD_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.