Facturar y cobrar desde tu aplicación
Recorrido completo para facturar y cobrar desde tu aplicación con UTILIA OS: identificar al usuario, guardar la tarjeta, emitir la factura y cobrar la cuota.
Actualizado el . 14 min de lectura
Incluido en los planes Profesional y Avanzado. Ver precios
Esta guía recorre el ciclo entero de facturar y cobrar desde tu aplicación: identificar al usuario, guardar su tarjeta, recoger su autorización, emitir la factura y enviársela, montar la cuota mensual, cobrar sin nadie delante y enterarte de todo por avisos.
Los ejemplos usan una aplicación inventada llamada Alinea. Su base de datos de usuarios y sus pantallas siguen siendo suyas; UTILIA OS aporta la numeración fiscal, la firma, el PDF, el correo, la pasarela y la contabilidad. Todas las facturas de Alinea son facturas de UTILIA OS con origen «aplicación externa», en la misma serie correlativa que las que emite tu equipo desde el panel.
Antes de empezar
- Una aplicación externa creada en UTILIA OS, con su clave de API activa.
- El plan de tu organización con los aplicativos que use cada parte: Integraciones para la pantalla y los usuarios, Finanzas para facturar, Finanzas y Pagos con tarjeta (Stripe) para cobrar con tarjeta, y las capacidades de facturación recurrente y de cobro automático para las cuotas y las autorizaciones.
- Facturar no exige pasarela. Con Integraciones y Finanzas, tu aplicación ya puede activar la facturación, emitir facturas, enviarlas por correo y registrar el cobro por transferencia. Stripe hace falta para cobrar con tarjeta, no para facturar.
- Un servidor propio. La clave de API nunca sale de él.
El recorrido de un extremo a otro
De registrar al usuario a cobrar su cuota
Usuarios externos
identify crea el usuario externo y su ficha de cliente en el CRM.Autorizaciones de cobro
El usuario acepta el texto de consent-text y lo registras como prueba.Finanzas
Facturar y cobrar
Cada periodo, UTILIA OS emite la factura, la cobra y, si lo tienes activado, la envía por correo.Tu servidor
SUBSCRIPTION_INVOICE_GENERATED, CHARGE_SUCCEEDED y el resto actualizan tu estado.
Paso 1: activar la facturación de la aplicación
Encender el interruptor
En Gestión de proyectos, Aplicaciones externas, tu aplicación, Facturación, la opción Activar la facturación permite a la aplicación emitir facturas y mover dinero.
Solo puede encenderla un Super Administrador, y al guardar se le pide un motivo de al menos veinte caracteres, que queda en el registro de auditoría con su nombre y la hora. Apagarla no exige nada: es la forma rápida de cortar.
Decidir si UTILIA OS envía las facturas
En la misma pantalla, Enviar las facturas por correo al emitirlas hace que cada factura emitida por la aplicación salga hacia el usuario final con la plantilla, el remitente y el PDF de tu organización, y pase a estado «Enviada».
Es una preferencia por defecto, no una obligación: cada llamada puede saltársela con
sendEmail.Comprobar la cuenta de cobro (solo si va a cobrar con tarjeta)
Los cobros con tarjeta se procesan con la cuenta Stripe de tu organización: no hay cuenta por aplicación. Su estado se ve en la misma pantalla de Facturación, con enlace a Finanzas › Ajustes › Pagos con tarjeta (Stripe). Sin una cuenta lista para operar, los cobros responden
409 STRIPE_ACCOUNT_NOT_READY.Este paso es opcional. Una organización sin la pasarela contratada llega hasta aquí igual: activa la facturación, emite, envía y anota el cobro por transferencia con
mark-paid. Los bloques de cobro con tarjeta aparecen con candado.Conceder los permisos de la aplicación
En Ajustes, Permisos de la aplicación, concede solo lo que vaya a usar. La pantalla distingue dos motivos por los que algo no está disponible, no incluido en el plan y no concedido a la aplicación, porque se arreglan en sitios distintos.
Suscribirse a los avisos
En Avisos a tu servidor, indica la dirección de tu servidor y marca los eventos. Guarda la clave de firma que te devuelve UTILIA OS: con ella comprobarás que cada aviso viene de verdad de UTILIA OS.
Paso 2: los permisos que necesita cada parte
Los permisos de la API (scopes) se conceden por familia. Estos son los del ciclo de facturación, con el aplicativo y la capacidad del plan que además hacen falta.
| Permiso | Para qué | Plan que exige |
|---|---|---|
crm:users:write | Dar de alta y actualizar usuarios finales | Integraciones |
crm:invoices:write | Crear facturas y registrar cobros fuera de la pasarela | Finanzas |
crm:invoices:issue | Emitir la factura (se exige junto al anterior al crear) | Finanzas |
crm:invoices:read | Listar, ver el detalle y descargar el PDF | Finanzas |
crm:invoices:send | Enviar la factura por correo | Finanzas |
crm:invoices:cancel | Anular una factura emitida | Finanzas |
billing:payment-methods:read y :write | Guardar y gestionar tarjetas | Finanzas y Pagos con tarjeta (Stripe) |
billing:charge | Cobrar una factura | Finanzas y Pagos con tarjeta (Stripe) |
billing:refund | Devolver dinero cobrado | Finanzas y Pagos con tarjeta (Stripe) |
billing:payment-authorizations:read, :write, :revoke | Autorizaciones de cobro | Finanzas y Pagos con tarjeta (Stripe), con cobro automático |
billing:subscriptions:read, :write | Cuotas recurrentes | Finanzas, con facturación recurrente |
billing:subscriptions:cancel | Dar de baja una cuota | Finanzas, con facturación recurrente |
billing:scheduled-charges:read, :write | Cobros programados | Finanzas y Pagos con tarjeta (Stripe), con cobro automático |
La pantalla de Facturación de la aplicación y su interruptor solo exigen Integraciones y Finanzas. La pasarela solo gobierna los cobros con tarjeta: guardar tarjetas, autorizaciones, charge, refund y los cobros programados.
Cinco permisos son críticos financieros porque mueven dinero o cortan un ingreso: billing:charge, billing:refund, billing:subscriptions:cancel, billing:payment-authorizations:write y crm:invoices:cancel. Los concede un Super Administrador con un motivo que queda en el registro de auditoría.
No hay periodo de gracia: un permiso que la aplicación no tenga produce 403 con errorCode: 'INSUFFICIENT_SCOPE' desde la primera llamada, y el mensaje no dice cuál falta. Concédelos antes de integrar. Las reglas completas están en Permisos de la API.
Paso 3: instalar el SDK
npm install @utilia-os/sdk-jsimport { UtiliaSDK } from '@utilia-os/sdk-js';
const sdk = new UtiliaSDK({
baseURL: 'https://os.utilia.ai/api',
apiKey: process.env.UTILIA_API_KEY!,
});La clave de API nunca llega al navegador
La clave tiene todos los permisos de tu aplicación, incluidos los que mueven dinero. Úsala siempre desde tu servidor: tu interfaz habla con tu servidor, y tu servidor con UTILIA OS. La clave empieza por utl_.
Paso 4: identificar al usuario
const user = await sdk.users.identify({
externalId: 'user_01HXYZ',
email: 'ana@estudiomarea.example',
name: 'Estudio Marea SL',
taxId: 'B76543210',
billingAddress: {
street: 'Avenida de Canarias 12, planta 3',
city: 'Las Palmas de Gran Canaria',
postalCode: '35001',
country: 'España',
province: 'Las Palmas',
},
metadata: { plan: 'pro' },
});
console.log(user.clientId);externalId es el identificador estable del usuario en Alinea, y es el que se usa en todas las llamadas posteriores como userId. La operación crea o actualiza: llámala sin comprobar antes si el usuario existe.
taxId y billingAddress son opcionales, pero conviene mandarlos aquí en cuanto Alinea los tenga: se copian a la ficha de cliente del CRM y a la de la pasarela, y son los datos que salen impresos en el PDF. Una factura emitida a un cliente sin identificador fiscal obliga a rectificarla después. Si no los mandas, el alta de la factura rellena los huecos con los datos del receptor, y lo que declare identify manda siempre. Omitir un campo no borra el que ya estaba guardado.
El puente con el CRM se monta solo
La primera vez que una aplicación factura, guarda una tarjeta o crea una suscripción para un usuario, UTILIA OS le crea su ficha de cliente en el CRM con su nombre, su NIF y su dirección, y un contacto de facturación con su correo. Desde ese momento, ese usuario existe para Finanzas, para el CRM y para la contabilidad como cualquier otro cliente, y la ficha muestra de qué aplicación viene. Si tu aplicación ya había creado la ficha con POST /external/v1/clients, pasa su identificador en clientId al identificar y se vinculan en lugar de duplicarse.
El puente solo trabaja con fichas creadas por la propia aplicación. Una ficha nativa del CRM, dada de alta por tu equipo desde el panel, nunca se toca desde la API externa: clientId la rechaza con 404 CLIENT_NOT_ACCESSIBLE, y la deduplicación automática por NIF o por correo tampoco la reutiliza. Si la coincidencia cae en una ficha nativa, se crea una ficha nueva y el hallazgo queda en el registro de auditoría para que tu equipo lo revise. Unir un usuario de Alinea con una ficha nativa sigue siendo posible, pero lo decide tu equipo desde el panel. Más detalle en Usuarios externos.
Paso 5: guardar la tarjeta
Guardar la tarjeta y autorizar el cobro son dos cosas distintas. Esta es la primera: la tarjeta queda registrada en la pasarela.
const setup = await sdk.paymentMethods.createSetupIntent({
externalId: 'user_01HXYZ',
email: 'ana@estudiomarea.example',
name: 'Estudio Marea SL',
});
// setup.clientSecret, setup.publishableKey, setup.stripeAccountIdTu interfaz monta el formulario de la pasarela con ese clientSecret y la cuenta que viene en stripeAccountId. El detalle está en Métodos de pago.
Paso 6: recoger la autorización de cobro
Con la tarjeta guardada, tu usuario todavía no ha autorizado nada. Para cobrarle sin que esté delante hace falta su consentimiento expreso, que es la prueba legal que respalda cada cargo futuro.
Pedir el texto vigente
TypeScriptconst consent = await sdk.paymentAuthorizations.getConsentText({ scope: 'RECURRING' }); // consent.text, consent.version, consent.companyNameMuéstraselo tal cual. Lleva el nombre fiscal de la organización: no lo reescribas ni lo reutilices entre organizaciones.
Registrar la aceptación
TypeScriptconst authorization = await sdk.paymentAuthorizations.create({ userId: 'user_01HXYZ', setupIntentId: 'seti_1OabcDEF', consentText: consent.text, consentVersion: consent.version, acceptedAt: new Date().toISOString(), acceptedIp: peticion.ip, acceptedUserAgent: peticion.headers['user-agent'], scope: 'RECURRING', });La IP y el navegador los manda tu servidor, porque la llamada sale de él. Junto con el texto, la versión y el momento, prueban quién aceptó y desde dónde. Si el texto o la versión no son los vigentes, la llamada falla con
422 PAYMENT_AUTHORIZATION_CONSENT_MISMATCHantes de consumir el SetupIntent.
Hay dos alcances y cada uno necesita su autorización: RECURRING cubre las cuotas de una suscripción y SCHEDULED_INVOICE, el cobro programado de una factura suelta. Una no vale para lo otro, así que una aplicación que haga las dos cosas recoge dos autorizaciones del mismo usuario.
Paso 7: emitir la factura y enviarla
const invoice = await sdk.invoices.create({
user: {
externalId: 'user_01HXYZ',
email: 'ana@estudiomarea.example',
name: 'Estudio Marea SL',
taxId: 'B76543210',
},
recipient: {
name: 'Estudio Marea SL',
taxId: 'B76543210',
address: 'Calle Mayor 1',
city: 'Las Palmas de Gran Canaria',
postalCode: '35001',
country: 'España',
email: 'administracion@estudiomarea.example',
},
taxSystem: 'IGIC',
lines: [
{ name: 'Plan Profesional', quantity: 1, unitPrice: 49.95, taxType: 'IGIC_7' },
],
sendEmail: true,
idempotencyKey: 'alinea_pedido_4821',
});
console.log(invoice.invoiceNumber, invoice.emailSent);La factura se numera con el contador correlativo de tu organización, se firma según VeriFactu si está activado y genera su PDF. sendEmail manda sobre la preferencia de la aplicación: true la envía aunque el interruptor esté apagado, y false la retiene aunque esté encendido.
emailSent en falso no es una factura fallida
La factura está emitida y numerada; solo ha fallado el correo. El envío ocurre después del cierre fiscal a propósito: un servidor de correo caído no puede tumbar una factura ya numerada. Reenvíala con sdk.invoices.send(...). No la vuelvas a crear nunca: sería una segunda factura.
Para reenviar, o para enviar en un momento distinto del de la emisión:
await sdk.invoices.send(invoice.id, {
userId: 'user_01HXYZ',
message: 'Adjuntamos la factura del mes de octubre.',
});Sin to, el destinatario es el contacto de facturación principal del cliente y, en su defecto, el correo del usuario externo.
Entregar la factura al usuario
El PDF de la API externa exige la clave de API, así que no sirve como enlace para tu usuario. Lo que se le entrega es el enlace público:
const link = await sdk.invoices.regeneratePublicLink(invoice.id, {
userId: 'user_01HXYZ',
});
// link.publicUrlLa dirección se entrega una sola vez
De un enlace emitido, UTILIA OS guarda solo su huella. Por eso getPublicLink es una lectura que devuelve publicUrl: null y solo informa de la vigencia y de las acciones permitidas, y por eso emitir uno nuevo revoca el anterior de esa factura. Guarda la dirección en tu base de datos la primera vez. La excepción es el enlace que devuelve un charge con REQUIRES_ACTION: ese es adicional y no revoca el que tu usuario ya tuviera. Todos permiten solo ver y pagar.
Si prefieres servir el PDF desde tu propia interfaz, descárgalo en tu servidor y entrégalo por una ruta tuya que compruebe el acceso de la persona:
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()));Paso 8: la cuota mensual
const subscription = await sdk.subscriptions.create({
userId: 'user_01HXYZ',
name: 'Alinea Profesional mensual',
frequency: 'MONTHLY_FIXED_DAY',
dayOfMonth: 1,
startDate: '2026-10-01',
automation: 'AUTO',
chargeMode: 'AUTO_STRIPE',
paymentAuthorizationId: authorization.id,
taxSystem: 'IGIC',
daysUntilDue: 7,
sendEmail: true,
lines: [
{ name: 'Plan Profesional', quantity: 1, unitPrice: 49.95, taxType: 'IGIC_7' },
],
metadata: { planId: 'pro_monthly' },
idempotencyKey: 'sub_user_01HXYZ_pro',
});A partir de aquí, cada periodo UTILIA OS emite la factura, la envía y la cobra, y te avisa de cada paso. Puedes consultar en cualquier momento qué toca a continuación:
const proximo = await sdk.subscriptions.getUpcomingCharge(subscription.id, {
userId: 'user_01HXYZ',
});
// proximo.nextInvoiceDate, proximo.amount, proximo.autoChargeReadyPedir chargeMode: "AUTO_STRIPE" sin una autorización vigente falla con 409 PAYMENT_AUTHORIZATION_REQUIRED. No pasa en silencio a cobro manual, porque te dejaría creyendo que tienes montado un cobro que nunca se produciría. El error trae un nextStep con las dos operaciones que faltan.
Pausar, reanudar y dar de baja son tres operaciones distintas. Pausar conserva el historial y la autorización; dar de baja corta el ingreso y no se deshace. El detalle está en Suscripciones.
Paso 9: cobrar sin nadie delante
Para cobrar ahora mismo una factura suelta contra la autorización del usuario:
const cobro = await sdk.invoices.charge(invoice.id, {
userId: 'user_01HXYZ',
idempotencyKey: `cobro_${invoice.id}`,
});
if (cobro.status === 'REQUIRES_ACTION') {
// El banco pide autenticación reforzada. El cobro NO se ha producido.
enviarAlUsuario(cobro.publicUrl);
}| Estado | Qué significa |
|---|---|
SUCCEEDED | Dinero cobrado. |
REQUIRES_ACTION | El banco pide autenticación reforzada. Lleva al usuario al publicUrl. |
PROCESSING | En curso. El resultado llega por aviso. |
FAILED | Rechazado, con failureCode y failureMessage. |
Cobrar con el usuario delante es otra operación: sdk.payments.payInvoice(...) devuelve un clientSecret para que tu interfaz monte el formulario de la pasarela y la persona confirme. sdk.invoices.charge(...) es el cobro sin nadie delante y exige la autorización previa. No son intercambiables.
Para cobrar en una fecha futura en lugar de ahora, usa los cobros programados, con una autorización de alcance SCHEDULED_INVOICE.
Paso 10: reintentos y avisos de morosidad
Cuando un cobro se rechaza, UTILIA OS lo reintenta solo hasta agotar los intentos configurados. No tienes que programar nada: te llegan los avisos.
| Aviso | Qué hacer en Alinea |
|---|---|
CHARGE_FAILED | Anotar el rechazo. willRetry dice si queda algún intento. |
CHARGE_RETRY_SCHEDULED | Nada; informar al usuario si procede. |
CHARGE_REQUIRES_ACTION | Pedir el enlace público y llevar al usuario a completarlo. |
CHARGE_RETRIES_EXHAUSTED | Cortar el servicio o abrir una gestión de cobro. Indica si la suscripción ha quedado pausada. |
PAYMENT_METHOD_EXPIRING | Pedir al usuario una tarjeta nueva antes de que falle la renovación. |
Paso 11: cobro fuera de la pasarela, reembolso y anulación
// El usuario pagó por transferencia
await sdk.invoices.markPaid(invoice.id, {
userId: 'user_01HXYZ',
method: 'TRANSFER',
reference: 'TRF-2026-10-0042',
});
// Devolver lo cobrado, entero o en parte
await sdk.invoices.refunds.create(invoice.id, {
userId: 'user_01HXYZ',
reason: 'requested_by_customer',
idempotencyKey: `devolucion_${invoice.id}`,
});
// Anular una factura emitida por error (no devuelve dinero)
await sdk.invoices.cancel(invoice.id, 'user_01HXYZ', 'Facturación duplicada');Anular y devolver son cosas distintas: anular corrige el documento, devolver mueve dinero.
Paso 12: recibir los avisos
Tu servidor expone una dirección HTTPS que acepta POST con JSON y comprueba la firma de cada petición. El SDK trae el verificador:
import express from 'express';
import { UtiliaSDK } from '@utilia-os/sdk-js';
const sdk = new UtiliaSDK({ baseURL: 'https://os.utilia.ai/api', apiKey: process.env.UTILIA_API_KEY! });
const app = express();
app.post('/webhooks/utilia', express.raw({ type: 'application/json' }), (req, res) => {
let evento;
try {
evento = sdk.webhooks.parse(req.body.toString('utf8'), req.headers, process.env.UTILIA_WEBHOOK_SECRET!);
} catch {
return res.status(401).send('Firma no válida');
}
const entrega = req.header('X-Utilia-Delivery-Id')!;
if (yaProcesado(entrega)) return res.status(200).send('OK');
switch (evento.event) {
case 'SUBSCRIPTION_INVOICE_GENERATED':
registrarFactura(evento.data);
break;
case 'CHARGE_SUCCEEDED':
renovarServicio(evento.data);
break;
case 'CHARGE_REQUIRES_ACTION':
pedirAutenticacion(evento.data);
break;
case 'CHARGE_RETRIES_EXHAUSTED':
suspenderServicio(evento.data);
break;
}
marcarProcesado(entrega);
res.status(200).send('OK');
});El verificador comprueba la firma sobre la cadena timestamp.rawBody y la ventana contra reenvíos. Usa el cuerpo crudo, sin volver a interpretarlo. El formato está en Firma HMAC, y el catálogo completo de avisos en Contenido de los avisos.
Cuando algo va mal
| Respuesta | Qué significa | Qué hacer |
|---|---|---|
403 con errorCode: 'MODULE_NOT_ENABLED' | El plan de tu organización no incluye el aplicativo. La respuesta trae su moduleKey. | Es una decisión comercial, no un fallo técnico. Contrata el aplicativo o retira esa parte de la integración. No reintentes. |
403 con errorCode: 'CAPABILITY_NOT_ENABLED' | El aplicativo está contratado, pero no la capacidad concreta. La respuesta trae su capabilityKey. | Igual que el anterior. El resto del aplicativo sigue funcionando. |
403 con errorCode: 'INSUFFICIENT_SCOPE' | La aplicación no tiene el permiso concedido. El mensaje no dice cuál falta, a propósito. | Concédelo en la ficha de la aplicación. No reintentes. |
403 con errorCode: 'BILLING_NOT_ENABLED' | La facturación de la aplicación está apagada. | Enciéndela en la pantalla Facturación de la aplicación. |
409 STRIPE_ACCOUNT_NOT_READY | La cuenta de cobro de tu organización no está lista. | Revisa Finanzas › Ajustes › Pagos con tarjeta (Stripe). |
429 | Has superado las 60 peticiones por minuto de los endpoints de facturas. | Espera lo que indique Retry-After. El SDK ya reintenta con espera creciente. |
Lista de comprobación antes de producción
- La clave de API vive solo en tu servidor, en una variable de entorno.
- La aplicación tiene concedidos exactamente los permisos que usa, ni uno más.
- Envías
idempotencyKeyen toda creación de factura, suscripción, cobro y reembolso. - Tu receptor de avisos comprueba la firma, valida la ventana de tiempo y descarta repetidos por
X-Utilia-Delivery-Id. - Tratas
emailSent: falsecomo un reenvío pendiente, nunca como una factura que rehacer. - Guardas el enlace público la primera vez que lo emites, en lugar de volver a emitirlo.
- Gestionas
CHARGE_REQUIRES_ACTIONyCHARGE_RETRIES_EXHAUSTED, no solo el cobro correcto. - No guardas nunca un
clientSecret: caduca enseguida.
Preguntas habituales
¿Qué correos envía UTILIA OS a mis usuarios y cuándo?
Solo si el interruptor Enviar las facturas por correo al emitirlas está encendido para tu aplicación, o si la llamada lo pide con sendEmail: true. En ese caso salen la factura al emitirse, el justificante al cobrarse y los avisos de vencimiento y recordatorio, con la plantilla y el remitente de tu organización. Con el interruptor apagado y sin sendEmail, UTILIA OS no escribe a tus usuarios: el envío es cosa tuya.
¿Puedo personalizar la numeración de mis facturas?
No. El contador es correlativo y compartido por toda la organización, como exige la normativa. Las facturas de tu aplicación se numeran en la misma serie que las que emite tu equipo desde el panel.
¿Puedo facturar en varias divisas?
No. Se factura en la divisa configurada en la organización. Si la cuenta de cobro declara otra divisa por defecto, el cobro se rechaza con 409 BILLING_CURRENCY_NOT_SUPPORTED en lugar de cobrar el mismo número de céntimos en otra moneda.
¿Puedo emitir facturas rectificativas desde la API?
No. Elegir el código legal de una rectificativa es una decisión de tu equipo, así que se emiten desde Finanzas o con la herramienta MCP correspondiente. Para devolver dinero, usa el reembolso.
¿Puede mi aplicación tener su propia cuenta de cobro?
No. Los cobros de todas las aplicaciones se procesan con la cuenta Stripe de la organización.
¿Puedo autenticarme sin clave de API, con OAuth de servidor a servidor?
No. La API externa se autentica con X-Api-Key. El flujo OAuth de UTILIA OS es para sesiones de personas, y no existe un permiso de cliente para aplicaciones sin usuario.
¿Dónde veo en UTILIA OS las facturas de mi aplicación?
En Gestión de proyectos › Aplicaciones externas › tu aplicación › Facturas, y también en Finanzas › Facturas, donde cada factura muestra de qué aplicación viene. El usuario final aparece como cliente en el CRM con la insignia de tu aplicación.
¿Qué pasa si mi aplicación pierde la conexión con UTILIA OS?
- Crear facturas o suscripciones: la clave de idempotencia te protege. Reintenta con la misma clave.
- Cobrar: si la pasarela ya confirmó el cargo, el aviso te llegará al volver.
- Avisos: hay cinco intentos con esperas de 5, 10, 20 y 40 segundos. La ventana total son minutos, no horas: ante una caída larga, cuadra los datos leyendo los listados en vez de confiar en los reintentos. Detalle en Entregas y reintentos.