Probar la facturación sin emitir facturas
Valida una factura sin emitirla, descarga un PDF de prueba sin validez fiscal y ensaya una rectificativa antes de emitir tu primera factura real.
Actualizado el . 10 min de lectura
Incluido en todos los planes. Ver precios
Cada factura que tu aplicación emite con POST /external/v1/invoices es un documento fiscal de verdad: lleva un número de la serie de tu organización y no se puede borrar. Para probarla hay tres rutas que calculan sin emitir, con el mismo cuerpo que usarás después. El diagnóstico general de tu aplicación (permisos, plan, avisos y llamadas rechazadas) está en Probar tu integración.
Las pruebas no numeran
La numeración de las facturas es correlativa dentro de cada serie (art. 6 del Real Decreto 1619/2012, que regula las obligaciones de facturación). Una factura emitida no se borra ni se renumera: si estaba mal, se corrige con una rectificativa, que es otro documento con su propio número. Si además tu organización usa VeriFactu, cada factura queda encadenada en su registro.
Por eso no hay una «serie de pruebas» ni un modo que emita facturas falsas: una factura emitida para probar sería una factura de verdad. Las pruebas pasan por tres rutas que calculan sin emitir:
| Para probar | Ruta | Qué devuelve |
|---|---|---|
| Una factura | POST /external/v1/invoices/validate | El veredicto y una vista previa de la factura, sin número. |
| Su PDF | POST /external/v1/invoices/validate/pdf | Un PDF de muestra marcado como documento de prueba. |
| Una rectificativa | POST /external/v1/invoices/:id/rectifications/validate | El veredicto y una vista previa de la rectificativa. |
Ninguna reserva número, escribe en la base de datos, envía correos ni dispara avisos. Puedes llamarlas tantas veces como quieras.
Los ejemplos que emiten de verdad
El guion 01-create-invoice.ts de examples/04-node-billing-integration y external-issue-invoice.ts de examples/sdk-js llaman a POST /external/v1/invoices: emiten una factura real y numerada. Para probar, cambia sdk.invoices.create por sdk.invoices.validate.
Validar una factura sin emitirla
/api/external/v1/invoices/validateComprueba una factura como si fuera a emitirse y devuelve su vista previa, sin numerarla ni guardarla. Mismo cuerpo que POST /external/v1/invoices. Basta con cualquier permiso crm:invoices:*.
El cuerpo es exactamente el mismo que el de crear una factura: manda lo que mandarías para emitirla y verás lo que pasaría.
- Permisos: basta con cualquier permiso
crm:invoices:(por ejemplo, solocrm:invoices:read). Sin ninguno,403 INSUFFICIENT_SCOPE. Así puedes probar antes de pedir los permisos de emisión. - No exige la facturación encendida. Mientras esté apagada, la respuesta lo dice como bloqueo (
BILLING_NOT_ENABLED). - No emite nada: no reserva número, no escribe, no envía el correo, no dispara avisos y no consume la clave de idempotencia.
idempotencyKeyse acepta y se ignora, así que puedes probar con el cuerpo definitivo. - Responde siempre
200con el resultado, también cuando la factura no se podría emitir: el motivo viaja enblockers. Solo un cuerpo mal formado (un campo obligatorio que falta, un tipo que no es el suyo) responde400 VALIDATION_ERROR, igual que en el alta.
Antes de validar el contenido, la comprobación de peticiones te dice si al cuerpo le sobra algún campo mal escrito que la emisión descartaría sin error.
Ejemplos
curl -X POST https://os.utilia.ai/api/external/v1/invoices/validate \
-H "X-Api-Key: $UTILIA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user": { "externalId": "user_1234", "email": "cliente@example.com", "name": "Ana Pérez" },
"type": "SIMPLIFIED",
"taxSystem": "IGIC",
"lines": [
{ "name": "Plan Premium mensual", "quantity": 1, "unitPrice": 28.04, "taxType": "IGIC_7" }
],
"payment": { "method": "CARD", "reference": "pago-1234" },
"sendEmail": true,
"idempotencyKey": "order_1234-invoice",
"metadata": { "orderId": "order_1234" }
}'sdk.invoices.validate, validatePdf y rectifications.validate están en los SDK a partir de la 4.5.0; si la versión que puedes instalar es anterior (Versiones y compatibilidad), llama a las rutas con fetch o con tu cliente HTTP. Con el SDK, el mismo objeto sirve para validate y, cuando todo cuadre, para create.
Respuesta
{
"valid": true,
"livemode": false,
"blockers": [],
"warnings": [],
"preview": {
"type": "SIMPLIFIED",
"seriesCode": "DEFAULT",
"numberFormat": "T-AAAA00000",
"displayNumberFormat": "T-AAAA-00000",
"issueDate": "2026-10-19",
"taxSystem": "IGIC",
"currency": "eur",
"subtotal": 28.04,
"totalTax": 1.96,
"totalAmount": 30,
"lines": [
{
"name": "Plan Premium mensual",
"quantity": 1,
"unitPrice": 28.04,
"discount": 0,
"subtotal": 28.04,
"taxType": "IGIC_7",
"taxRate": 7,
"taxAmount": 1.96,
"total": 30,
"exemptionReason": null,
"exempt": false
}
],
"taxes": [
{ "taxType": "IGIC_7", "taxRate": 7, "taxBase": 28.04, "taxAmount": 1.96 }
],
"legalMentions": ["Factura simplificada (RD 1619/2012 art. 7)"],
"recipient": {
"name": "Consumidor final",
"taxId": null,
"address": null,
"city": null,
"postalCode": null,
"country": null
},
"payment": { "willRegister": true, "method": "CARD", "paidAt": null },
"email": { "willSend": true, "recipient": "cliente@example.com", "reason": null }
}
}- validbooleanobligatorio
- true si POST /external/v1/invoices emitiría esta factura tal cual. false si hay al menos un bloqueo.
- livemodebooleanobligatorio
- Siempre false: es una prueba y no tiene efecto fiscal.
- blockersarrayobligatorio
- Lo que impediría emitir, cada uno con code, message y, a veces, details. El code es el mismo errorCode que respondería el alta (por ejemplo SIMPLIFIED_INVOICE_AMOUNT_EXCEEDED o INVOICE_RECIPIENT_INCOMPLETE con missingFields en details), más BILLING_NOT_ENABLED, INSUFFICIENT_SCOPE y APP_RESPONSIBLE_INACTIVE.
- warningsarrayobligatorio
- Avisos que no impiden emitir, cada uno con code y message.
- previewobjeto | nullobligatorio
- La factura que se emitiría. Con bloqueos puede llegar a null: no te fíes de ella hasta que valid sea true.
- preview.numberFormatstringobligatorio
- Forma del número que recibirá, sin el número en sí (AAAA es el año y los ceros, la secuencia). Nunca se reserva un número en una prueba.
- preview.displayNumberFormatstringobligatorio
- La forma tal como se imprime en el PDF, con un guion entre el año y la secuencia (T-AAAA-00000).
- preview.issueDatestringobligatorio
- Fecha de expedición que tendría (AAAA-MM-DD), en la zona horaria de tu organización.
- preview.subtotal / totalTax / totalAmountnumberobligatorio
- Base, cuota y total, redondeados como los redondea la emisión real.
- preview.linesarrayobligatorio
- Cada línea con su base, su tipo, su cuota, su total y, si está exenta, la causa de la exención.
- preview.taxesarrayobligatorio
- El desglose por tipo de impuesto que se imprimirá.
- preview.legalMentionsstring[]obligatorio
- Las menciones legales que llevará el PDF, en su orden.
- preview.recipientobjetoobligatorio
- El destinatario tal como se imprimirá.
- preview.paymentobjeto | nullobligatorio
- Si mandas el bloque payment, el cobro que se registraría al emitir (willRegister, method y paidAt). null sin él.
- preview.emailobjetoobligatorio
- willSend dice si saldría el correo, recipient a qué dirección y reason, si no sale, por qué: NOT_REQUESTED, SEND_SCOPE_MISSING (falta crm:invoices:send) o NO_RECIPIENT.
Una factura que no se podría emitir responde también 200, con valid: false:
{
"valid": false,
"livemode": false,
"blockers": [
{
"code": "BILLING_NOT_ENABLED",
"message": "La facturación de esta aplicación está desactivada, así que todavía no puede emitir facturas. La activa un Super Administrador de la organización en UTILIA OS, en la ficha de la aplicación, Dinero › Facturación («Activar la facturación»)."
},
{
"code": "SIMPLIFIED_INVOICE_AMOUNT_EXCEEDED",
"message": "Una factura simplificada no puede superar 400 € impuestos incluidos. Emite una ordinaria."
}
],
"warnings": [],
"preview": null
}La facturación apagada o un permiso de emisión que falte no impiden calcular la vista previa: con solo esos bloqueos, preview llega completa. Llega en null cuando el propio documento no se puede calcular, como en el ejemplo, por el importe de la simplificada. El único aviso que hoy puede traer warnings es SEND_SCOPE_GRACE: la aplicación no tiene crm:invoices:send y, por ahora, el correo saldría igual.
Cuando valid sea true con el cuerpo definitivo, ese mismo cuerpo emitirá la factura, salvo que algo cambie entre las dos llamadas (la facturación se apaga, un permiso se retira o la fecha de la última factura de la serie avanza).
PDF de prueba
/api/external/v1/invoices/validate/pdfGenera el PDF de muestra de la factura sin emitirla: número PRUEBA, sin código QR y con la marca de agua DOCUMENTO DE PRUEBA · SIN VALIDEZ FISCAL. Mismo cuerpo y mismos permisos que /invoices/validate.
Sirve para ver cómo quedará el documento con la marca, los datos fiscales y la plantilla de tu organización. Responde application/pdf con una muestra sin validez fiscal: en lugar del número lleva la palabra «PRUEBA», no lleva el código QR de VeriFactu y lleva en diagonal la marca de agua «DOCUMENTO DE PRUEBA · SIN VALIDEZ FISCAL». No lo entregues a tus usuarios ni lo guardes como factura.
Si el propio documento no se puede calcular, no hay PDF: la respuesta es 422 con el primer bloqueo que daría /invoices/validate como errorCode y la lista completa en blockers.
{
"statusCode": 422,
"message": "Una factura simplificada no puede superar 400 € impuestos incluidos. Emite una ordinaria.",
"error": "UNPROCESSABLE_ENTITY",
"errorCode": "SIMPLIFIED_INVOICE_AMOUNT_EXCEEDED",
"blockers": [
{
"code": "SIMPLIFIED_INVOICE_AMOUNT_EXCEEDED",
"message": "Una factura simplificada no puede superar 400 € impuestos incluidos. Emite una ordinaria."
}
],
"timestamp": "2026-10-19T10:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}curl -X POST https://os.utilia.ai/api/external/v1/invoices/validate/pdf \
-H "X-Api-Key: $UTILIA_API_KEY" \
-H "Content-Type: application/json" \
-d @factura.json \
--output factura-de-prueba.pdfRectificativas de prueba
/api/external/v1/invoices/:id/rectifications/validateComprueba la rectificativa de la factura :id sin emitirla ni registrar la devolución. Mismo cuerpo y mismos permisos que POST /external/v1/invoices/:id/rectifications: sin crm:invoices:rectify, 403 INSUFFICIENT_SCOPE.
Antes de devolver dinero de verdad, comprueba qué rectificativa saldría. El cuerpo es el de emitir una rectificativa y se comprueban sus mismos permisos: crm:invoices:rectify, además crm:invoices:write si mandas refund y crm:invoices:send si pides sendEmail: true. Sin crm:invoices:rectify, la respuesta es 403 INSUFFICIENT_SCOPE y no enseña nada de la factura. Si lo tienes y falta alguno de los otros dos, la respuesta sigue siendo 200, con el bloqueo INSUFFICIENT_SCOPE. :id es una factura real ya emitida por tu aplicación: la prueba no la toca.
La respuesta tiene la misma forma que /invoices/validate, con su vista previa. Si la factura no admite todavía la rectificativa (por ejemplo, porque su emisión sigue en curso), el bloqueo es RECTIFICATION_NOT_ALLOWED con su motivo y, si basta con esperar, retryable y retryAfterSeconds en details. Si la idempotencyKey ya tiene una operación, warnings trae IDEMPOTENT_REPLAY y preview llega en null.
{
"valid": true,
"livemode": false,
"blockers": [],
"warnings": [],
"preview": {
"cause": "REFUND",
"rectificationCode": "R5",
"totalAmount": -30,
"remainingAfter": 0,
"issueDate": "2026-10-19",
"refund": null,
"willSendEmail": false
}
}- preview.causeenumobligatorio
- La causa que mandaste: REFUND o CHARGEBACK.
- preview.rectificationCodestringobligatorio
- El código legal que decidiría UTILIA OS (R5 para una simplificada, R1 para una ordinaria).
- preview.totalAmountnumberobligatorio
- Total de la rectificativa, impuestos incluidos, en negativo.
- preview.remainingAfternumberobligatorio
- Lo que quedaría por rectificar de la factura original después de esta.
- preview.issueDatestringobligatorio
- Fecha de expedición que tendría (AAAA-MM-DD, en la zona horaria de tu organización).
- preview.refundobjeto | nullobligatorio
- La devolución que se registraría si mandas el bloque refund: reference, method, amount y refundedOn. null sin él.
- preview.willSendEmailbooleanobligatorio
- Si la rectificativa se enviaría por correo al emitirla.
En JavaScript, sdk.invoices.rectifications.validate(facturaId, datos); en Python, await sdk.invoices.rectifications.validate(factura_id, datos) con el mismo CreateExternalInvoiceRectificationInput que usarías para emitirla.
Hasta la primera factura real
El orden recomendado, de cero a la primera factura con número. El diagnóstico de tu aplicación marca solo los pasos que el servidor puede observar (ver Primeros pasos que se marcan solos).
Concede los permisos
crm:invoices:writeycrm:invoices:issuepara emitir,crm:invoices:sendsi envías por correo ycrm:invoices:rectifysi vas a rectificar (este último solo lo concede un Super Administrador). Ver Permisos de la API.Valida la factura
Llama a
/invoices/validatecon el cuerpo definitivo hasta quevalidseatrueywarningsno diga nada que no esperes.Descarga el PDF de prueba
Revisa con
/invoices/validate/pdflos datos fiscales, la plantilla y las menciones.Prueba los avisos
Envía un
INVOICE_CREATEDde ejemplo y comprueba que tu servidor verifica la firma, lo reconoce como prueba y responde2xx. Ver Probar los avisos a tu servidor.Activa la facturación
La enciende un Super Administrador con Activar la facturación en Dinero › Facturación de la ficha de la aplicación, con un motivo. Ver Paso 1 de la guía de facturación.
Emite la primera factura con clave de idempotencia
POST /external/v1/invoicescon unaidempotencyKeyque salga de tu pedido (order_1234-invoice). Repite la misma petición: tiene que devolver la misma factura conidempotent: true, sin emitir otra.Comprueba el correo y el cobro
Lee
emailDeliveryen la respuesta. Si cobras fuera de la pasarela y no mandastepayment, registra el cobro conPOST /external/v1/invoices/:id/mark-paidy comprueba que llegaINVOICE_PAID.Ensaya la rectificativa
Sobre esa factura real, comprueba con
/rectifications/validatequé saldría si tuvieras que devolver el dinero. No emitas una rectificativa para probar: es otro documento fiscal numerado.
Tiempos de espera y reintentos
Emitir una factura hace varias cosas en el mismo paso (numerarla, registrarla, generar su PDF y, si lo pides, registrar el cobro y enviar el correo), y a veces tarda varios segundos. Pon a tu cliente HTTP un tiempo de espera de al menos 30 segundos para POST /external/v1/invoices.
Si la petición se agota o responde 5xx, no sabes si la factura llegó a emitirse. Repítela con la misma idempotencyKey y el mismo cuerpo: si ya se emitió, recibes esa factura con idempotent: true; si la primera sigue en marcha, la misma factura con issuanceDeliveryStatus: processing (vuelve a consultar en unos segundos); y si no llegó a emitirse, se emite ahora. Con 500 INVOICE_NUMBERING_UNAVAILABLE no se llegó a crear nada; con 500 INVOICE_ISSUANCE_REVIEW_REQUIRED la factura existe y pasa a revisión de tu organización.
Nunca reintentes con una clave nueva
Para UTILIA OS sería otra factura, con otro número, y tendrías que rectificar la que sobra. Calcula la clave a partir de tu pedido, no al azar en cada intento, o guárdala junto al pedido antes de la primera llamada.
Cuándo merece la pena reintentar y cuándo no está en Errores, identificador de petición y reintentos.
El correo de la factura
La respuesta de una factura real trae dos campos pensados para tu interfaz y tu soporte:
displayNumber: el número tal como se imprime en el PDF, con un guion entre el año y el número de orden (T-202600001se imprimeT-2026-00001). Úsalo para enseñarlo.invoiceNumberes el número registrado: guarda y busca por él o porid.emailDelivery: qué ha pasado con el correo. Solo viaja si pediste el correo (sendEmail: true, o sinsendEmailcon el interruptor de la aplicación encendido).
{
"emailDelivery": {
"status": "retrying",
"reason": "MAIL_SERVER_UNAVAILABLE",
"message": "Todavía no se ha enviado por correo porque el servidor de correo no respondía. UTILIA la enviará sola en unos minutos; no hace falta que la reenvíes.",
"attempts": 1,
"lastAttemptAt": "2026-10-19T10:00:04.000Z"
}
}¿Qué hago según emailDelivery.status?
¿Qué dice status?
sent:
Enviado
El correo ha salido, al emitirla o después. No hace falta nada.retrying:
Espera
Un fallo pasajero que UTILIA OS reintenta solo durante unas dos horas. No la reenvíes ni vuelvas a crearla.failed:
Resuelve el motivo y envíala con send
Fallo definitivo, reintentos agotados (RETRIES_EXHAUSTED), sin dirección (NO_RECIPIENT) o emisión cortada (INTERRUPTED).withheld:
Concede el permiso o registra el cobro, y envíala con send
Retenido a propósito: falta crm:invoices:send (SEND_SCOPE_MISSING) o el cobro de payment no se registró (PAYMENT_NOT_REGISTERED).
El envío manual es POST /external/v1/invoices/:id/send. El estado del correo nunca afecta a la factura: ya está emitida y numerada. La lista completa de motivos está en El correo de la factura.