Aller au contenu

SDK Node

@flouviahq/node : toute l’API v1 et la vérification des webhooks depuis votre serveur, sans dépendances.

SDK et outils officiels

Paquet Usage État
@flouviahq/node API v1 depuis votre serveur, webhooks, proxy d’Elements Publié (1.1.0)
@flouviahq/elements Devis, facture et formulaire fiscal dans le navigateur Publié (2.0.2). Voir Cord Elements.
@flouviahq/cli Connexion, simulateurs et webhooks sur votre localhost Publié (1.2.1). Voir CLI.
Python et PHP — Pas encore publiés. Il n’existe aucun paquet sur PyPI ni sur Packagist.

Tant que les SDK Python et PHP n’existent pas, appelez directement l’API REST. Pour vérifier les webhooks avec Laravel, Django, Flask ou FastAPI, cord init ajoute à votre projet un vérificateur de signature sans dépendances ; vous pouvez aussi copier l’algorithme de vérification.

Installation

npm install @flouviahq/node

Il s’exécute sur Node 20 ou version ultérieure, Bun, Deno, Cloudflare Workers et Vercel. Il est publié en ESM et en CommonJS, avec ses types.

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

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

Le SDK accepte une clé secrète (sk_live_, sk_test_) ou restreinte (rk_live_, rk_test_). Il refuse une clé publiable (pk_) et refuse de s’exécuter dans un navigateur : une clé secrète placée dans une page web est exposée.

Configuration

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' },
});
Option Par défaut Effet
apiVersion 2026-10-01 (CORD_API_VERSION) Envoyée dans l’en-tête Cord-Version de chaque requête. Voir Versions.
baseUrl https://cordhq.app L’origine de Cord. Le SDK ajoute /api/v1.
timeoutMs 30000 Interrompt chaque tentative.
maxRetries 2 (maximum 5) Nouvelles tentatives en cas de panne réseau, de 408, 425, 429, 5xx et de 409 idempotency_in_progress.
fetch fetch global Injectez votre propre fetch (tests, proxys).
appInfo — Ajoute votre application au User-Agent (cord-node/1.1.0 my-erp/4.2.0). Utile pour les demandes d’assistance.

cord.mode vaut 'test' ou 'live' selon le préfixe de la clé.

Ressources et méthodes

Chaque méthode correspond à une route de l’API v1 et conserve les noms de champs du serveur. Les méthodes qui lisent ou écrivent un objet renvoient le data de la réponse ; les méthodes list() renvoient { data, meta }.

Ressource Méthodes Route
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 (offre Professionnel ou supérieure)
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/* (sk_test_ uniquement)
cord.webhooks constructEvent, signPayload Vérification locale, sans réseau
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); // l’émet : irréversible
await cord.invoices.recordPayment(factura.id, { monto: 5220, moneda: 'MXN', metodo: 'transferencia' });

quotes.del() ne supprime que les brouillons. invoices.create() crée un brouillon ; finalize() l’émet. recordPayment() exige monto (montant) et moneda (devise) ensemble.

Listes et pagination automatique

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() demande des pages de 200 et suit next_cursor jusqu’à ce qu’il vaille null : il ne saute ni ne répète aucun enregistrement, même si d’autres sont créés pendant le parcours. Les factures et les événements se paginent uniquement par curseur.

Si vous êtes encore en 1.0.0

En 1.0.0, quotes.listAll(), clients.listAll() et products.listAll() parcourent par offset, et list() sur ces trois ressources n’accepte pas cursor. Depuis la 1.1.0, ils parcourent par curseur et clients.list() accepte q et email. Mettez à jour avec npm install @flouviahq/node@latest.

Idempotence et nouvelles tentatives

Chaque opération d’écriture (POST, PATCH, DELETE) porte une Idempotency-Key. Si vous n’en fournissez pas, le SDK génère un UUID par appel et le répète à chaque nouvelle tentative de cet appel : une coupure réseau ne crée jamais deux devis et n’enregistre jamais deux paiements. Pour lier l’opération à l’un de vos identifiants, de sorte qu’elle survive à un redémarrage de votre processus :

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

Les options par appel sont idempotencyKey, timeoutMs et signal (un AbortSignal). Les nouvelles tentatives attendent ce qu’indique Retry-After (jusqu’à 60 s) ou un backoff exponentiel avec jitter.

Erreurs typées

Toute réponse d’erreur est convertie en CordError :

Propriété Contenu
status Code HTTP. 0 en l’absence de réponse (réseau coupé ou délai dépassé).
code Le code stable de l’API (invalid_request, insufficient_permissions, invalid_state…). network_error en l’absence de réponse.
message Le message du champ error.
requestId Le Cord-Request-Id (req_…). Recherchez-le dans Développeurs › Journaux.
docUrl Lien vers l’explication du code.
raw Le corps complet, avec des champs supplémentaires comme 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') {
    // Le devis n’était ni envoyé ni consulté : rien n’a été appliqué.
  } else {
    throw err;
  }
}

La liste complète des codes se trouve dans Gestion des erreurs.

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?) utilise WebCrypto : c’est donc la même fonction dans tous les environnements.

  • payload est le corps brut (string, Uint8Array ou ArrayBuffer). Un objet déjà parsé puis resérialisé ne correspondra pas à la signature.
  • headers accepte un objet Headers ou un objet simple comme le req.headers de Node.
  • Il exige X-Cord-Signature-V1 et rejette un envoi dont l’écart dépasse 300 secondes (tolerance, en secondes). Pendant une rotation de secret, deux signatures arrivent : il suffit que l’une corresponde.
  • La signature historique X-Cord-Signature, sans horodatage, n’est acceptée qu’avec { allowLegacySignature: true }.
  • Il renvoie CordWebhookEvent, une union discriminée par event avec le data exact des 41 événements et de ping.
CordWebhookSignatureError.code Cause
missing_signature Aucune signature n’est arrivée, ou le secret est manquant.
invalid_signature_header L’en-tête n’a pas la forme t=<unix>,v1=<hex>.
timestamp_out_of_tolerance La signature est plus ancienne (ou plus récente) que la tolérance : rejeu possible.
signature_mismatch La signature ne correspond pas à ce secret.
legacy_signature_rejected Seule la signature historique est arrivée et vous ne l’avez pas acceptée.
invalid_payload Le corps n’est pas un JSON de la forme { id, event, created_at, data }.

signPayload(body, secret, timestamp?) signe un corps comme le fait Cord, pour vos tests :

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');

Il exporte aussi WEBHOOK_EVENT_TYPES, WEBHOOK_EVENT_OBJECTS et isWebhookEventType() avec le catalogue des événements.

Mode test

Avec une clé sk_test_, cord.testHelpers provoque ce qu’un tiers fait en production, en passant par le même code que la production. Avec une clé live, il lève une erreur avant d’appeler 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');

Pour recevoir les webhooks sur votre machine, utilisez cord listen de la CLI.

Autres utilitaires

  • createElementsProxy() : le backend du mode proxyUrl de Cord Elements. Voir Proxy serveur.
  • validateFiscalReceptor(), validRfc(), validSpainTaxId() et validEin() : la même validation fiscale que celle appliquée par Cord à l’enregistrement, pour valider sur votre serveur avant d’appeler l’API. Voir Formulaire fiscal.
  • newIdempotencyKey() et CORD_API_VERSION.

Modifications par version

L’historique complet se trouve dans le CHANGELOG.md du paquet. Une version publiée n’est jamais réécrite : chaque modification incrémente le numéro.