TypeScript
Instala @factuarea/sdk para Node.js, autentícate y crea tu primera factura. ESM + CommonJS dual, declaraciones de tipos completas, Node 20+.
El SDK oficial de TypeScript es @factuarea/sdk
en npm — ESM + CommonJS dual con declaraciones de tipos completas. Código fuente:
github.com/factuarea/factuarea-node.
Envuelve la API REST v1 con reintentos automáticos, idempotency keys,
auto-paginación por cursor, una jerarquía de errores tipada y verificación de
webhooks — todo cubierto en la introducción al SDK.
Instalación
npm install @factuarea/sdkRequiere Node 20 o superior. El SDK está construido sobre el estándar Web
fetch, así que también funciona en Deno, Bun y Cloudflare Workers.
Autenticación
Pasa tu API key. El prefijo de la clave selecciona el entorno — no hay un
flag aparte: una clave fact_test_… siempre se ejecuta contra el
sandbox aislado, y una clave fact_live_… contra producción.
import { Factuarea } from "@factuarea/sdk";
const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! });
factuarea.environment; // "test" or "live", derived from the key prefixConfiguración opcional:
new Factuarea({
apiKey: "fact_live_…", // required
baseUrl: "https://api.factuarea.com/v1", // override for staging
timeout: 60_000, // per-request ms (default 60s)
maxRetries: 2, // attempts after the first try
factuareaVersion: "2026-06-04", // pinned API version header
defaultHeaders: {}, // extra headers on every request
});Solo en el servidor. Tu API key es un secreto. Nunca distribuyas el SDK con una clave live a un navegador, app móvil o cualquier cliente público — úsala desde tu backend.
Inicio rápido
Crea un contacto con rol de cliente y una factura, y descarga su PDF con los métodos publicados del SDK que aparecen a continuación. Para otras operaciones, comprueba tu SDK instalado o usa el ejemplo HTTP de la referencia de la API.
import { writeFile } from "node:fs/promises";
import { Factuarea } from "@factuarea/sdk";
const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! });
// Company axis: every company resource hangs off `/v1/companies/{company}/…`, so the
// company id is the FIRST argument of each call (`data.scope[].id` of GET /v1/me).
const company = process.env.FACTUAREA_COMPANY_ID!;
// Responses are the API's `{ data: … }` envelope — read the resource off `.data`.
// 1. Create a customer contact.
const { data: contact } = await factuarea.contacts.create(company, {
name: "Cliente Demo SL",
kind: "company",
roles: ["customer"],
tax_id: "B12345674",
});
// 2. Create an invoice (the API computes the totals).
const { data: invoice } = await factuarea.invoices.create(company, {
client_id: contact.id,
series_id: "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e",
issued_on: "2026-06-05",
due_on: "2026-07-05",
lines: [
{
description: "Consultoría — junio 2026",
quantity: 10,
unit_price: 100,
tax_rate_id: "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f",
},
],
});
// 3. Download the PDF. It is a BinaryOrJsonResponse: narrow it on `kind` first.
const pdf = await factuarea.invoices.pdf(company, invoice.id);
if (pdf.kind === "binary") {
await writeFile("invoice.pdf", pdf.toBuffer());
} else {
// 202 (`kind: "json"`): still being generated. Ask again after `pdf.retryAfter` seconds.
console.log("The PDF is not ready yet:", pdf.status);
}Ejecuta todo primero con una clave fact_test_ — los efectos del sandbox
(VeriFactu → AEAT, FACe, email, webhooks) están desactivados. Cuando tu flujo
funcione de extremo a extremo, cambia el prefijo a fact_live_. Consulta
Modo de prueba y sandbox.
Modelo de cuentas
El eje de cuenta (/v1/accounts/{account}/…) lo cubren factuarea.companies (la cartera de NIF), factuarea.account.nifTransfers, factuarea.managedAccounts y, para webhooks y registros de peticiones, factuarea.account.webhooks y factuarea.account.requestLogs. Llegan con 0.7.0, la versión objetivo del modelo de cuentas; todavía no está publicada.
El argumento de cuenta es posicional. Los métodos del eje de cuenta reciben
el id de la cuenta como primer argumento, antes de cualquier otro id y antes
del cuerpo. Quien llama desde TypeScript recibe un error de compilación, pero
quien llama desde JavaScript puro desplazaría cada argumento una posición sin
ningún error: revisa cada llamada a account.*, companies.* y
managedAccounts.* al actualizar.
El ejemplo lista los NIF de la cuenta, aplica una importación masiva de cuentas gestionadas con un FormData (accounts_file es obligatorio, customers_file es opcional y las opciones viajan como campos options[...]) y crea un webhook de cuenta:
import { readFile } from "node:fs/promises";
import { Factuarea } from "@factuarea/sdk";
const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! });
// El id de la cuenta es `data.account.id` de `factuarea.account.show()` (GET /v1/me).
const me = (await factuarea.account.show()) as { data: { account: { id: string } } };
const account = me.data.account.id;
// 1. Lista los NIF de la cuenta (el id de la cuenta va PRIMERO).
const nifs = await factuarea.companies.list(account, { limit: 50 });
for await (const nif of nifs) {
console.log((nif as { id: string }).id);
}
// 2. Previsualiza y aplica una importación masiva de cuentas gestionadas (multipart/form-data).
const form = new FormData();
form.append(
"accounts_file",
new Blob([await readFile("accounts.csv")], { type: "text/csv" }),
"accounts.csv",
);
form.append("options[send_email]", "false");
await factuarea.managedAccounts.imports.preview(form); // no escribe nada
await factuarea.managedAccounts.imports.create(form); // la aplica
// 3. Crea un webhook de cuenta (exige una clave fact_live_).
const created = (await factuarea.account.webhooks.create(account, {
url: "https://example.com/webhooks/factuarea",
enabled_events: ["account.nif_added", "invoice.paid"],
account_relationship: "all",
})) as { data: { id: string; secret: string | null } };
// `secret` (whsec_…) solo se devuelve ahora: guárdalo para verificar las entregas.
console.log(created.data.id, created.data.secret);El traspaso de NIF tiene la misma forma: factuarea.account.nifTransfers.create(account, body), .list(account), .accept(account, body) y .revoke(account, nifTransfer). Una entrega de un webhook de cuenta se verifica con factuarea.webhooks.verify, igual que una de empresa; consulta Verificación de webhooks.
Plantillas PDF
La biblioteca de plantillas PDF tiene sus propios recursos: factuarea.pdfTemplates (plantillas, revisiones, valores por defecto de la empresa y de cada contacto, imágenes privadas), factuarea.pdfCommercialIdentities, factuarea.pdfTemplatePreviews y factuarea.pdfArtifacts (prepare, render, show y download). Las facturas, los presupuestos, las facturas proforma y los albaranes ganan getPresentation, listPresentations, assignPresentation y revisePresentation, y factuarea.series gana getPdfTemplate y assignPdfTemplate. Llegan con la próxima versión de @factuarea/sdk; hasta que actualices, la referencia de la API tiene el ejemplo HTTP de cada operación.
Como todo recurso de empresa, cuelgan de /v1/companies/{company}/…, así que el id de la empresa es el primer argumento de cada llamada, antes de los demás ids y del cuerpo. Es el id de una entrada de data.scope en GET /v1/me; consulta el segmento de empresa. Los recursos de la biblioteca llegan como el objeto tal cual, sin el envoltorio data. Sus listados paginan por page y per_page: cada llamada es una petición y devuelve { items, meta } tal cual, no un iterador que pagina solo.
import { Factuarea } from "@factuarea/sdk";
const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! });
const company = process.env.FACTUAREA_COMPANY_ID!; // `data.scope[].id` of GET /v1/me
const invoiceId = "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b";
// 1. Create a template: only `name` is required.
const template = await factuarea.pdfTemplates.create(company, {
name: "Factura clásica",
base_preset: "classic",
});
// 2. List the library, one page per call.
const page = await factuarea.pdfTemplates.list(company, { page: 1, per_page: 20 });
for (const item of page.items) {
console.log(item.id, item.name);
}
// 3. Pin the template on an invoice. `expected_version` is the `selection_version`
// of the presentation state; `template_id: null` hands the choice back to the defaults.
const state = await factuarea.invoices.getPresentation(company, invoiceId);
await factuarea.invoices.assignPresentation(company, invoiceId, {
template_id: template.id,
expected_version: state.data.selection_version,
});Consulta la biblioteca de plantillas PDF, Presentación de documentos y Vistas previas y artefactos PDF para el modelo.
Peticiones condicionales y descargas
Cada método de recurso recibe las opciones de la petición (RequestConfig) como último argumento. Además de idempotencyKey, timeout, maxRetries y headers, estas opciones cubren la concurrencia, las descargas y los reintentos. Llegan con la misma versión que los recursos de plantillas:
| Opción | Qué hace |
|---|---|
ifMatch | Envía If-Match tal cual: el ETag de la última lectura del recurso que cambias. Un tag desfasado responde 412; una escritura que exige precondición y no la recibe responde 428. |
ifNoneMatch | Envía If-None-Match tal cual: el ETag de una copia que ya tienes. Si esa copia sigue vigente, la descarga responde 304. |
onResponse | Callback que recibe el ResponseMeta de la respuesta final, con éxito o sin él: status, headers, requestId, etag, retryAfter (segundos, o null) e idempotentReplayed. |
noRetry | Desactiva los reintentos automáticos solo para esta llamada; el cliente compartido no cambia. |
signal | Un AbortSignal. No se hace ningún intento más y la llamada lanza un ConnectionError; una escritura que el servidor ya aceptó no se deshace. |
Lee el ETag y devuélvelo
Los métodos de recurso devuelven solo el cuerpo decodificado, así que lees el ETag con onResponse. El expected_version de un cuerpo es una comprobación aparte que responde 409 cuando está desfasado. Un null en el cuerpo se envía como null; solo se omite una clave que dejas fuera (o que vale undefined). Los fragmentos reutilizan factuarea, company e invoiceId del ejemplo anterior.
const templateId = "0199f3b2-5c1d-7e8f-9a0b-1c2d3e4f5a6b";
// Read the template and keep the ETag of the response.
let etag: string | null = null;
await factuarea.pdfTemplates.find(company, templateId, {
onResponse: (meta) => {
etag = meta.etag;
},
});
// Send it back as If-Match. `null` clears the description; `name` is not sent, so it is kept.
await factuarea.pdfTemplates.update(
company,
templateId,
{ description: null },
{ ifMatch: etag ?? undefined },
);Consulta Concurrencia para los códigos de error y cómo recuperarte.
Descargas: bytes, 202 o 304
invoices.pdf, quotes.pdf, proformas.pdf, deliveryNotes.pdf, pdfArtifacts.download y pdfTemplates.assetGet devuelven un BinaryOrJsonResponse, una unión que acotas por kind antes de usar el resultado:
kind | Cuándo | Qué lleva |
|---|---|---|
"binary" | 200 con los bytes. | toBuffer(), toBlob(), body, contentType y etag. |
"json" | Un documento JSON, como el 202 de un PDF de documento que aún se está generando. | data, status y retryAfter (segundos, o null). |
"not_modified" | 304: enviaste ifNoneMatch y tu copia está vigente. | Sin cuerpo; status, headers y etag. |
import { writeFile } from "node:fs/promises";
import { setTimeout as sleep } from "node:timers/promises";
// `cachedEtag` is the ETag of the copy you already hold, if any.
let cachedEtag: string | undefined;
for (let attempt = 0; attempt < 5; attempt++) {
const pdf = await factuarea.invoices.pdf(company, invoiceId, undefined, { ifNoneMatch: cachedEtag });
if (pdf.kind === "binary") {
await writeFile("invoice.pdf", pdf.toBuffer());
cachedEtag = pdf.etag ?? undefined;
break;
}
if (pdf.kind === "not_modified") {
break; // 304: your copy is current.
}
await sleep((pdf.retryAfter ?? 1) * 1000); // 202: still being generated.
}pdfArtifacts.download de un artefacto que aún no está listo responde 503 pdf_artifact_not_ready. El SDK lo reintenta como cualquier 5xx, respetando Retry-After; si se rinde, el error que lanza lleva retryAfter. pdfTemplates.assetGet responde por defecto con los metadatos JSON de la imagen; pide los bytes con { headers: { Accept: "image/png" } }. Consulta Cómo responden las descargas.
Reintentos que desactivas
El envío y la cancelación en FACe (invoices.faceSubmissions.submit y faceSubmissions.cancel) llegan a un tercero, así que el SDK nunca los reintenta automáticamente, diga lo que diga maxRetries. Tras un fallo de conexión el resultado es desconocido: consulta los envíos antes de volver a enviar. Pasa noRetry: true para tener el mismo comportamiento en cualquier otra llamada.
Próximos pasos
El comportamiento en tiempo de ejecución — reintentos, idempotencia, auto-paginación por cursor, la jerarquía de errores tipada y la verificación de webhooks — es común a ambos SDK y está documentado una sola vez en la introducción al SDK:
Cobertura de las versiones
La referencia de septiembre de 2026 es 0.2.0 para ambos SDK. Los specs de sus ramas principales contienen 413 entradas y todavía les faltan 39 endpoints REST. Es necesario actualizar el paquete para disponer de nuevos métodos generados. La referencia de la API ofrece ejemplos HTTP completos con fetch de TypeScript, cURL de PHP y terminal para el contrato documentado; estos ejemplos no presuponen que exista un método del SDK.