Factuarea APIDevelopers

TypeScript

Install @factuarea/sdk for Node.js, authenticate, and create your first invoice. Dual ESM + CommonJS, full type declarations, Node 20+.

The official TypeScript SDK is @factuarea/sdk on npm — dual ESM + CommonJS with full type declarations. Source: github.com/factuarea/factuarea-node. It wraps the v1 REST API with automatic retries, idempotency keys, cursor auto-pagination, a typed error hierarchy and webhook verification — covered in the SDK overview.

Install

npm install @factuarea/sdk

Requires Node 20 or newer. The SDK is built on the Web fetch standard, so it also runs on Deno, Bun and Cloudflare Workers.

Authenticate

Pass your API key. The key prefix selects the environment — there is no separate flag: a fact_test_… key always runs against the isolated sandbox, a fact_live_… key against production.

import { Factuarea } from "@factuarea/sdk";

const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! });

factuarea.environment; // "test" or "live", derived from the key prefix

Optional configuration:

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

Server-side only. Your API key is a secret. Never ship the SDK with a live key to a browser, mobile app or any public client — use it from your backend.

Quickstart

Create a customer contact and an invoice, then download its PDF using the released SDK methods below. For other operations, check your installed SDK or use the HTTP snippet in the API Reference.

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

Run everything with a fact_test_ key first — sandbox effects (VeriFactu → AEAT, FACe, email, webhooks) are switched off. When your flow works end-to-end, swap the prefix to fact_live_. See Test mode & sandbox.

Account model

The account axis (/v1/accounts/{account}/…) is covered by factuarea.companies (the NIF portfolio), factuarea.account.nifTransfers, factuarea.managedAccounts and, for webhooks and request logs, factuarea.account.webhooks and factuarea.account.requestLogs. They arrive with 0.7.0, the target version of the account model; it is not published yet.

The account argument is positional. Account-scoped methods take the account id as their first argument, before any other id and before the body. TypeScript callers get a compile error, but plain JavaScript callers would shift every argument by one place without an error: review each call to account.*, companies.* and managedAccounts.* when you upgrade.

The example lists the NIF of the account, applies a bulk import of managed accounts with a FormData (accounts_file is required, customers_file is optional and the options travel as options[...] fields) and creates an account webhook:

import { readFile } from "node:fs/promises";
import { Factuarea } from "@factuarea/sdk";

const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! });

// The account id is `data.account.id` of `factuarea.account.show()` (GET /v1/me).
const me = (await factuarea.account.show()) as { data: { account: { id: string } } };
const account = me.data.account.id;

// 1. List the NIF of the account (the account id goes FIRST).
const nifs = await factuarea.companies.list(account, { limit: 50 });
for await (const nif of nifs) {
  console.log((nif as { id: string }).id);
}

// 2. Preview and apply a bulk import of managed accounts (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); // writes nothing
await factuarea.managedAccounts.imports.create(form);  // applies it

// 3. Create an account webhook (needs a fact_live_ key).
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_…) is returned only now: store it to verify deliveries.
console.log(created.data.id, created.data.secret);

The NIF transfer follows the same shape: factuarea.account.nifTransfers.create(account, body), .list(account), .accept(account, body) and .revoke(account, nifTransfer). A delivery of an account webhook is verified with factuarea.webhooks.verify, exactly like a company one; see Verifying webhooks.

PDF templates

The PDF template library has its own resources: factuarea.pdfTemplates (templates, revisions, company and contact defaults, private images), factuarea.pdfCommercialIdentities, factuarea.pdfTemplatePreviews and factuarea.pdfArtifacts (prepare, render, show and download). Invoices, quotes, proformas and delivery notes gain getPresentation, listPresentations, assignPresentation and revisePresentation, and factuarea.series gains getPdfTemplate and assignPdfTemplate. They arrive with the next release of @factuarea/sdk; until you upgrade, the API Reference has the HTTP example of each operation.

Like every company resource, they hang from /v1/companies/{company}/…, so the company id is the first argument of each call, before the other ids and the body. It is the id of an entry of data.scope in GET /v1/me; see the company segment. Library resources come back as the bare object, without a data envelope. Their lists paginate by page and per_page: each call is one request and returns { items, meta } as is, not an auto-paginating iterator.

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,
});

See the PDF template library, Document presentations and PDF previews and artifacts for the model.

Conditional requests and downloads

Every resource method takes the request options (RequestConfig) as its last argument. Besides idempotencyKey, timeout, maxRetries and headers, these options cover concurrency, downloads and retries. They arrive with the same release as the template resources:

OptionWhat it does
ifMatchSends If-Match verbatim: the ETag of the last read of the resource you change. A stale tag answers 412; a write that needs a precondition and gets none answers 428.
ifNoneMatchSends If-None-Match verbatim: the ETag of a copy you already hold. If that copy is still current, the download answers 304.
onResponseCallback that receives the ResponseMeta of the final response, successful or not: status, headers, requestId, etag, retryAfter (seconds, or null) and idempotentReplayed.
noRetryTurns the automatic retries off for this call only; the shared client does not change.
signalAn AbortSignal. No further attempt is made and the call throws a ConnectionError; a write the server already accepted is not undone.

Read the ETag and send it back

Resource methods return only the decoded body, so you read the ETag with onResponse. The expected_version of a body is a separate check that answers 409 when it is stale. A null in the body is sent as null; only a key you leave out (or set to undefined) is omitted. The snippets reuse factuarea, company and invoiceId from the example above.

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 },
);

See Concurrency for the error codes and how to recover.

Downloads: bytes, 202 or 304

invoices.pdf, quotes.pdf, proformas.pdf, deliveryNotes.pdf, pdfArtifacts.download and pdfTemplates.assetGet return a BinaryOrJsonResponse, a union you narrow on kind before touching the result:

kindWhenWhat it carries
"binary"200 with the bytes.toBuffer(), toBlob(), body, contentType and etag.
"json"A JSON document, such as the 202 of a document PDF that is still being generated.data, status and retryAfter (seconds, or null).
"not_modified"304: you sent ifNoneMatch and your copy is current.No body; status, headers and 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 of an artifact that is not ready yet answers 503 pdf_artifact_not_ready. The SDK retries it like any 5xx, honouring Retry-After; if it gives up, the error it throws carries retryAfter. pdfTemplates.assetGet answers with the JSON metadata of the image by default; ask for the bytes with { headers: { Accept: "image/png" } }. See How downloads answer.

Retries you turn off

FACe submission and cancellation (invoices.faceSubmissions.submit and faceSubmissions.cancel) reach a third party, so the SDK never retries them automatically, whatever maxRetries says. After a connection failure the outcome is unknown: read the submissions before sending again. Pass noRetry: true to get the same behaviour on any other call.

Next steps

The runtime behaviour — retries, idempotency, cursor auto-pagination, the typed error hierarchy and webhook verification — is shared across both SDKs and documented once in the SDK overview:

Release coverage

The September 2026 baseline is 0.2.0 for both SDKs. Their main-branch specs contain 413 entries, with 39 REST endpoints still absent. A package upgrade is needed before newly generated SDK methods become available. The API Reference provides complete TypeScript fetch, PHP cURL and shell HTTP examples for the documented contract; these examples do not assume an SDK method exists.

On this page

Need a hand?Contact support