Facturas
Emite facturas con numeración y registro fiscal desde tu aplicación, envíalas, cóbralas, entrega su enlace público, anúlalas, devuelve el dinero y programa cobros.
Actualizado el . 8 min de lectura
Incluido en todos los planes. Ver precios
El servicio de facturas emite facturas en nombre de tu organización para los usuarios de tu aplicación: con numeración correlativa, el registro VeriFactu cuando corresponde y el PDF con los datos fiscales del cliente. Las facturas aparecen en Finanzas, necesitas entrar con tu cuenta como cualquier otra, vinculadas a la ficha del cliente en el CRM. Desde el mismo servicio las envías por correo, las cobras, entregas su enlace público, las anulas, devuelves dinero y programas cobros. Funciona con la clave de tu aplicación externa, siempre desde tu servidor.
De la factura emitida al cobro confirmado
invoices.create
Emitir
Numeración correlativa y registro VeriFactu cuando corresponde.regeneratePublicLink o send
Entregar
Enlace público para verla y pagarla, o envío por correo con el PDF.
Antes de empezar
- La clave de tu aplicación debe tener la facturación activada y los scopes de cada acción (en la tabla siguiente). Úsala solo desde tu servidor.
- La organización necesita el aplicativo Finanzas en su plan. Para cobrar con tarjeta hace falta además Pagos con tarjeta (Stripe) y la cuenta de Stripe conectada; para emitir, no. Puedes comprobar si todo está listo para cobrar con
paymentMethods.getReadiness()(payment_methods.get_readiness()), explicado en Métodos de pago. - El usuario tiene que estar identificado, a ser posible con su NIF y su dirección de facturación, que son los que salen en el PDF.
| JavaScript | Python | Para qué | Scope |
|---|---|---|---|
create(datos) | create(datos) | Emitir una factura | crm:invoices:write y crm:invoices:issue |
list(userId, filtros?) | list(user_id, filtros) | Facturas de un usuario | crm:invoices:read |
get(id, userId) | get(invoice_id, user_id) | Detalle con sus líneas | crm:invoices:read |
downloadPdf(id, userId) | download_pdf(invoice_id, user_id) | PDF (Blob en JavaScript, bytes en Python) | crm:invoices:read |
getStats(userId) | get_stats(user_id) | Totales del usuario | crm:invoices:read |
send(id, datos) | send(invoice_id, datos) | Enviarla por correo con su PDF | crm:invoices:send |
charge(id, datos) | charge(invoice_id, datos) | Cobrarla sin el usuario delante | billing:charge |
markPaid(id, datos) | mark_paid(invoice_id, datos) | Registrar un cobro fuera de la pasarela | crm:invoices:write |
getPublicLink(id, { userId }) | get_public_link(invoice_id, user_id=...) | Vigencia del enlace público | crm:invoices:read |
regeneratePublicLink(id, datos) | regenerate_public_link(invoice_id, datos) | Emitir un enlace público nuevo | crm:invoices:write |
cancel(id, userId, motivo?) | cancel(invoice_id, user_id, reason) | Anularla | crm:invoices:cancel |
refunds.create(id, datos) | refunds.create(invoice_id, datos) | Devolver lo cobrado | billing:refund |
scheduledCharges.* | scheduled_charges.* | Programar cobros en una fecha | billing:scheduled-charges:read / :write |
Emitir una factura
create emite directamente: no hay borradores ni un paso posterior de emisión. Los datos principales:
| JavaScript | Python | Obligatorio | Qué es |
|---|---|---|---|
user | user | Sí | El usuario: externalId y email obligatorios; name, taxId y billingAddress opcionales. |
recipient | recipient | Sí | Datos fiscales del receptor: name obligatorio; taxId, address, city, postalCode y country. |
lines | lines | Sí | Al menos una línea: name, quantity, unitPrice (unit_price) y taxType (tax_type), entre otros. |
issueDate, dueDate | issue_date, due_date | No | Fechas AAAA-MM-DD. Sin fecha de emisión, hoy. |
taxSystem | tax_system | No | IVA, IGIC, IPSI, EXEMPT o INTERNATIONAL. |
sendEmail | send_email | No | Enviarla por correo al emitirla. Sin el campo, manda la preferencia de la aplicación. |
idempotencyKey | idempotency_key | No | Clave estable de esta factura para no emitirla dos veces. |
En cada línea, discount es un importe que se resta a esa línea (dos unidades de 100 € con descuento 10 dan 190 € antes de impuestos); discountPercent (discount_percent) es el porcentaje global de la factura. Para el IGIC hay además igicOperationType (IGIC_RETAIL o IGIC_GENERAL, solo si el emisor tiene declarado el régimen minorista) y, en líneas exentas, exemptionReason (EXEMPT_IGIC_RETAILER, EXEMPT_IGIC_INTERNAL, EXEMPT_EXPORT o EXEMPT_OTHER, que exige una exemptionNote de hasta 300 caracteres). El servidor comprueba la coherencia fiscal: que el SDK acepte un campo no hace válida cualquier combinación.
const invoice = await sdk.invoices.create({
user: {
externalId: 'user_01HXYZ',
email: 'cliente@empresa.com',
name: 'Empresa de Canarias SL',
taxId: 'B76543210',
},
recipient: {
name: 'Empresa de Canarias SL',
taxId: 'B76543210',
address: 'Avenida de Canarias 12',
city: 'Las Palmas de Gran Canaria',
postalCode: '35001',
country: 'España',
},
dueDate: '2026-11-08',
taxSystem: 'IGIC',
lines: [{ name: 'Plan Premium mensual', quantity: 1, unitPrice: 49.95, taxType: 'IGIC_7' }],
idempotencyKey: 'order_01HXYZ-invoice',
});
console.log(invoice.invoiceNumber, invoice.idempotent);La respuesta trae el id, el número de factura, el estado y idempotent, que vale true cuando la clave ya se había usado y el servidor te devuelve la factura existente en lugar de emitir otra. Que la factura se haya creado no significa por sí solo que la Agencia Tributaria haya aceptado su registro.
Una emisión dudosa no se repite a ciegas
create hace un único intento aunque configures reintentos: un fallo de red puede llegar después de emitir la factura. Conserva la idempotencyKey y los datos originales y vuelve a llamar con la misma clave, o consulta la factura antes. Nunca cambies la clave para «desbloquear» una emisión: crearías una segunda factura.
Si emites con sendEmail y la respuesta trae emailSent (email_sent) en falso, la factura está emitida y numerada: solo ha fallado el correo. Reenvíala con send, nunca vuelvas a crearla.
Comprobaciones locales: en JavaScript, si falta user, su externalId o su email, o recipient o su name, el SDK lanza un UtiliaSDKError de validación sin enviar nada; si lines está vacío, lanza un Error corriente con el mensaje «La factura debe tener al menos una línea». En Python esas comprobaciones las hacen los modelos de entrada.
Consultar facturas
list acepta filtros por status, page y limit. Ojo con la forma de la respuesta: en JavaScript la lista viene en data; en Python, en invoices.
Estados: DRAFT, ISSUED, SENT, PAID, PARTIALLY_PAID, OVERDUE, CANCELLED, DISPUTED, OVERDUE_UNCOLLECTABLE, REFUNDED, PARTIALLY_REFUNDED y RECTIFIED.
const page = await sdk.invoices.list('user_01HXYZ', { status: 'PAID', limit: 50 });
for (const item of page.data) {
console.log(`${item.invoiceNumber}: ${item.totalAmount} ${item.currency}`);
}
const stats = await sdk.invoices.getStats('user_01HXYZ');
console.log(`Pendiente: ${stats.totalPending} ${stats.currency}`);El PDF y el enlace público
downloadPdf descarga el PDF autenticado con tu clave: hazlo en tu servidor y entrégalo a tu usuario desde una ruta tuya que compruebe que puede verlo. El campo pdfUrl de la factura es una ruta protegida, no un enlace que puedas compartir.
Para que el usuario vea y pague la factura por su cuenta, emite un enlace público con regeneratePublicLink. El servidor solo guarda una huella del enlace, así que:
- La dirección te llega una sola vez, en la respuesta. Guárdala en tu base de datos.
getPublicLinksolo informa de vigencia, caducidad y acciones permitidas; supublicUrlllega siempre vacío.- Emitir un enlace nuevo revoca el anterior de esa factura.
Los enlaces de la API externa permiten ver y pagar (VIEW y PAY).
import { writeFile } from 'node:fs/promises';
const blob = await sdk.invoices.downloadPdf(invoice.id, 'user_01HXYZ');
await writeFile('factura.pdf', new Uint8Array(await blob.arrayBuffer()));
const link = await sdk.invoices.regeneratePublicLink(invoice.id, { userId: 'user_01HXYZ' });
await guardarEnlace(invoice.id, link.publicUrl, link.expiresAt);Enviar, cobrar y registrar pagos
Enviar por correo. send usa la plantilla, el remitente y la vía de envío de tu organización. Sin to, va al contacto de facturación principal del cliente o, si no hay, al correo del usuario. Solo alcanza a direcciones conocidas de esa factura (contactos de facturación del cliente, contactos del CRM marcados para recibir facturas, el correo del usuario y el destinatario de la emisión): cualquier otra en to o cc rechaza el envío entero con un 422 INVOICE_EMAIL_RECIPIENT_NOT_ALLOWED. Hay un cupo de 100 envíos por hora y aplicación; al superarlo, la respuesta es un 429 con la espera en Retry-After.
Cobrar sin el usuario delante. charge cobra contra una autorización de cobro que el usuario firmó antes; sin ella responde 409 PAYMENT_AUTHORIZATION_REQUIRED. Usa siempre la misma idempotencyKey para reintentar el mismo cobro: una clave nueva es un cobro nuevo. El resultado puede ser SUCCEEDED, PROCESSING, FAILED o REQUIRES_ACTION: este último no es un error, significa que el banco pide que el usuario se autentique; llévalo a la publicUrl que trae la respuesta (es un enlace adicional y no revoca el que ya tuviera). Para cobrar con el usuario delante, usa Cobros.
Registrar un pago fuera de la pasarela. markPaid (mark_paid) registra una transferencia, efectivo, cheque u otro medio (TRANSFER, CASH, CHECK, CARD u OTHER). Sin amount la marca como pagada entera; con él, registra un pago parcial.
await sdk.invoices.send(invoice.id, {
userId: 'user_01HXYZ',
message: 'Adjuntamos la factura de octubre.',
});
const cobro = await sdk.invoices.charge(invoice.id, {
userId: 'user_01HXYZ',
idempotencyKey: `cobro_${invoice.id}`,
});
if (cobro.status === 'REQUIRES_ACTION') llevarAlUsuario(cobro.publicUrl);
await sdk.invoices.markPaid(invoice.id, {
userId: 'user_01HXYZ',
method: 'TRANSFER',
reference: 'TRF-2026-10-0042',
});Anular y devolver dinero
cancel anula una factura emitida y genera el registro VeriFactu de anulación cuando corresponde. No se pueden anular borradores ni facturas ya anuladas. Hace un único intento: si falla por la red, consulta la factura antes de repetir.
Anular no devuelve dinero. Para eso está refunds.create: sin amountCents (amount_cents) devuelve todo lo cobrado; el motivo puede ser duplicate, fraudulent o requested_by_customer; y con createRectifyingInvoice (create_rectifying_invoice) pides en el mismo acto la factura rectificativa. Las rectificativas sueltas no se emiten desde el SDK: elegir su código legal es una decisión del equipo de contabilidad, desde el panel o con la herramienta MCP crm_invoices_rectifications.
await sdk.invoices.cancel(invoice.id, 'user_01HXYZ', 'Factura duplicada por error');
await sdk.invoices.refunds.create(otraFactura.id, {
userId: 'user_01HXYZ',
reason: 'requested_by_customer',
idempotencyKey: `devolucion_${otraFactura.id}`,
});Programar un cobro
scheduledCharges (scheduled_charges) cobra una factura en una fecha futura, contra una autorización de cobro vigente de alcance SCHEDULED_INVOICE (las de alcance RECURRING cubren suscripciones, no facturas sueltas; si no vale, responde 409 PAYMENT_AUTHORIZATION_NOT_ACTIVE). La fecha tiene que caer después de la ventana de cancelación de la organización.
schedule(facturaId, { userId, scheduledAt, paymentAuthorizationId })lo programa.list(facturaId, { userId })lista los de una factura, con el estado de cada uno (figura siguiente).cancel(cobroId, { userId })lo cancela mientras siga abierta la ventana (cancellableUntil); después, 409SCHEDULED_CHARGE_NOT_CANCELLABLE. Para cambiar la fecha, cancela y vuelve a programar.retryNow(cobroId, { userId })(retry_now) adelanta el reintento de un cobro fallido. El resultado llega por aviso a tu servidor. Hay un tope de cinco reintentos manuales por factura en 24 horas.
Estados de un cobro programado
Programado
PENDING
A la espera de su fecha.Se llega desde Programado cuando: cancel antes de cancellableUntil
Cancelado
CANCELLED
Estado final
No llega a cobrarse.
Se llega desde Programado cuando: Llega la fecha programada
En curso
RUNNING
Se está intentando el cobro.Se llega desde En curso cuando: El banco pide autenticación
Requiere autenticación
AUTH_REQUIRED
El usuario tiene que confirmar el cobro.Se llega desde En curso cuando: Fallo recuperable
Reintento programado
RETRY_SCHEDULED
Hay un reintento programado; retryNow lo adelanta.Se llega desde En curso cuando: Se agotan los reintentos o la tarjeta se rechaza sin remedio
Fallido
FAILED_PERMANENTLY
Estado final
No se volverá a intentar.
Se llega desde En curso cuando: El cobro se completa
Cobrado
COMPLETED
Estado final
La factura queda cobrada.
Otras transiciones
- Reintento programado pasa a En curso: Llega la hora del reintento, o lo adelantas con retryNow.