Ir al contenido

FactuAPI

FactuAPI es la API programática de Keirost. A diferencia de la API REST (orientada a CRUD), FactuAPI esta pensada para:

  • Operaciones transaccionales (todo o nada)
  • Crear documentos relacionados en una sola llamada
  • Acceso directo a la base de datos del tenant
  • Scripts de importación, exportación y automatización
  • Plugins que necesitan lógica de negocio compleja
API REST FactuAPI
CRUD individual sobre una entidad Transacciones multi-documento
Frontend → Backend Scripts, integraciones, plugins
Sin estado entre peticiones Sesión con conexion persistente
HTTP request/response TypeScript SDK directo
Operaciones simples Lógica de negocio compleja

Ventana de terminal
npm install @openfactu/plugin-sdk
import { FactuApi } from '@openfactu/plugin-sdk';

El método session() hace login, selecciona tenant y devuelve todo listo para operar:

const ctx = await FactuApi.session(
'admin@keirost.es', // email o username
'password123', // password
'Mi Empresa' // nombre del tenant (opcional, usa el primero)
);
// ctx contiene:
console.log(ctx.token); // JWT para API REST
console.log(ctx.tenantId); // ID del tenant
console.log(ctx.tenantName); // Nombre del tenant
console.log(ctx.db); // Cliente Drizzle del tenant
console.log(ctx.api); // FactuApiTransaction lista para usar
console.log(ctx.user); // { id, email, username, role }

Si necesitas controlar tu mismo la selección de tenant:

const session = await FactuApi.login('admin@keirost.es', 'password123');
// session contiene:
console.log(session.user); // { id, email, username, role }
console.log(session.tenants); // Array de tenants accesibles
console.log(session.token); // JWT string
// Seleccionar un tenant especifico
const tenant = session.tenants.find(t => t.tenantName === 'Mi Empresa');
const db = await FactuApi.getTenantDb(tenant.tenantId);
const api = FactuApi.connect(tenant.tenantId, db, session.user);
// Si ya tienes un token JWT
const db = await FactuApi.getTenantDb('tenant-id');
const api = FactuApi.connect('tenant-id', db, { id: 'user-id', role: 'admin' });

Para desarrollo remoto de plugins:

const ctx = await FactuApi.devSession(
'tu-dev-api-key',
'Mi Empresa'
);
await FactuApi.disconnect();

FactuAPI soporta 6 tipos de documento. Cada uno tiene un método factory en FactuApiTransaction:

Método Tipo Código Stock Estado inicial
salesInvoice() Factura de venta SINV Salida (OUT) Borrador (D)
purchaseInvoice() Factura de compra PINV Entrada (IN) Borrador (D)
salesOrder() Pedido de venta SO Ninguno Abierto (O)
purchaseOrder() Pedido de compra PO Ninguno Abierto (O)
salesDeliveryNote() Albaran de venta SDN Salida (OUT) Abierto (O)
purchaseDeliveryNote() Albaran de compra PDN Entrada (IN) Abierto (O)
const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
// 1. Crear documento
const invoice = ctx.api.salesInvoice();
// 2. Rellenar campos
invoice.partnerId = 'bp_cliente_001';
invoice.seriesId = 'series_factura_venta';
invoice.periodId = 'period_2026_05';
invoice.date = new Date();
invoice.warehouseId = 'wh_principal';
invoice.billToAddress = 'Calle Mayor 1, Madrid';
invoice.shipToAddress = 'Calle Mayor 1, Madrid';
// 3. Anadir lineas
invoice.addLine({
itemId: 'item_producto_a',
quantity: 10,
price: 99.90,
taxGroupId: 'tg_iva21',
warehouseId: 'wh_principal',
});
invoice.addLine({
itemId: 'item_producto_b',
quantity: 5,
price: 149.00,
taxGroupId: 'tg_iva21',
warehouseId: 'wh_principal',
});
// 4. Guardar
const result = await ctx.api.save(invoice);
console.log(result.id); // UUID pre-asignado
console.log(result.docNum); // Numero de documento asignado

Todos los documentos comparten estos campos:

Campo Tipo Descripción
id string UUID pre-asignado (read-only)
partnerId string ID del tercero (cliente/proveedor)
seriesId string ID de la serie documental
periodId string ID del periodo contable
date Date | string Fecha del documento
warehouseId string? Almacén (opcional)
billToAddress string? Dirección de facturación
shipToAddress string? Dirección de envío
deliveryDate Date | string? Fecha de entrega
orderId string? ID del pedido base
customFields Record<string, any> Campos personalizados
lines DiLine[] Líneas del documento (read-only)
invoice.addLine({
itemId: 'item_001', // ID del articulo
quantity: 10, // Cantidad
price: 100.00, // Precio unitario
taxGroupId: 'tg_iva21', // ID del grupo de impuestos
warehouseId: 'wh_001', // Almacen (opcional)
zoneId: 'zone_a1', // Zona (opcional)
uomId: 'uom_unidad', // Unidad de medida (opcional)
uomFactor: 1, // Factor de conversion (opcional)
batchDetails: [ // Lotes (opcional)
{ batchNum: 'LOT001', quantity: 10, expiryDate: new Date('2026-12-31'), zoneId: 'zone_a1' }
],
pluginData: { // Datos de plugin (opcional)
'mi-plugin': { customField: 'valor' }
},
});
const doc = FactuApi.create('SINV'); // o PINV, SO, PO, SDN, PDN
doc.fromBody({
partnerId: 'bp_001',
seriesId: 'series_001',
periodId: 'period_001',
date: '2026-05-08',
warehouseId: 'wh_001',
lines: [
{ itemId: 'item_001', quantity: 5, price: 100, taxGroupId: 'tg_iva21' }
],
customFields: { campo1: 'valor1' }
});
const result = await doc.save(tenantId, db, user);

El método transaction() ejecuta un bloque de código dentro de una transacción de base de datos. Si falla cualquier operación, se hace rollback automático de todo.

await FactuApi.transaction(ctx.tenantId, ctx.db, ctx.user, async (tx) => {
// Todas las operaciones aqui son atomicas
const invoice = tx.salesInvoice();
invoice.partnerId = 'bp_001';
invoice.addLine({ itemId: 'item_001', quantity: 10, price: 100, taxGroupId: 'tg_iva21' });
await tx.save(invoice);
const payment = await tx.registerPayment({
invoiceId: invoice.id,
direction: 'sales',
amount: 1210,
reference: 'PAGO-001',
});
});
// Si llega aqui, todo se confirmo. Si lanza error, rollback total.
const api = FactuApi.connect(tenantId, db, user);
// Usa api directamente sin transaccion explicita

Los IDs se generan al crear la instancia del documento, antes de guardar:

await FactuApi.transaction(ctx.tenantId, ctx.db, ctx.user, async (tx) => {
// 1. Crear albaran — el ID se genera ANTES de guardar
const albaran = tx.salesDeliveryNote();
albaran.partnerId = 'bp_001';
albaran.seriesId = 'series_albaran';
albaran.addLine({ itemId: 'item_001', quantity: 10, price: 100, taxGroupId: 'tg_iva21' });
console.log(albaran.id); // "a1b2c3d4-..." — ya disponible
// 2. Crear factura que referencia el albaran por su ID
const factura = tx.salesInvoice();
factura.partnerId = 'bp_001';
factura.seriesId = 'series_factura';
factura.orderId = albaran.id; // <-- vinculacion antes de guardar nada
factura.addLine({ itemId: 'item_001', quantity: 10, price: 100, taxGroupId: 'tg_iva21' });
// 3. Guardar ambos juntos: si falla uno, ninguno se persiste
await tx.save(albaran);
await tx.save(factura);
console.log('Albaran guardado:', albaran.id);
console.log('Factura guardada:', factura.id, 'vinculada a', factura.orderId);
});

Solo facturas (SINV, PINV) soportan asentamiento:

// Via REST endpoint
POST /api/factuapi/documents/SINV/{id}/post
// Via FactuApiTransaction (no disponible directamente, usar endpoint REST o db.update)
await db.update(schema.salesInvoices)
.set({ status: 'O' })
.where(eq(schema.salesInvoices.id, invoiceId));
// Via REST endpoint
POST /api/factuapi/documents/SINV/{id}/cancel
POST /api/factuapi/documents/PINV/{id}/cancel
POST /api/factuapi/documents/SDN/{id}/cancel
POST /api/factuapi/documents/PDN/{id}/cancel
POST /api/factuapi/documents/SO/{id}/cancel
POST /api/factuapi/documents/PO/{id}/cancel

// Buscar por ID o codigo
const partner = await ctx.api.getPartner('bp_001');
const partner = await ctx.api.getPartner('CLI-001'); // por codigo
// Listar todos
const partners = await ctx.api.getPartners();
// Buscar por ID o codigo
const item = await ctx.api.getItem('item_001');
const item = await ctx.api.getItem('PROD-A'); // por codigo
// Listar todos
const items = await ctx.api.getItems();
const categories = await ctx.api.getCategories();
const warehouses = await ctx.api.getWarehouses();
// Todas las series
const allSeries = await ctx.api.getSeries();
// Series de un tipo de documento
const invoiceSeries = await ctx.api.getSeries('SINV');
const taxGroups = await ctx.api.getTaxGroups();
const currencies = await ctx.api.getCurrencies();
const paymentMethods = await ctx.api.getPaymentMethods();
const paymentTerms = await ctx.api.getPaymentTerms();
const docTypes = await ctx.api.getDocumentTypes();

// Periodos abiertos
const openPeriods = await ctx.api.getOpenPeriods();
// Todos los periodos
const periods = await ctx.api.getPeriods();
// Listar todas las cuentas
const accounts = await ctx.api.getChartOfAccounts();
// Buscar cuenta por ID o codigo
const account = await ctx.api.getAccount('43000001');
const account = await ctx.api.getAccount('a1b2c3d4'); // por ID
const cc = await ctx.api.getCostCenter('CC-001');
const pc = await ctx.api.getProfitCenter('PC-001');
const order = await ctx.api.getInternalOrder('OI-001');
const rule = await ctx.api.getDimensionRule('account-id');

const payment = await ctx.api.registerPayment({
invoiceId: 'inv_001', // ID de la factura
direction: 'sales', // 'sales' = cobro, 'purchase' = pago
amount: 1210.00, // Importe
date: new Date(), // Fecha (opcional, por defecto hoy)
reference: 'TRANS-001', // Referencia obligatoria
paymentMethodId: 'pm_transfer', // Metodo de pago (opcional)
notes: 'Pago cliente XYZ', // Notas (opcional)
});
console.log(payment.id); // ID del pago
console.log(payment.journalEntryId); // ID del asiento generado (o null)

Si la factura esta asentada (status: 'O') y hay mapeos contables configurados, se genera y postea un asiento automáticamente.


const je = ctx.api.journalEntry();
je.date = new Date();
je.periodId = 'period_2026_05';
je.description = 'Asiento de regularizacion';
je.addLine({
accountId: '43000001',
debit: 1210,
partnerId: 'bp_001',
description: 'Cliente XYZ',
});
je.addLine({
accountId: '70000001',
credit: 1000,
description: 'Ventas',
});
je.addLine({
accountId: '47700001',
credit: 210,
description: 'IVA repercutido',
});
const result = await je.save(ctx.db, ctx.user.id);
console.log(result.id); // ID del asiento
// Postear el asiento
await ctx.api.postJournalEntry(result.id);
await ctx.api.postJournalEntry('je_001');
await ctx.api.reverseJournalEntry('je_001', 'Reversion por error');
const account = await ctx.api.resolveAccount('customer', 'bp_001');
const account = await ctx.api.resolveAccount('sales', null);
const account = await ctx.api.resolveAccount('vat_output', 'tg_iva21');
// Desde factura de venta
const invoice = await ctx.api.getSalesInvoice('inv_001'); // usar db.select
await ctx.api.createEntryFromSalesInvoice(invoice, invoiceLines);
// Desde factura de compra
await ctx.api.createEntryFromPurchaseInvoice(invoice, invoiceLines);
const payment = await ctx.api.registerPayment({ ... });
await ctx.api.createEntryFromPayment(
payment,
{ id: invoice.id, partnerId: invoice.partnerId, periodId: invoice.periodId, docNum: invoice.docNum },
'sales'
);

const preview = await ctx.api.previewPeriodClose('period_2026_05');
console.log(preview.errors); // Errores que impedirian cerrar
console.log(preview.warnings); // Advertencias
console.log(preview.summary); // Resumen de saldos
await ctx.api.closePeriod('period_2026_05');

// Listar todos
const employees = await ctx.api.getEmployees();
// Buscar por ID o codigo
const emp = await ctx.api.getEmployee('emp_001');
const emp = await ctx.api.getEmployee('E-001'); // por codigo
// Buscar departamento
const dept = await ctx.api.getDepartment('dept_001');
// Todos los contratos
const contracts = await ctx.api.getContracts();
// De un empleado especifico
const empContracts = await ctx.api.getContracts({ employeeId: 'emp_001' });
// Solo activos
const activeContracts = await ctx.api.getContracts({ activeOnly: true });
// Todos
const concepts = await ctx.api.getPayrollConcepts();
// Solo activos
const activeConcepts = await ctx.api.getPayrollConcepts({ activeOnly: true });
// Listar con filtros
const payrolls = await ctx.api.getPayrolls();
const empPayrolls = await ctx.api.getPayrolls({ employeeId: 'emp_001' });
const mayPayrolls = await ctx.api.getPayrolls({ year: 2026, month: 5 });
// Obtener una nomina con sus lineas
const payroll = await ctx.api.getPayroll('pay_001');
console.log(payroll.lines); // Array de lineas
// Anadir linea a nomina existente
await ctx.api.addPayrollLine('pay_001', {
conceptId: 'pc_sal_base',
concept: 'Salario Base',
type: 'earning',
amount: 1500,
quantity: 30,
rate: 50,
baseAmount: 1500,
accountId: '64000001',
});
// Aprobar nomina y generar asiento
await ctx.api.approvePayroll('pay_001', 'period_2026_05');
// Leer fichajes con filtros
const entries = await ctx.api.getTimeclockEntries();
const empEntries = await ctx.api.getTimeclockEntries({ employeeId: 'emp_001' });
const rangeEntries = await ctx.api.getTimeclockEntries({
from: '2026-05-01',
to: '2026-05-31',
kind: 'in',
});
// Registrar fichaje administrativo
await ctx.api.addTimeclockEntry({
employeeId: 'emp_001',
kind: 'in', // 'in' | 'out' | 'break_start' | 'break_end'
at: new Date(), // opcional, por defecto ahora
source: 'admin', // 'web' | 'kiosk' | 'admin'
notes: 'Entrada manual',
});
// Plantillas de turno
const templates = await ctx.api.getShiftTemplates();
// Asignaciones en un rango
const assignments = await ctx.api.getShiftAssignments({
employeeId: 'emp_001',
from: '2026-05-01',
to: '2026-05-31',
});
// Tipos de incidencia
const types = await ctx.api.getIncidentTypes();
// Incidencias con filtros
const incidents = await ctx.api.getIncidents();
const pending = await ctx.api.getIncidents({ status: 'pending' });
const empIncidents = await ctx.api.getIncidents({
employeeId: 'emp_001',
from: '2026-05-01',
to: '2026-05-31',
});
const agreements = await ctx.api.getCollectiveAgreements();
const evaluations = await ctx.api.getEvaluations();
const cycleEvals = await ctx.api.getEvaluations({ cycleId: 'cycle_001' });
const empEvals = await ctx.api.getEvaluations({ employeeId: 'emp_001' });
const objectives = await ctx.api.getObjectives();
const empObjectives = await ctx.api.getObjectives({ employeeId: 'emp_001' });
const activeObjectives = await ctx.api.getObjectives({ status: 'active' });
// Reglas de comision
const rules = await ctx.api.getCommissionRules();
// Devengos de comision con filtros
const accruals = await ctx.api.getCommissionAccruals();
const empAccruals = await ctx.api.getCommissionAccruals({ employeeId: 'emp_001' });
const pendingAccruals = await ctx.api.getCommissionAccruals({ status: 'pending' });
const monthlyAccruals = await ctx.api.getCommissionAccruals({ year: 2026, month: 5 });

// Listar con filtros
const tasks = await ctx.api.getTasks();
const myTasks = await ctx.api.getTasks({ assigneeId: 'emp_001' });
const projectTasks = await ctx.api.getTasks({ projectId: 'oi_001' });
const todoTasks = await ctx.api.getTasks({ status: 'todo' });
// Crear tarea
const task = await ctx.api.createTask({
title: 'Implementar informe trimestral',
description: 'Detalles de la tarea...',
status: 'todo', // 'todo' | 'in_progress' | 'done' | 'cancelled'
priority: 'high', // 'low' | 'normal' | 'high' | 'urgent'
assigneeId: 'emp_001',
internalOrderId: 'oi_001', // Proyecto asociado
departmentId: 'dept_001',
startDate: '2026-05-01',
dueDate: '2026-05-15',
estimatedHours: 16,
});
// Actualizar tarea
await ctx.api.updateTask(task.id, { status: 'in_progress', assigneeId: 'emp_002' });
const gantt = await ctx.api.getGantt({
from: '2026-05-01',
to: '2026-05-31',
projectId: 'oi_001', // opcional
});
console.log(gantt.tasks); // Array de tareas
console.log(gantt.dependencies); // Array de dependencias

Los documentos soportan campos personalizados a nivel de cabecera y línea:

const invoice = ctx.api.salesInvoice();
invoice.customFields = {
projectCode: 'PROJ-001',
deliveryInstructions: 'Dejar en recepcion',
};
invoice.addLine({
itemId: 'item_001',
quantity: 5,
price: 100,
taxGroupId: 'tg_iva21',
pluginData: {
'mi-plugin': {
serialNumber: 'SN123456',
warrantyMonths: 24,
},
},
});
await ctx.api.save(invoice);

FactuAPI expone el cliente Drizzle para consultas directas cuando necesitas algo que los helpers no cubren:

const db = ctx.db;
// Consulta directa con Drizzle
const result = await db
.select()
.from(schema.items)
.where(eq(schema.items.code, 'PROD-A'));
// Consulta compleja
const sales = await db
.select({
month: sql`EXTRACT(MONTH FROM ${schema.salesInvoices.date})`,
total: sql`SUM(${schema.salesInvoices.total})`,
})
.from(schema.salesInvoices)
.where(eq(schema.salesInvoices.status, 'O'))
.groupBy(sql`EXTRACT(MONTH FROM ${schema.salesInvoices.date})`);
// Dentro de una transaccion
await FactuApi.transaction(ctx.tenantId, ctx.db, ctx.user, async (tx) => {
const db = tx.db;
// Consultas aqui usan la misma transaccion
});

try {
await ctx.api.save(invoice);
} catch (error: any) {
if (error.message.includes('VALIDATION')) {
console.error('Campos invalidos:', error.message);
} else if (error.message.includes('stock')) {
console.error('Error de stock:', error.message);
} else if (error.message.includes('periodo')) {
console.error('Error de periodo:', error.message);
} else if (error.message.includes('no encontrado')) {
console.error('Entidad no existe:', error.message);
} else {
console.error('Error:', error.message);
}
}

En transacciones, cualquier error hace rollback automático:

try {
await FactuApi.transaction(ctx.tenantId, ctx.db, ctx.user, async (tx) => {
await tx.save(invoice);
await tx.save(deliveryNote); // Si esto falla, invoice tambien se revierte
});
} catch (error: any) {
console.error('Rollback automatico:', error.message);
}

import { FactuApi } from '@openfactu/plugin-sdk';
import { parse } from 'csv-parse/sync';
import * as fs from 'fs';
async function importClients() {
const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
const csv = fs.readFileSync('clientes.csv', 'utf-8');
const records = parse(csv, { columns: true });
for (const row of records) {
const id = crypto.randomUUID();
await ctx.db.insert(schema.businessPartners).values({
id,
code: row.codigo,
name: row.nombre,
nif: row.nif,
email: row.email,
phone: row.telefono,
groupId: 'gp_clientes',
isCustomer: true,
isSupplier: false,
status: 'active',
});
}
console.log(`Importados ${records.length} clientes`);
}
importClients().catch(console.error);

Ejemplo 2: Albaran + Factura en transacción atómica

Sección titulada «Ejemplo 2: Albaran + Factura en transacción atómica»
async function albaranYFactura() {
const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
await FactuApi.transaction(ctx.tenantId, ctx.db, ctx.user, async (tx) => {
// 1. Crear albaran
const dn = tx.salesDeliveryNote();
dn.partnerId = 'bp_cliente_001';
dn.seriesId = 'series_albaran';
dn.periodId = 'period_2026_05';
dn.addLine({
itemId: 'item_producto_a',
quantity: 10,
price: 99.90,
taxGroupId: 'tg_iva21',
warehouseId: 'wh_principal',
});
await tx.save(dn);
// 2. Crear factura desde albaran
const inv = tx.salesInvoice();
inv.partnerId = 'bp_cliente_001';
inv.seriesId = 'series_factura';
inv.periodId = 'period_2026_05';
inv.addLine({
itemId: 'item_producto_a',
quantity: 10,
price: 99.90,
taxGroupId: 'tg_iva21',
warehouseId: 'wh_principal',
});
await tx.save(inv);
console.log('Albaran:', dn.id, dn.docNum);
console.log('Factura:', inv.id, inv.docNum);
});
}

Ejemplo 3: Pedido → Albaran → Factura (pipeline completo)

Sección titulada «Ejemplo 3: Pedido → Albaran → Factura (pipeline completo)»
async function pipelineCompleto() {
const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
await FactuApi.transaction(ctx.tenantId, ctx.db, ctx.user, async (tx) => {
// 1. Pedido de venta
const order = tx.salesOrder();
order.partnerId = 'bp_cliente_001';
order.seriesId = 'series_pedido';
order.periodId = 'period_2026_05';
order.addLine({ itemId: 'item_a', quantity: 10, price: 100, taxGroupId: 'tg_iva21' });
await tx.save(order);
// 2. Albaran desde pedido
const dn = tx.salesDeliveryNote();
dn.partnerId = 'bp_cliente_001';
dn.seriesId = 'series_albaran';
dn.periodId = 'period_2026_05';
dn.orderId = order.id;
dn.addLine({ itemId: 'item_a', quantity: 10, price: 100, taxGroupId: 'tg_iva21' });
await tx.save(dn);
// 3. Factura desde albaran
const inv = tx.salesInvoice();
inv.partnerId = 'bp_cliente_001';
inv.seriesId = 'series_factura';
inv.periodId = 'period_2026_05';
inv.addLine({ itemId: 'item_a', quantity: 10, price: 100, taxGroupId: 'tg_iva21' });
await tx.save(inv);
});
}

Ejemplo 4: Facturación masiva mensual desde pedidos

Sección titulada «Ejemplo 4: Facturación masiva mensual desde pedidos»
async function facturacionMensual() {
const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
// Obtener pedidos abiertos del mes
const pedidos = await ctx.db
.select()
.from(schema.salesOrders)
.where(
and(
eq(schema.salesOrders.status, 'O'),
gte(schema.salesOrders.date, '2026-04-01'),
lte(schema.salesOrders.date, '2026-04-30'),
)
);
for (const pedido of pedidos) {
await FactuApi.transaction(ctx.tenantId, ctx.db, ctx.user, async (tx) => {
// Obtener lineas del pedido
const lines = await tx.db
.select()
.from(schema.salesOrderLines)
.where(eq(schema.salesOrderLines.orderId, pedido.id));
// Crear albaran
const dn = tx.salesDeliveryNote();
dn.partnerId = pedido.partnerId;
dn.seriesId = 'series_albaran';
dn.periodId = pedido.periodId;
dn.orderId = pedido.id;
for (const line of lines) {
dn.addLine({
itemId: line.itemId,
quantity: line.quantity,
price: line.price,
taxGroupId: line.taxGroupId,
});
}
await tx.save(dn);
// Crear factura
const inv = tx.salesInvoice();
inv.partnerId = pedido.partnerId;
inv.seriesId = 'series_factura';
inv.periodId = pedido.periodId;
for (const line of lines) {
inv.addLine({
itemId: line.itemId,
quantity: line.quantity,
price: line.price,
taxGroupId: line.taxGroupId,
});
}
await tx.save(inv);
console.log(`Facturado pedido ${pedido.docNum} → albaran ${dn.docNum} → factura ${inv.docNum}`);
});
}
}
async function generarNominas() {
const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
const empleados = await ctx.api.getEmployees();
for (const emp of empleados) {
if (emp.status !== 'active') continue;
// Obtener contrato activo
const contracts = await ctx.api.getContracts({ employeeId: emp.id, activeOnly: true });
const contract = contracts[0];
if (!contract) continue;
const grossSalary = parseFloat(contract.grossSalary);
// Crear nomina
const payrollId = crypto.randomUUID();
await ctx.db.insert(schema.payrolls).values({
id: payrollId,
employeeId: emp.id,
periodYear: 2026,
periodMonth: 5,
status: 'draft',
});
// Linea: salario base
await ctx.api.addPayrollLine(payrollId, {
concept: 'Salario Base',
type: 'earning',
amount: grossSalary / 12,
});
// Linea: IRPF
await ctx.api.addPayrollLine(payrollId, {
concept: 'IRPF',
type: 'deduction',
amount: -(grossSalary / 12 * 0.19),
});
// Linea: Seguridad Social (patronal)
await ctx.api.addPayrollLine(payrollId, {
concept: 'SS Patronal',
type: 'employer_cost',
amount: grossSalary / 12 * 0.3,
});
console.log(`Nomina creada para ${emp.name}: ${payrollId}`);
}
}

Ejemplo 6: Exportar facturas a JSON (previo a Excel)

Sección titulada «Ejemplo 6: Exportar facturas a JSON (previo a Excel)»
async function exportarFacturas() {
const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
const facturas = await ctx.db
.select()
.from(schema.salesInvoices)
.where(
and(
gte(schema.salesInvoices.date, '2026-01-01'),
lte(schema.salesInvoices.date, '2026-03-31'),
eq(schema.salesInvoices.status, 'O'),
)
);
const rows = await Promise.all(
facturas.map(async (f) => {
const [partner] = await ctx.db
.select()
.from(schema.businessPartners)
.where(eq(schema.businessPartners.id, f.partnerId));
return {
numero: f.docNum,
fecha: f.date,
cliente: partner?.name || f.partnerId,
total: f.total,
estado: f.status,
};
})
);
fs.writeFileSync('facturas_q1.json', JSON.stringify(rows, null, 2));
console.log(`Exportadas ${rows.length} facturas`);
}
async function asientoManual() {
const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
const je = ctx.api.journalEntry();
je.date = new Date();
je.periodId = 'period_2026_05';
je.description = 'Asiento de regularizacion IVA';
je.addLine({ accountId: '47700001', debit: 2100, description: 'Haber IVA' });
je.addLine({ accountId: '47200001', credit: 1800, description: 'Debe IVA' });
je.addLine({ accountId: '47500001', credit: 300, description: 'A pagar' });
const result = await je.save(ctx.db, ctx.user.id);
await ctx.api.postJournalEntry(result.id);
console.log('Asiento posteado:', result.id);
}

Ejemplo 8: Pipeline de cobro con asiento automático

Sección titulada «Ejemplo 8: Pipeline de cobro con asiento automático»
async function cobroConAsiento() {
const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
await FactuApi.transaction(ctx.tenantId, ctx.db, ctx.user, async (tx) => {
// 1. Crear factura
const inv = tx.salesInvoice();
inv.partnerId = 'bp_cliente_001';
inv.seriesId = 'series_factura';
inv.periodId = 'period_2026_05';
inv.addLine({ itemId: 'item_001', quantity: 1, price: 1000, taxGroupId: 'tg_iva21' });
const saved = await tx.save(inv);
// 2. Asentar la factura (necesario para que el pago genere asiento)
await tx.db.update(schema.salesInvoices)
.set({ status: 'O' })
.where(eq(schema.salesInvoices.id, saved.id));
// 3. Registrar cobro
const payment = await tx.registerPayment({
invoiceId: saved.id,
direction: 'sales',
amount: 1210,
reference: 'COBRO-001',
paymentMethodId: 'pm_transfer',
notes: 'Cobro factura prueba',
});
console.log('Factura:', saved.id);
console.log('Pago:', payment.id);
console.log('Asiento:', payment.journalEntryId);
});
}

Métodos estaticos de la clase FactuApi:

Método Parámetros Retorno Descripción
create(docType) DocType DiDocument Crea documento por tipo (SINV, PINV, SO, PO, SDN, PDN)
salesInvoice() SalesInvoice Factory de factura venta
purchaseInvoice() PurchaseInvoice Factory de factura compra
salesOrder() SalesOrder Factory de pedido venta
purchaseOrder() PurchaseOrder Factory de pedido compra
salesDeliveryNote() SalesDeliveryNote Factory de albaran venta
purchaseDeliveryNote() PurchaseDeliveryNote Factory de albaran compra
transaction(tenantId, db, user, fn) string, any, any, callback Promise<T> Ejecuta callback en transacción atómica
connect(tenantId, db, user) string, any, any FactuApiTransaction Conexion sin transacción explicita
getTenants() Promise<Tenant[]> Lista todos los tenants
getTenant(tenantId) string Promise<Tenant | null> Obtener tenant por ID
getTenantByName(name) string Promise<Tenant | null> Buscar tenant por nombre
getTenantDb(tenantId) string Promise<db> Cliente Drizzle de un tenant
login(emailOrUsername, password) string, string Promise<Session> Login y lista de tenants
session(emailOrUsername, password, tenantName?) string, string, string? Promise<Context> Login + tenant + conexion lista
disconnect() Promise<void> Cierra todas las conexiones

Métodos disponibles en ctx.api (instancia de FactuApiTransaction):

  • salesInvoice()SalesInvoice
  • purchaseInvoice()PurchaseInvoice
  • salesOrder()SalesOrder
  • purchaseOrder()PurchaseOrder
  • salesDeliveryNote()SalesDeliveryNote
  • purchaseDeliveryNote()PurchaseDeliveryNote
  • create(docType)DiDocument
  • journalEntry()JournalEntry
  • save(doc)Promise<{ id: string; docNum: number }>
  • getPartner(idOrCode)Promise<Partner \| null>
  • getPartners()Promise<Partner[]>
  • getItem(idOrCode)Promise<Item \| null>
  • getItems()Promise<Item[]>
  • getCategories()Promise<Category[]>
  • getWarehouses()Promise<Warehouse[]>
  • getSeries(docType?)Promise<Series[]>
  • getTaxGroups()Promise<TaxGroup[]>
  • getCurrencies()Promise<Currency[]>
  • getPaymentMethods()Promise<PaymentMethod[]>
  • getPaymentTerms()Promise<PaymentTerm[]>
  • getDocumentTypes()Promise<DocumentType[]>
  • getOpenPeriods()Promise<Period[]>
  • getPeriods()Promise<Period[]>
  • getChartOfAccounts()Promise<Account[]>
  • getAccount(idOrCode)Promise<Account \| null>
  • getCostCenter(idOrCode)Promise<CostCenter \| null>
  • getProfitCenter(idOrCode)Promise<ProfitCenter \| null>
  • getInternalOrder(idOrCode)Promise<InternalOrder \| null>
  • getDimensionRule(accountId)Promise<DimensionRule \| null>
  • registerPayment(opts)Promise<{ id: string; journalEntryId: string \| null }>
  • postJournalEntry(entryId)Promise<void>
  • reverseJournalEntry(entryId, description?)Promise<void>
  • resolveAccount(kind, key?)Promise<Account \| null>
  • createEntryFromSalesInvoice(invoice, lines?)Promise<JournalEntry \| null>
  • createEntryFromPurchaseInvoice(invoice, lines?)Promise<JournalEntry \| null>
  • createEntryFromPayment(payment, invoice, direction)Promise<JournalEntry \| null>
  • previewPeriodClose(periodId)Promise<PreviewResult>
  • closePeriod(periodId)Promise<void>
  • getEmployees()Promise<Employee[]>
  • getEmployee(idOrCode)Promise<Employee \| null>
  • getDepartment(idOrCode)Promise<Department \| null>
  • getContracts(opts?)Promise<Contract[]>
  • getPayrollConcepts(opts?)Promise<PayrollConcept[]>
  • getPayrolls(opts?)Promise<Payroll[]>
  • getPayroll(id)Promise<Payroll & { lines } \| null>
  • addPayrollLine(payrollId, line)Promise<PayrollLine>
  • approvePayroll(payrollId, periodId?)Promise<JournalEntry \| null>
  • getTimeclockEntries(opts?)Promise<TimeclockEntry[]>
  • addTimeclockEntry(entry)Promise<TimeclockEntry>
  • getShiftAssignments(opts?)Promise<ShiftAssignment[]>
  • getShiftTemplates()Promise<ShiftTemplate[]>
  • getIncidentTypes()Promise<IncidentType[]>
  • getIncidents(opts?)Promise<Incident[]>
  • getCollectiveAgreements()Promise<CollectiveAgreement[]>
  • getEvaluations(opts?)Promise<Evaluation[]>
  • getObjectives(opts?)Promise<Objective[]>
  • getCommissionRules()Promise<CommissionRule[]>
  • getCommissionAccruals(opts?)Promise<CommissionAccrual[]>
  • getTasks(opts?)Promise<Task[]>
  • createTask(task)Promise<Task>
  • updateTask(id, patch)Promise<Task>
  • getGantt(opts?)Promise<{ tasks; dependencies }>