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.
payloadest le corps brut (string,Uint8ArrayouArrayBuffer). Un objet déjà parsé puis resérialisé ne correspondra pas à la signature.headersaccepte un objetHeadersou un objet simple comme lereq.headersde Node.- Il exige
X-Cord-Signature-V1et 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 pareventavec ledataexact des 41 événements et deping.
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 modeproxyUrlde Cord Elements. Voir Proxy serveur.validateFiscalReceptor(),validRfc(),validSpainTaxId()etvalidEin(): 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()etCORD_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.