Facturas
Emite facturas ordinarias y simplificadas desde tu aplicación, envíalas, cóbralas, anúlalas, rectifícalas, devuelve el dinero y programa cobros.
Actualizado el . 13 min de lectura
Incluido en todos los planes. Ver precios
El servicio de facturas emite facturas ordinarias y simplificadas en nombre de tu organización para los usuarios de tu aplicación: con numeración correlativa (la de tu organización o la de una serie reservada a tu aplicación) 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, las rectificas, 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 PDF con los datos fiscales.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 |
listSeries() | list_series() | Series en las que tu aplicación puede numerar (4.4.0) | crm:invoices:read |
getEuVatRates(filtros?) | get_eu_vat_rates(date=..., country=...) | Tipos de IVA vigentes de la Unión para la ventanilla única (4.4.0, Próximamente) | 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 con la pasarela de UTILIA OS | billing:refund |
rectifications.create(id, datos) | rectifications.create(invoice_id, datos) | Emitir una rectificativa (4.3.0) | crm:invoices:rectify, y crm:invoices:write con refund |
rectifications.list(id, userId) | rectifications.list(invoice_id, user_id) | Rectificativas de una factura y lo que queda por rectificar (4.3.0) | crm:invoices:read |
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. |
type | type | No | STANDARD (ordinaria, por defecto) o SIMPLIFIED (simplificada, hasta 400 € impuestos incluidos y con su propia serie T-). Desde la 4.3.0. |
seriesCode | series_code | No | Código de una serie de tu aplicación (de listSeries()) o DEFAULT para la de siempre. Sin él, la serie por defecto de tu aplicación para el tipo o, si no tiene, la de siempre. Desde la 4.4.0. |
recipient | recipient | En la ordinaria | Datos fiscales del receptor: name obligatorio si lo mandas; taxId, address, city, postalCode y country. En la simplificada es opcional: sin él se imprime «Consumidor final»; con NIF exige la dirección completa. |
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 expedición, hoy en la zona horaria de tu organización. Desde la 4.4.0 la expedición no puede ser futura ni anterior a la de la última factura de la misma serie (INVOICE_ISSUE_DATE_OUT_OF_ORDER). |
taxSystem | tax_system | No | IVA, IGIC, IPSI, EXEMPT, INTERNATIONAL o, desde la 4.4.0, EU_OSS (ventanilla única del IVA, Próximamente; ver más abajo). |
pricesIncludeTax | prices_include_tax | No | Los precios de las líneas llevan impuestos y el total de cada línea es exactamente lo cobrado. Desde la 4.3.0. |
payment | payment | No | Cobro que ya hiciste con tu propia pasarela: method, paidAt (paid_at) y reference. La factura nace cobrada. Desde la 4.3.0. |
consumerResidence | consumer_residence | No | Residencia del consumidor (region, countryCode, evidence), cotejada con el sistema fiscal. Desde la 4.3.0. |
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. |
Las reglas de cada campo nuevo, los errores y un ejemplo completo de una simplificada de 39,99 € cobrada en el acto están en Facturas de la API REST.
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, EXEMPT_OTHER, que exige una exemptionNote de hasta 300 caracteres, o EXEMPT_IGIC_SMALL_BUSINESS, la del régimen del pequeño empresario del IGIC, que UTILIA OS pone sola en las líneas exentas sin causa cuando tu organización está en ese régimen). 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 la respuesta trae emailSent (email_sent) en falso, la factura está emitida y numerada: solo falta el correo. Nunca vuelvas a crearla. El servidor dice además en emailDelivery qué ha pasado: con retrying, UTILIA OS la está reenviando sola y no tienes que hacer nada; con failed o withheld, resuelve el motivo y envíala con send. Los motivos están en El correo de la factura. Los tipos del SDK declaran emailDelivery y displayNumber (email_delivery y display_number) a partir de la 4.5.0.
Comprobaciones locales: en JavaScript, si falta user, su externalId o su email, si una factura que no es simplificada no lleva recipient, o si recipient llega sin 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: una ordinaria sin recipient falla al construir el modelo con ValueError.
La respuesta trae también, desde la 4.3.0, type, legalMentions (legal_mentions), las menciones impresas, y originalInvoiceId (original_invoice_id); si mandaste payment, paymentRegistered (payment_registered) dice si el cobro quedó registrado. Desde la 4.4.0, seriesCode (series_code) dice en qué serie se numeró (DEFAULT en la de siempre). En el detalle, cada línea trae exempt: úsalo en lugar de taxRate, porque un tipo cero no es una exención.
Series de tu aplicación
Desde la 4.4.0, tu organización puede reservar a tu aplicación una o varias series (por ejemplo, APP-202600001) para que sus facturas no se intercalen con las del equipo, y fijar cuál se usa por defecto. Las crea una persona de tu organización en UTILIA OS; tu aplicación no puede crearlas ni cambiarlas, solo consultarlas con listSeries() y elegir entre ellas con seriesCode. Cómo se elige la serie, los errores (INVOICE_SERIES_NOT_AVAILABLE, INVOICE_SERIES_TYPE_MISMATCH, INVOICE_SERIES_INACTIVE) y la fecha de expedición están en Series disponibles.
const { series } = await sdk.invoices.listSeries();
const propia = series.find((s) => s.documentType === 'STANDARD' && s.isAppDefault);
console.log(propia?.code, propia?.example); // APP APP-202600001
const factura = await sdk.invoices.create({ ...datosDeLaFactura, seriesCode: 'APP' });
console.log(factura.invoiceNumber, factura.seriesCode); // APP-202600001 APPLa lista trae también, solo informativa (selectable: false), la serie de rectificativas: tus rectificativas van siempre a ella, aunque la original esté en una serie de tu aplicación. Nunca devuelve el siguiente número.
Ventanilla única del IVA (Próximamente)
La ventanilla única del IVA está en «Próximamente»: todavía no está disponible para las organizaciones, y cualquier dato suyo responde 403 FEATURE_COMING_SOON. Compruébalo con isFeatureComingSoonError(error) (is_feature_coming_soon_error(exc) en Python) y getComingSoonFeature(error) (get_coming_soon_feature), que devuelve featureKey y featureLabel. No es un bloqueo de plan: isPlanError() responde false.
Desde la 4.4.0 el SDK ya trae su contrato:
taxSystem: 'EU_OSS'(TaxSystem.EU_OSS), con líneasEU_VATque llevanvatRateClass(vat_rate_class) yossSupplyType(oss_supply_type). UTILIA OS resuelve y congela el porcentaje del país del consumidor.consumerResidence.evidenceItems(evidence_items, conOssLocationEvidenceItemen Python): la prueba de la ubicación del consumidor.getEuVatRates({ date, country })(get_eu_vat_rates(date=..., country=...), asíncrono y síncrono): los tipos vigentes de la Unión.- En las respuestas,
taxSystem,consumerCountryCodeyossSchemeen cada factura, yvatRateClassyossSupplyTypeen cada línea. En Python, leer una facturaEU_OSSno falla. - Los códigos
OSS_*enINVOICE_ERROR_CODES(InvoiceErrorCode).
La ventanilla única del IVA está en «Próximamente»: hasta que esté disponible para tu organización, cualquier dato suyo responde 403 FEATURE_COMING_SOON.
Consultar facturas
list acepta filtros por status, type (desde la 4.3.0: STANDARD, SIMPLIFIED, RECTIFYING o CREDIT_NOTE), seriesCode (series_code, desde la 4.4.0), 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. 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, y es solo para una factura que no debió emitirse. Para devolver lo cobrado con la pasarela de UTILIA OS 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, con el código legal que decide UTILIA OS.
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}`,
});Rectificar una factura
Desde la 4.3.0, rectifications.create emite la rectificativa de una
factura de tu aplicación cuando devolviste el dinero con tu propia
pasarela (cause: 'REFUND') o un contracargo deja la venta sin efecto
(cause: 'CHARGEBACK'). Tú no eliges el código legal: UTILIA OS emite
R5 si la original es una simplificada sin destinatario identificado y
R1 en los demás casos, siempre por diferencias y con importes
negativos.
amount(con impuestos, 2 decimales como máximo) rectifica una parte; sin él, todo lo que quede.refund(reference,refundedAt,method) registra la devolución que ya hiciste en tu pasarela, sin mover dinero. Exige además el scopecrm:invoices:writey que la factura conste cobrada fuera de la pasarela de UTILIA OS.idempotencyKey(idempotency_key) es obligatoria. SioperationStatus(operation_status) llega enreview_required, repite la misma llamada con la misma clave para terminarla.- La fecha de expedición es siempre la de hoy.
- La rectificativa va siempre a la serie de rectificativas (
R-), también si la original está en una serie de tu aplicación.
rectifications.list devuelve las rectificativas de una factura,
remainingRectifiableAmount y, cuando es menor,
rectifiableWithRefundAmount. La rectificativa es una factura más: se
lee, se descarga y se envía con get, downloadPdf y send usando su
id.
const resultado = await sdk.invoices.rectifications.create(factura.id, {
userId: 'user_01HXYZ',
idempotencyKey: `devolucion_${factura.id}`,
cause: 'REFUND',
refund: { reference: 're_3QabcDEF' },
});
console.log(resultado.rectification.rectificationCode); // 'R5' o 'R1'
console.log(resultado.remainingRectifiableAmount);Los errores de negocio traen sus datos en error.details desde la
4.3.0: missingFields, lineNumber o remainingRectifiableAmount. Las
reglas completas, la advertencia sobre los contracargos y la tabla de
errores están en Facturas rectificativas.
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.