# 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.

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 **Nubeta**. Su base de datos de usuarios y sus pantallas siguen siendo suyas; UTILIA OS aporta la numeración fiscal, el PDF, el correo y la pasarela. Todas las facturas de Nubeta son facturas de UTILIA OS con origen «aplicación externa», en la misma serie correlativa que las que emite tu equipo desde el panel, salvo que tu organización reserve a Nubeta una serie propia (ver [Series disponibles](https://os.utilia.ai/docs/developers/api/invoices#series-disponibles)).

## 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.
- **Si cobras con tu propia pasarela** y solo quieres que UTILIA OS facture, sigue la guía [Facturar cuando cobras con tu propia pasarela](https://os.utilia.ai/docs/developers/api/own-gateway-billing).
- Un servidor propio. La clave de API nunca sale de él.

> **Consejo: Prueba sin emitir facturas reales**
>
> Cada factura emitida lleva un número correlativo que no se puede borrar. Antes de la primera real, comprueba tu cuerpo con `POST /external/v1/invoices/validate`, descarga su PDF de muestra y recibe avisos de ejemplo en tu servidor. Todo está en [Probar la facturación sin emitir facturas](https://os.utilia.ai/docs/developers/api/invoice-testing). El diagnóstico de tu aplicación, que dice qué le falta antes de facturar, está en [Probar tu integración](https://os.utilia.ai/docs/developers/getting-started/testing-quickstart).

## El recorrido de un extremo a otro

**De registrar al usuario a cobrar su cuota**

1. [**Identificar al usuario**](https://os.utilia.ai/docs/developers/api/users) (Usuarios externos): identify crea el usuario externo y su ficha de cliente en el CRM.
2. [**Guardar la tarjeta**](https://os.utilia.ai/docs/developers/api/payment-methods) (Métodos de pago): setup-intent deja la tarjeta guardada en la pasarela.
3. [**Recoger la autorización**](https://os.utilia.ai/docs/developers/api/payment-authorizations) (Autorizaciones de cobro): El usuario acepta el texto de consent-text y lo registras como prueba.
4. [**Dar de alta la cuota**](https://os.utilia.ai/docs/developers/api/subscriptions) (Suscripciones): Cuota mensual con cobro automático.
5. **Facturar y cobrar** (Finanzas): Cada periodo, UTILIA OS emite la factura, la cobra y, si lo tienes activado, la envía por correo.
6. [**Recibir los avisos**](https://os.utilia.ai/docs/developers/webhooks/events) (Tu servidor): SUBSCRIPTION_INVOICE_GENERATED, CHARGE_SUCCEEDED y el resto actualizan tu estado.

_Los cuatro primeros pasos los da tu aplicación una vez; los dos últimos se repiten cada periodo sin que intervengas._

## Paso 1: activar la facturación de la aplicación

1. **Encender el interruptor**

   En la ficha de tu aplicación, en **Dinero › 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.

   Mientras esté apagada, cada factura que la aplicación intente emitir responde `403 BILLING_NOT_ENABLED`. Si la aplicación tiene permisos de facturas y tu plan incluye Finanzas, el diagnóstico lo marca como paso pendiente: el bloque **Estado** del **Resumen** lo dice en una línea y la pestaña **Pruebas** ofrece **Activar la facturación**. La validación en seco también lo avisa.
2. **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: `sendEmail` en la llamada manda sobre ella en los dos sentidos. Con `true` la factura sale aunque el interruptor esté apagado; con `false` no sale aunque esté encendido; sin `sendEmail`, decide el interruptor.
3. **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.
4. **Conceder los permisos de la aplicación**

   En la ficha de la aplicación, en **Configuración › Ajustes**, pestaña **Permisos**, 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.
5. **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 |
| `crm:invoices:rectify` | Emitir la rectificativa de una factura cuando devuelves el dinero con tu propia pasarela | 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, y reintentar ya el cobro de una cuota de suscripción | 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.

Siete permisos son críticos financieros porque mueven dinero, cortan un ingreso o restan ingresos con un documento fiscal: `billing:charge`, `billing:refund`, `billing:subscriptions:cancel`, `billing:payment-authorizations:write`, `billing:scheduled-charges:write`, `crm:invoices:cancel` y `crm:invoices:rectify`. Los concede un Super Administrador con un motivo que queda en el registro de auditoría. `billing:scheduled-charges:write` entró en la lista el 21 de octubre de 2026, porque adelantar un reintento cobra en el momento: las aplicaciones que ya lo tenían lo conservan.

No hay periodo de gracia: un permiso que la aplicación no tenga produce `403` con `errorCode: 'INSUFFICIENT_SCOPE'` desde la primera llamada, con el que falta en `requiredScopes`. Concédelos antes de integrar. Las reglas completas están en [Permisos de la API](https://os.utilia.ai/docs/developers/api/scopes).

## Paso 3: instalar el SDK

### JavaScript / TypeScript

```bash
npm install @utilia-os/sdk-js
```

```typescript
import { UtiliaSDK } from '@utilia-os/sdk-js';

const sdk = new UtiliaSDK({
  baseURL: 'https://os.utilia.ai/api',
  apiKey: process.env.UTILIA_API_KEY!,
});
```

### Python

```bash
pip install utilia-sdk
```

```python
import os
from utilia_sdk import UtiliaSDK

sdk = UtiliaSDK(
    base_url="https://os.utilia.ai/api",
    api_key=os.environ["UTILIA_API_KEY"],
)
```

> **Importante: 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

```typescript
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 Nubeta, 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 Nubeta 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 y para el CRM 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 Nubeta con una ficha nativa sigue siendo posible, pero lo decide tu equipo desde el panel. Más detalle en [Usuarios externos](https://os.utilia.ai/docs/developers/api/users).

## 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.

```typescript
const setup = await sdk.paymentMethods.createSetupIntent({
  externalId: 'user_01HXYZ',
  email: 'ana@estudiomarea.example',
  name: 'Estudio Marea SL',
});

// setup.clientSecret, setup.publishableKey, setup.stripeAccountId
```

Tu 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](https://os.utilia.ai/docs/developers/api/payment-methods).

## 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.

1. **Pedir el texto vigente**

   ```typescript
   const consent = await sdk.paymentAuthorizations.getConsentText({ scope: 'RECURRING' });
   // consent.text, consent.version, consent.companyName
   ```

   Muéstraselo tal cual. Lleva el nombre fiscal de la organización: no lo reescribas ni lo reutilices entre organizaciones.
2. **Registrar la aceptación**

   ```typescript
   const 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_MISMATCH` **antes** 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

Antes de emitir la primera de verdad, manda el mismo cuerpo a
`POST /external/v1/invoices/validate`: te dice si se emitiría, qué lo
impediría y cómo quedaría, sin numerar nada (ver
[Validar una factura sin emitirla](https://os.utilia.ai/docs/developers/api/invoice-testing#validar-una-factura-sin-emitirla)).
Después, la emisión real:

```typescript
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: 'nubeta_pedido_4821',
});

console.log(invoice.invoiceNumber, invoice.emailSent);
```

La factura se numera con el contador correlativo de tu organización 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. Guarda `invoiceNumber` (`202600042`, el número registrado) junto con el `id` para buscarla y conciliarla; para enseñarla, usa `displayNumber` (`2026-00042`), el número tal como va impreso, con guion entre el año y el número de orden.

La llamada puede tardar varios segundos: dale a tu cliente HTTP un tiempo de espera de al menos 30 y, si se corta, repítela con la misma `idempotencyKey`. Si la primera sigue en marcha, recibes la misma factura con `issuanceDeliveryStatus: processing`, nunca una segunda.

> **Atención: emailSent en falso no es una factura fallida**
>
> La factura está emitida y numerada; solo falta 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. Mira `emailDelivery.status`: 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 `sdk.invoices.send(...)`. **No la vuelvas a crear nunca**: sería una segunda factura. Los motivos están en [El correo de la factura](https://os.utilia.ai/docs/developers/api/invoices#el-correo-de-la-factura).

Para reenviar, o para enviar en un momento distinto del de la emisión:

```typescript
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:

```typescript
const link = await sdk.invoices.regeneratePublicLink(invoice.id, {
  userId: 'user_01HXYZ',
});
// link.publicUrl
```

> **Atención: La 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:

```typescript
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

```typescript
const subscription = await sdk.subscriptions.create({
  userId: 'user_01HXYZ',
  name: 'Nubeta 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:

```typescript
const proximo = await sdk.subscriptions.getUpcomingCharge(subscription.id, {
  userId: 'user_01HXYZ',
});
// proximo.nextInvoiceDate, proximo.amount, proximo.autoChargeReady
```

Pedir `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](https://os.utilia.ai/docs/developers/api/subscriptions).

## Paso 9: cobrar sin nadie delante

Para cobrar ahora mismo una factura suelta contra la autorización del usuario:

```typescript
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](https://os.utilia.ai/docs/developers/api/scheduled-charges), 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 Nubeta |
| - | - |
| `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

```typescript
// 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:

### JavaScript / TypeScript

```typescript
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');
});
```

### Python

```python
import os
from flask import Flask, request
from utilia_sdk import UtiliaSDK

sdk = UtiliaSDK(base_url="https://os.utilia.ai/api", api_key=os.environ["UTILIA_API_KEY"])
app = Flask(__name__)


@app.post("/webhooks/utilia")
def recibir_aviso():
    try:
        evento = sdk.webhooks.parse(
            request.get_data(as_text=True),
            request.headers,
            os.environ["UTILIA_WEBHOOK_SECRET"],
        )
    except Exception:
        return "Firma no válida", 401

    entrega = request.headers["X-Utilia-Delivery-Id"]
    if ya_procesado(entrega):
        return "OK", 200

    nombre = evento["event"]
    if nombre == "SUBSCRIPTION_INVOICE_GENERATED":
        registrar_factura(evento["data"])
    elif nombre == "CHARGE_SUCCEEDED":
        renovar_servicio(evento["data"])
    elif nombre == "CHARGE_REQUIRES_ACTION":
        pedir_autenticacion(evento["data"])
    elif nombre == "CHARGE_RETRIES_EXHAUSTED":
        suspender_servicio(evento["data"])

    marcar_procesado(entrega)
    return "OK", 200
```

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](https://os.utilia.ai/docs/developers/webhooks/hmac-signature), y el catálogo completo de avisos en [Contenido de los avisos](https://os.utilia.ai/docs/developers/webhooks/events).

## Cobras con tu propia pasarela

Si tu aplicación cobra con su propia cuenta de Stripe (o con otra
pasarela) y usa UTILIA OS solo para facturar, sigue la guía
[Facturar cuando cobras con tu propia pasarela](https://os.utilia.ai/docs/developers/api/own-gateway-billing):
factura simplificada ya cobrada, devolución en tu pasarela y
rectificativa. Solo necesitas **Finanzas**.

## 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. `requiredScopes` dice cuál falta. | Pide que lo concedan en los permisos de la API de la aplicación. No reintentes. |
| `403` con `errorCode: 'BILLING_NOT_ENABLED'` | La facturación de la aplicación está apagada. | Un Super Administrador la enciende con **Activar la facturación** en la pantalla Facturación de la aplicación. Antes de la primera factura real, la validación en seco lo detecta. |
| `409` con `retryable: true` | La operación tiene que esperar a que termine otra (por ejemplo, una rectificativa mientras la factura se sigue emitiendo). | Repite la misma petición, con la misma clave, pasados `retryAfterSeconds`. |
| `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`. Los SDK la respetan solos a partir de la 4.6.0; las versiones anteriores no la leen. |

## 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 `idempotencyKey` en toda creación de factura, suscripción, cobro y reembolso, y das a `POST /external/v1/invoices` un tiempo de espera de al menos 30 segundos.
- [ ] Has probado el cuerpo con `POST /external/v1/invoices/validate` y tu receptor con un aviso de ejemplo de la pestaña **Pruebas**.
- [ ] Tu receptor de avisos comprueba la firma, valida la ventana de tiempo y descarta repetidos por `X-Utilia-Delivery-Id`.
- [ ] Lees `emailDelivery` para saber si el correo salió: con `retrying` esperas, con `failed` o `withheld` envías con `send`, y nunca rehaces la factura.
- [ ] Guardas el enlace público la primera vez que lo emites, en lugar de volver a emitirlo.
- [ ] Gestionas `CHARGE_REQUIRES_ACTION` y `CHARGE_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?

Siempre con la plantilla y el remitente de tu organización:

- **La factura al emitirse** sale si la llamada lo pide con `sendEmail: true`, aunque el interruptor **Enviar las facturas por correo al emitirlas** esté apagado. Con `sendEmail: false` no sale aunque esté encendido, y sin `sendEmail` decide el interruptor.
- **El justificante al cobrarse y los avisos de vencimiento y recordatorio** solo salen con el interruptor encendido, y si tu organización los tiene activados; `sendEmail` no los cambia.

Con el interruptor apagado y sin `sendEmail: true`, UTILIA OS **no escribe a tus usuarios**: el envío es cosa tuya.

### ¿Puedo personalizar la numeración de mis facturas?

Sí: tu organización puede crear una serie para tu aplicación (por ejemplo, `APP-202600001`), con su propia numeración correlativa, en [Ajustes › Organización](https://os.utilia.ai/dashboard/settings/organization), pestaña **Facturas**. Tu aplicación la elige con `seriesCode` al crear la factura, o queda como serie por defecto de la aplicación y se usa sola. Las consultas con [`GET /external/v1/invoices/series`](https://os.utilia.ai/docs/developers/api/invoices#series-disponibles). La clave de API no crea ni cambia series, ni elige libremente el prefijo, y la numeración sigue siendo correlativa dentro de cada serie, como exige la normativa.

Sin serie propia, todo sigue como siempre: las ordinarias en la serie general, compartida con las que emite tu equipo desde el panel, y las simplificadas en la suya (`T-`). Las rectificativas van siempre a la suya (`R-`), también las de una factura numerada en la serie de tu aplicación.

### ¿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?

Sí, cuando devuelves el dinero o un contracargo deja la venta sin efecto. Si cobraste con tu propia pasarela, usa [`POST /external/v1/invoices/:id/rectifications`](https://os.utilia.ai/docs/developers/api/invoice-rectifications); si cobraste con la de UTILIA OS, el reembolso con `createRectifyingInvoice: true`. Tú das la causa y, si es parcial, el importe; el código legal (R1 o R5) lo decide UTILIA OS, siempre por diferencias. Las rectificativas por otros motivos las emite tu equipo desde Finanzas.

### ¿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 Ajustes › Creadas por tu empresa › tu aplicación › **Dinero** › **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](https://os.utilia.ai/docs/developers/webhooks/delivery-and-retries).

## Siguiente paso

- [Acciones sobre una factura](https://os.utilia.ai/docs/developers/api/invoice-actions): Enviar, cobrar, marcar pagada, devolver y enlace público.
- [Autorizaciones de cobro](https://os.utilia.ai/docs/developers/api/payment-authorizations): El consentimiento que habilita cobrar sin el usuario delante.
- [Suscripciones](https://os.utilia.ai/docs/developers/api/subscriptions): Cuotas que se generan, envían y cobran solas.
- [Contenido de los avisos](https://os.utilia.ai/docs/developers/webhooks/events): Los avisos de facturación, suscripciones y cobros.
