Node-SDK
@flouviahq/node: die vollständige API v1 und die Webhook-Verifizierung von Ihrem Server aus, ohne Abhängigkeiten.
Offizielle SDKs und Tools
| Paket | Wofür | Status |
|---|---|---|
@flouviahq/node |
API v1 von Ihrem Server, Webhooks, Elements-Proxy | Veröffentlicht (1.1.0) |
@flouviahq/elements |
Angebotsrechner, Rechnung und Steuerformular im Browser | Veröffentlicht (2.0.2). Siehe Cord Elements. |
@flouviahq/cli |
Anmeldung, Simulatoren und Webhooks auf Ihrem localhost |
Veröffentlicht (1.2.1). Siehe CLI. |
| Python und PHP | — | Noch nicht veröffentlicht. Es gibt kein Paket auf PyPI oder Packagist. |
Solange es die SDKs für Python und PHP nicht gibt, rufen Sie die REST-API direkt auf. Um Webhooks in Laravel, Django, Flask oder FastAPI zu verifizieren, legt cord init einen Signaturprüfer ohne Abhängigkeiten in Ihrem Projekt an; Sie können auch den Verifizierungsalgorithmus übernehmen.
Installation
npm install @flouviahq/node
Das SDK läuft auf Node 20 oder höher, Bun, Deno, Cloudflare Workers und Vercel. Es wird als ESM und CommonJS veröffentlicht, mit Typen.
import { Cord } from '@flouviahq/node';
const cord = new Cord(process.env.CORD_SECRET_KEY!);
Das SDK akzeptiert einen geheimen Schlüssel (sk_live_, sk_test_) oder einen eingeschränkten (rk_live_, rk_test_). Einen veröffentlichbaren Schlüssel (pk_) lehnt es ab, und es verweigert die Ausführung im Browser: Ein geheimer Schlüssel auf einer Webseite liegt offen.
Konfiguration
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 | Standard | Was sie tut |
|---|---|---|
apiVersion |
2026-10-01 (CORD_API_VERSION) |
Wird im Header Cord-Version jeder Anfrage gesendet. Siehe Versionen. |
baseUrl |
https://cordhq.app |
Origin von Cord. Das SDK hängt /api/v1 an. |
timeoutMs |
30000 |
Bricht jeden Versuch ab. |
maxRetries |
2 (maximal 5) |
Wiederholungen bei Netzwerkausfall, 408, 425, 429, 5xx und 409 idempotency_in_progress. |
fetch |
globales fetch |
Fügen Sie Ihr eigenes fetch ein (Tests, Proxys). |
appInfo |
— | Ergänzt Ihre App im User-Agent (cord-node/1.1.0 my-erp/4.2.0). Hilfreich bei Supportanfragen. |
cord.mode ist je nach Präfix des Schlüssels 'test' oder 'live'.
Ressourcen und Methoden
Jede Methode entspricht einer Route der API v1 und behält die Feldnamen des Servers bei. Methoden, die ein Objekt lesen oder schreiben, geben das data der Antwort zurück; die list()-Methoden geben { data, meta } zurück.
| Ressource | Methoden | 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 (ab dem Professional-Tarif) |
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/* (nur sk_test_) |
cord.webhooks |
constructEvent, signPayload |
Lokale Verifizierung, ohne Netzwerk |
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); // stellt die Rechnung aus: unumkehrbar
await cord.invoices.recordPayment(factura.id, { monto: 5220, moneda: 'MXN', metodo: 'transferencia' });
quotes.del() löscht nur Entwürfe. invoices.create() erstellt einen Entwurf; finalize() stellt ihn aus. recordPayment() verlangt monto (Betrag) und moneda (Währung) gemeinsam.
Listen und automatische Paginierung
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() fordert Seiten mit 200 Einträgen an und folgt next_cursor, bis null zurückkommt: Es werden keine Datensätze übersprungen oder wiederholt, auch wenn während des Durchlaufs neue angelegt werden. Rechnungen und Ereignisse werden nur per Cursor paginiert.
Wenn Sie noch 1.0.0 verwenden
In 1.0.0 durchlaufen quotes.listAll(), clients.listAll() und products.listAll() die Daten per offset, und list() dieser drei Ressourcen akzeptiert keinen cursor. Seit 1.1.0 laufen sie per Cursor, und clients.list() akzeptiert q und email. Aktualisieren Sie mit npm install @flouviahq/node@latest.
Idempotenz und Wiederholungen
Jede schreibende Operation (POST, PATCH, DELETE) trägt einen Idempotency-Key. Wenn Sie keinen übergeben, erzeugt das SDK eine UUID pro Aufruf und wiederholt sie bei jeder Wiederholung dieses Aufrufs: Ein Netzwerkabbruch erzeugt nie zwei Angebote und erfasst nie zwei Zahlungen. So binden Sie die Operation an eine eigene Kennung, die einen Neustart Ihres Prozesses übersteht:
await cord.invoices.recordPayment(
facturaId,
{ monto: 5000, moneda: 'MXN', referencia: 'SPEI-88213' },
{ idempotencyKey: `payment-${paymentId}` },
);
Die Optionen pro Aufruf sind idempotencyKey, timeoutMs und signal (ein AbortSignal). Wiederholungen warten so lange, wie Retry-After angibt (bis zu 60 s), oder nutzen einen exponentiellen Backoff mit Jitter.
Typisierte Fehler
Jede Fehlerantwort wird zu einem CordError:
| Eigenschaft | Inhalt |
|---|---|
status |
HTTP-Statuscode. 0, wenn keine Antwort kam (Netzwerkausfall oder Timeout). |
code |
Der stabile code der API (invalid_request, insufficient_permissions, invalid_state…). network_error, wenn keine Antwort kam. |
message |
Die Meldung aus dem Feld error. |
requestId |
Die Cord-Request-Id (req_…). Suchen Sie sie unter Entwickler › Protokolle. |
docUrl |
Link zur Erklärung des Codes. |
raw |
Der vollständige Body, mit zusätzlichen Feldern wie limit oder plan_required. |
import { Cord, CordError } from '@flouviahq/node';
try {
await cord.quotes.approve(quoteId);
} catch (err) {
if (err instanceof CordError && err.code === 'invalid_state') {
// Das Angebot war weder gesendet noch angesehen: Es wurde nichts angewendet.
} else {
throw err;
}
}
Die vollständige Liste der Codes finden Sie unter Fehlerbehandlung.
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?) verwendet WebCrypto und ist daher in jeder Umgebung dieselbe Funktion:
payloadist der rohe Body (string,Uint8ArrayoderArrayBuffer). Ein bereits geparstes und wieder serialisiertes Objekt passt nicht zur Signatur.headersakzeptiert einHeaders-Objekt oder ein einfaches Objekt wiereq.headersin Node.- Die Funktion verlangt
X-Cord-Signature-V1und lehnt eine Zustellung mit mehr als 300 Sekunden Abweichung ab (tolerance, in Sekunden). Während einer Secret-Rotation kommen zwei Signaturen an: Es genügt, wenn eine passt. - Die Legacy-Signatur
X-Cord-Signatureohne Zeitstempel wird nur mit{ allowLegacySignature: true }akzeptiert. - Sie gibt
CordWebhookEventzurück, eine nacheventunterschiedene Union mit dem exaktendatader 41 Ereignisse und vonping.
CordWebhookSignatureError.code |
Ursache |
|---|---|
missing_signature |
Es kam keine Signatur an, oder das Secret fehlt. |
invalid_signature_header |
Der Header hat nicht die Form t=<unix>,v1=<hex>. |
timestamp_out_of_tolerance |
Die Signatur ist älter (oder neuer) als die Toleranz: möglicher Replay. |
signature_mismatch |
Die Signatur passt nicht zu diesem Secret. |
legacy_signature_rejected |
Es kam nur die Legacy-Signatur an, und Sie haben sie nicht akzeptiert. |
invalid_payload |
Der Body ist kein JSON der Form { id, event, created_at, data }. |
signPayload(body, secret, timestamp?) signiert einen Body so, wie Cord es tut, für Ihre 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');
Außerdem exportiert das SDK WEBHOOK_EVENT_TYPES, WEBHOOK_EVENT_OBJECTS und isWebhookEventType() mit dem Ereigniskatalog.
Testmodus
Mit einem sk_test_-Schlüssel löst cord.testHelpers aus, was in der Produktion ein Dritter tut, über denselben Code wie in der Produktion. Mit einem Live-Schlüssel wirft es einen Fehler, bevor Cord aufgerufen wird.
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');
Um die Webhooks auf Ihrem Rechner zu empfangen, verwenden Sie cord listen aus der CLI.
Weitere Hilfsfunktionen
createElementsProxy(): das Backend für denproxyUrl-Modus von Cord Elements. Siehe Server-Proxy.validateFiscalReceptor(),validRfc(),validSpainTaxId()undvalidEin(): dieselbe steuerliche Validierung, die Cord beim Speichern anwendet, damit Ihr Server vor dem API-Aufruf validieren kann. Siehe Steuerformular.newIdempotencyKey()undCORD_API_VERSION.
Änderungen nach Version
Der vollständige Verlauf steht in der CHANGELOG.md des Pakets. Eine veröffentlichte Version wird nie überschrieben: Jede Änderung erhöht die Versionsnummer.