Skip to content

Quickstart ​

Register your first invoice against the AEAT pre-production environment in under thirty lines. The flow is the same against production — only the environment flag and the certificate change.

Prerequisites ​

  1. An AEAT-issued mTLS certificate in .pfx (PKCS#12) or PEM form. See Certificates for how to obtain it.
  2. Bun 1.4.2 (or Node ≥ 20).
  3. The SDK installed in your project (bun add verifactu-sdk).
  4. A HashStore to persist the chain. This is a required constructor option — there's no in-memory default, since a chain that can vanish on restart is a compliance failure. Pick one of the bundled adapters below (verifactu-sdk/store/*) or implement the two-method interface yourself; see Hash chain → Persisting the chain for the full list (Postgres, MySQL, SQLite, Redis, Drizzle…) and how each is set up.

Minimal client setup ​

The example below uses BunSqlHashStore (Postgres via Bun's built-in Bun.sql, zero extra dependencies) — swap it for any other adapter from the table linked above without changing anything else.

ts
import { VerifactuClient, Environment } from 'verifactu-sdk';
import { BunSqlHashStore } from 'verifactu-sdk/store/bun-sql';
import { SQL } from 'bun';
import { readFileSync } from 'node:fs';

const hashStore = new BunSqlHashStore(new SQL(process.env.DATABASE_URL!));
await hashStore.migrate(); // idempotent — creates verifactu_hash_chain if missing

const client = new VerifactuClient({
  environment: Environment.Preproduction,
  mode: 'verifactu',
  certificate: {
    pfx: readFileSync('./cert.pfx'),
    passphrase: process.env.CERT_PASS ?? '',
  },
  taxpayer: { nif: 'B12345678', legalName: 'My Company SL' },
  billingSystem: {
    producerName: 'My Company SL',
    nif: 'B12345678',
    systemId: 'JC',
    systemName: 'My App',
    version: '1.0.0',
    installationNumber: '0001',
    onlyVerifactu: 'S',
    multipleTaxpayer: 'N',
    hasMultipleTaxpayers: 'N',
  },
  hashStore,
});

The client owns the hash chain, the flow controller (which respects the TiempoEsperaEnvio returned by the AEAT) and the per-call validator. You only need one instance per taxpayer.

Registering an invoice ​

ts
const response = await client.registerInvoice({
  invoiceId: {
    issuerNif: 'B12345678',
    seriesNumber: 'A/2026/0001',
    issueDate: '2026-05-20',
  },
  invoiceType: 'F1',
  description: 'Consulting services — May 2026',
  recipients: [{ nif: '12345678Z', legalName: 'Customer SL' }],
  breakdown: [
    {
      tax: '01',
      regimeKey: '01',
      operationQualification: 'S1',
      taxRate: '21',
      taxBase: '100.00',
      taxAmount: '21.00',
    },
  ],
  totalTaxAmount: '21.00',
  totalAmount: '121.00',
});

console.log(response.csv);            // → AEAT CSV (Código Seguro de Verificación)
console.log(response.envelopeState);  // → 'Correcto' | 'ParcialmenteCorrecto' | 'Incorrecto'
console.log(response.records[0]?.state);

The response.records array carries one entry per submitted invoice with its per-record outcome (Correcto, AceptadoConErrores or Incorrecto) and any AEAT error code. See Error codes for the full catalog.

Rendering the mandatory QR ​

Every invoice issued under VERI*FACTU must carry a QR code linking back to the AEAT validation portal. The SDK renders it for you in PNG, SVG or DataURL form.

ts
import { buildQrUrl, renderQrPng } from 'verifactu-sdk/qr';

const url = buildQrUrl({
  mode: 'verifactu',
  environment: 'production',
  invoice: {
    issuerNif: 'B12345678',
    seriesNumber: 'A/2026/0001',
    issueDate: '2026-05-20',
    totalAmount: '121.00',
  },
});

const png = await renderQrPng(url, { sizeMm: 35 });
// → Buffer ready to embed in your PDF/HTML invoice.

The QR size must be 30–40 mm per the AEAT specification. See the QR guide for the layout requirements.

Querying previously submitted invoices ​

In verifactu mode (only) the AEAT exposes a paginated query endpoint. The SDK returns an async iterable so you never have to manage cursors yourself.

ts
for await (const page of client.queryInvoices({ year: '2026', period: '05' })) {
  for (const record of page.records) {
    console.log(record.invoiceId.seriesNumber, record.state);
  }
}

Cancelling ​

ts
await client.cancelInvoice({
  cancelledInvoiceId: {
    issuerNif: 'B12345678',
    seriesNumber: 'A/2026/0001',
    issueDate: '2026-05-20',
  },
  generatedBy: 'E',
});

Where to go next ​

  • Hash chain — how the SDK chains records and what the AEAT verifies.
  • Validations — the 23 business rules enforced locally.
  • Flow control — how the SDK respects TiempoEsperaEnvio and the 1 000-record-per-submission limit.
  • Testing — running the e2e suite against AEAT pre-production.

Released under the MIT license.