Preguntas frecuentes
Esta página responde preguntas frecuentes basadas en el comportamiento real del código de openUBL.
¿Qué algoritmo de firma usa openUBL?
Sección titulada «¿Qué algoritmo de firma usa openUBL?»openUBL firma con RSA-SHA-256 y digestión SHA-256 (XMLDSig), siguiendo la estructura exigida por SUNAT (UBLExtension/ExtensionContent) y los requisitos de seguridad de la infraestructura de certificación peruana.
En src/openubl/signer.py se configura explícitamente:
signer = XMLSigner( c14n_algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315", digest_algorithm="sha256", signature_algorithm="rsa-sha256",)Base normativa
Sección titulada «Base normativa»- RS N.° 300-2014/SUNAT y modificatorias: definen la estructura XMLDSig y la ubicación de la firma dentro de
UBLExtension/ExtensionContent. - Resolución de Secretaría N.° 007-2024-PCM/SGTD: aprueba la Directiva N.° 002-2024-PCM/SGTD que regula el uso de la firma digital en entidades públicas.
- INDECOPI/IOFE — Guía de Acreditación de Entidades de Certificación: exige algoritmos SHA-2 (RSA-SHA-256/384/512 o ECDSA) para certificados digitales.
¿Puedo omitir campos de totales si uso la API REST?
Sección titulada «¿Puedo omitir campos de totales si uso la API REST?»Sí, siempre que el documento sea enriquecible. El ContentEnricher (src/openubl/enricher.py) calcula automáticamente los siguientes campos cuando no se proporcionan:
| Campo calculado | Fórmula |
|---|---|
fechaEmision | Fecha actual del sistema |
valorVenta (línea) | cantidad × precio |
igv (línea) | valorVenta × igvTasa (solo si tipoAfectacionIGV == "10") |
precioVenta (línea) | valorVenta + igv |
valorVentaTotal | Suma de valorVenta de todas las líneas |
igvTotal | Suma de igv de todas las líneas |
importeTotal | valorVentaTotal + igvTotal |
El enriquecimiento aplica a Invoice, CreditNote y DebitNote. No aplica a SummaryDocuments, Perception ni Retention, porque esos documentos requieren totales explícitos por ítem o comprobante.
Ejemplo de petición mínima válida:
import httpxfrom openubl.models import Invoice, Proveedor, Cliente, DocumentoVentaDetalle
invoice = Invoice( serie="F001", numero=1, proveedor=Proveedor( ruc="20100066603", razonSocial="Softgreen S.A.C.", ), cliente=Cliente( nombre="Carlos Feria", numeroDocumentoIdentidad="12121212121", tipoDocumentoIdentidad="6", ), detalles=[ DocumentoVentaDetalle( descripcion="Item", cantidad=10, precio=100, ) ],)
response = httpx.post( "http://localhost:8000/api/v1/invoice/create", json=invoice.model_dump(mode="json"),)response.raise_for_status()print(response.json()["xml"])import { createInvoice } from "@openubl/sdk";import { zInvoice } from "@openubl/sdk/zod.gen";
const invoice = zInvoice.parse({serie: "F001",numero: 1,proveedor: { ruc: "20100066603", razonSocial: "Softgreen S.A.C.",},cliente: { nombre: "Carlos Feria", numeroDocumentoIdentidad: "12121212121", tipoDocumentoIdentidad: "6",},detalles: [ { descripcion: "Item", cantidad: 10, precio: 100, },],});
const { data, error } = await createInvoice({body: invoice,});
if (error) {throw new Error(JSON.stringify(error));}
console.log(data.xml);curl -X POST "http://localhost:8000/api/v1/invoice/create" \-H "Content-Type: application/json" \-d '{ "serie": "F001", "numero": 1, "proveedor": { "ruc": "20100066603", "razonSocial": "Softgreen S.A.C." }, "cliente": { "nombre": "Carlos Feria", "numeroDocumentoIdentidad": "12121212121", "tipoDocumentoIdentidad": "6" }, "detalles": [ { "descripcion": "Item", "cantidad": 10, "precio": 100 } ]}'La API devolverá el XML con los totales ya calculados.
¿Qué diferencia hay entre ?validate=true y ?validate=false?
Sección titulada «¿Qué diferencia hay entre ?validate=true y ?validate=false?»Todos los endpoints de creación de documentos aceptan el parámetro de consulta validate:
| Valor | Comportamiento |
|---|---|
true (por defecto) | Genera el XML y lo valida contra los esquemas XSD y reglas de negocio de SUNAT antes de responder. Si falla, devuelve 422 Unprocessable Entity con el listado de errores. |
false | Genera el XML sin ejecutar validaciones. Es útil para depuración o pruebas internas, pero el resultado puede ser rechazado por SUNAT. |
Ejemplo con validación desactivada:
import httpxfrom openubl.models import Invoice, Proveedor, Cliente, DocumentoVentaDetalle
invoice = Invoice( serie="F001", numero=1, proveedor=Proveedor(ruc="20100066603", razonSocial="Softgreen S.A.C."), cliente=Cliente( nombre="Carlos Feria", numeroDocumentoIdentidad="12121212121", tipoDocumentoIdentidad="6", ), detalles=[DocumentoVentaDetalle(descripcion="Item", cantidad=10, precio=100)],)
response = httpx.post( "http://localhost:8000/api/v1/invoice/create?validate=false", json=invoice.model_dump(mode="json"),)print(response.status_code)print(response.json()["xml"])import { createInvoice } from "@openubl/sdk";import { zInvoice } from "@openubl/sdk/zod.gen";
const invoice = zInvoice.parse({serie: "F001",numero: 1,proveedor: { ruc: "20100066603", razonSocial: "Softgreen S.A.C." },cliente: { nombre: "Carlos Feria", numeroDocumentoIdentidad: "12121212121", tipoDocumentoIdentidad: "6",},detalles: [{ descripcion: "Item", cantidad: 10, precio: 100 }],});
const { data, error } = await createInvoice({query: { validate: false },body: invoice,});
if (error) {throw new Error(JSON.stringify(error));}
console.log(data.xml);curl -X POST "http://localhost:8000/api/v1/invoice/create?validate=false" \-H "Content-Type: application/json" \-d '{ "serie": "F001", "numero": 1, "proveedor": { "ruc": "20100066603", "razonSocial": "Softgreen S.A.C." }, "cliente": { "nombre": "Carlos Feria", "numeroDocumentoIdentidad": "12121212121", "tipoDocumentoIdentidad": "6" }, "detalles": [{ "descripcion": "Item", "cantidad": 10, "precio": 100 }]}'¿Cómo convierto un certificado .pfx a PEM?
Sección titulada «¿Cómo convierto un certificado .pfx a PEM?»openUBL expone la función load_pfx en src/openubl/signer.py para extraer la clave privada y el certificado X.509 de un archivo .pfx o .p12:
from pathlib import Pathfrom openubl.signer import load_pfx
pfx_bytes = Path("certificado.pfx").read_bytes()key_pem, cert_pem = load_pfx(pfx_bytes, password="mi-contraseña")
print(key_pem) # -----BEGIN RSA PRIVATE KEY-----print(cert_pem) # -----BEGIN CERTIFICATE-----// load_pfx es una utilidad interna del paquete Python openubl.// En TypeScript puedes enviar el certificado ya convertido a PEM al endpoint /api/v1/sign.# load_pfx es una utilidad del paquete Python openubl.# Convierte el .pfx a PEM con openssl u otra herramienta y envía el resultado a /api/v1/sign.Internamente usa cryptography.hazmat.primitives.serialization.pkcs12 para:
- Descifrar el contenedor PKCS#12 con la contraseña proporcionada.
- Exportar la clave privada en formato PEM tradicional (
TraditionalOpenSSL). - Exportar el certificado en formato PEM.
Una vez convertidos, puedes usarlos para firmar XML:
from openubl.signer import sign_ubl_xml
signed_xml = sign_ubl_xml(xml_output, cert_pem, key_pem)// sign_ubl_xml es una utilidad interna del paquete Python openubl.// Desde TypeScript firma el XML enviándolo al endpoint /api/v1/sign.# sign_ubl_xml es una utilidad del paquete Python openubl.# Envía el XML sin firmar junto con cert_pem y key_pem a /api/v1/sign.¿Cuándo debo usar Resumen Diario vs Comunicación de Baja?
Sección titulada «¿Cuándo debo usar Resumen Diario vs Comunicación de Baja?»Ambos documentos tienen propósitos diferentes:
| Aspecto | Resumen Diario (SummaryDocuments) | Comunicación de Baja (VoidedDocuments) |
|---|---|---|
| Tipo | RC | RA |
| Identificador | RC-YYYYMMDD-NNNN | RA-YYYYMMDD-NNNN |
| ¿Para qué sirve? | Declarar boletas y notas asociadas emitidas en un día | Anular facturas o boletas antes de que sean declaradas |
| Documentos afectados | Boletas (03) y notas de crédito/débito asociadas | Facturas (01) o boletas (03) no comunicadas |
| Frecuencia | Diaria | Eventual, dentro de los 7 días calendario siguientes a la emisión |
Reglas importantes:
- Las boletas solo adquieren validez ante SUNAT cuando se declaran en un Resumen Diario.
- No se puede dar de baja un comprobante que ya fue incluido en un Resumen Diario aceptado.
- La Comunicación de Baja aplica a comprobantes que aún no han sido comunicados a SUNAT.
Ejemplo de Resumen Diario:
from openubl.models import ( SummaryDocuments, SummaryDocumentsItem, Comprobante, ComprobanteImpuestos, ComprobanteValorVenta, Proveedor, Cliente,)from decimal import Decimal
summary = SummaryDocuments( numero=1, fechaEmisionComprobantes="2025-06-10", proveedor=Proveedor(ruc="20100100100", razonSocial="Mi Empresa S.A.C."), comprobantes=[ SummaryDocumentsItem( tipoOperacion="1", # Adicionar comprobante=Comprobante( tipoComprobante="03", serieNumero="B001-45", cliente=Cliente( nombre="Juan Pérez", numeroDocumentoIdentidad="46779327", tipoDocumentoIdentidad="1", ), impuestos=ComprobanteImpuestos(igv=Decimal("27.00")), valorVenta=ComprobanteValorVenta(importeTotal=Decimal("177.00")), ), ), ],)
print(summary.model_dump_json(indent=2))import { createSummaryDocuments } from "@openubl/sdk";import { zSummaryDocuments } from "@openubl/sdk/zod.gen";
const summary = zSummaryDocuments.parse({numero: 1,fechaEmisionComprobantes: "2025-06-10",proveedor: { ruc: "20100100100", razonSocial: "Mi Empresa S.A.C." },comprobantes: [ { tipoOperacion: "1", comprobante: { tipoComprobante: "03", serieNumero: "B001-45", cliente: { nombre: "Juan Pérez", numeroDocumentoIdentidad: "46779327", tipoDocumentoIdentidad: "1", }, impuestos: { igv: "27.00" }, valorVenta: { importeTotal: "177.00" }, }, },],});
const { data, error } = await createSummaryDocuments({body: summary,});
if (error) {throw new Error(JSON.stringify(error));}
console.log(data.xml);curl -X POST "http://localhost:8000/api/v1/summary-documents/create" \-H "Content-Type: application/json" \-d '{ "numero": 1, "fechaEmisionComprobantes": "2025-06-10", "proveedor": { "ruc": "20100100100", "razonSocial": "Mi Empresa S.A.C." }, "comprobantes": [{ "tipoOperacion": "1", "comprobante": { "tipoComprobante": "03", "serieNumero": "B001-45", "cliente": { "nombre": "Juan Pérez", "numeroDocumentoIdentidad": "46779327", "tipoDocumentoIdentidad": "1" }, "impuestos": { "igv": "27.00" }, "valorVenta": { "importeTotal": "177.00" } } }]}'¿Qué hace check_api_version?
Sección titulada «¿Qué hace check_api_version?»check_api_version es una utilidad del SDK que verifica en runtime que la versión del cliente coincida con la versión de la API REST:
from openubl.version import check_api_version
result = check_api_version("http://localhost:8000")assert result["ok"], f"Desfase: SDK {result['sdk_version']} vs API {result['api_version']}"print(result)import { checkApiVersion } from "@openubl/sdk";
const result = await checkApiVersion("http://localhost:8000");if (!result.ok) {throw new Error("Desfase: SDK " + result.sdkVersion + " vs API " + result.apiVersion);}console.log(result);curl -X GET "http://localhost:8000/api/v1/version"El código realiza lo siguiente:
- Consulta
GET /api/v1/version. - Obtiene la versión reportada por la API.
- La compara con la versión del SDK.
- Devuelve un objeto con:
| Campo | Descripción |
|---|---|
ok | True si las versiones coinciden, False en caso contrario |
sdk_version / sdkVersion | Versión instalada del SDK |
api_version / apiVersion | Versión reportada por la API |
Esto ayuda a detectar desfases entre el cliente y el servidor, especialmente en despliegues donde la API se actualiza independientemente del SDK.
¿Cómo configuro tasas de IGV diferentes?
Sección titulada «¿Cómo configuro tasas de IGV diferentes?»La tasa de IGV por defecto es 0.18 (18 %), definida en src/openubl/models/defaults.py:
from pydantic import BaseModelfrom decimal import Decimal
class Defaults(BaseModel): igvTasa: Decimal = Decimal("0.18") icbTasa: Decimal = Decimal("0.2")// Defaults y ContentEnricher son utilidades del paquete Python openubl.// Desde TypeScript envía el campo igv calculado manualmente en cada línea.# Defaults y ContentEnricher son utilidades del paquete Python openubl.# Incluye el campo igv calculado manualmente en cada línea del JSON enviado.Para usar una tasa diferente, crea una instancia de Defaults y pásala al ContentEnricher:
from decimal import Decimalfrom openubl.enricher import ContentEnricherfrom openubl.models import Defaults, Invoice, Proveedor, Cliente, DocumentoVentaDetalle
invoice = Invoice( serie="F001", numero=1, proveedor=Proveedor(ruc="20100100100", razonSocial="Mi Empresa S.A.C."), cliente=Cliente( nombre="Cliente Ejemplo", numeroDocumentoIdentidad="46779327", tipoDocumentoIdentidad="1", ), detalles=[ DocumentoVentaDetalle(descripcion="Item", cantidad=1, precio=100) ],)
enricher = ContentEnricher( defaults=Defaults(igvTasa=Decimal("0.10")))enricher.enrich(invoice)
print(invoice.igvTotal) # 10.00print(invoice.importeTotal) # 110.00import { createInvoice } from "@openubl/sdk";import { zInvoice } from "@openubl/sdk/zod.gen";
const invoice = zInvoice.parse({serie: "F001",numero: 1,proveedor: { ruc: "20100100100", razonSocial: "Mi Empresa S.A.C." },cliente: { nombre: "Cliente Ejemplo", numeroDocumentoIdentidad: "46779327", tipoDocumentoIdentidad: "1",},detalles: [ { descripcion: "Item", cantidad: 1, precio: 100, igv: 10, // calculado manualmente con tasa 10% valorVenta: 100, precioVenta: 110, },],igvTotal: 10,valorVentaTotal: 100,importeTotal: 110,});
const { data, error } = await createInvoice({body: invoice,});
if (error) {throw new Error(JSON.stringify(error));}
console.log(data.xml);curl -X POST "http://localhost:8000/api/v1/invoice/create" \-H "Content-Type: application/json" \-d '{ "serie": "F001", "numero": 1, "proveedor": { "ruc": "20100100100", "razonSocial": "Mi Empresa S.A.C." }, "cliente": { "nombre": "Cliente Ejemplo", "numeroDocumentoIdentidad": "46779327", "tipoDocumentoIdentidad": "1" }, "detalles": [ { "descripcion": "Item", "cantidad": 1, "precio": 100, "igv": 10, "valorVenta": 100, "precioVenta": 110 } ], "igvTotal": 10, "valorVentaTotal": 100, "importeTotal": 110}'Si usas la API REST, no puedes cambiar la tasa directamente por parámetro de consulta. Debes enviar el campo igv calculado manualmente en cada línea, o bien usar la biblioteca Python con un ContentEnricher personalizado.
¿Puedo usar el SDK sin levantar la API REST?
Sección titulada «¿Puedo usar el SDK sin levantar la API REST?»No. El SDK @openubl/sdk es un cliente HTTP generado desde OpenAPI; necesita un servidor openUBL corriendo. Para generar XML sin servidor, usa la biblioteca Python nativa (openubl.models, ContentEnricher y render_invoice).
¿Cuál es la diferencia entre client.post y createInvoice?
Sección titulada «¿Cuál es la diferencia entre client.post y createInvoice?»createInvoice es el helper tipado generado desde OpenAPI. Conoce la URL, el body y los errores del endpoint, y te da autocompletado y validación automática. client.post es el cliente genérico de @hey-api/client-fetch; te permite llamar cualquier endpoint, pero sin el tipado específico del helper.
// Helper tipado (recomendado)const { data, error } = await createInvoice({ body: invoice });
// Cliente genéricoconst { data, error } = await client.post("/api/v1/invoice/create", { body: invoice });¿Por qué los schemas Zod están en @openubl/sdk/zod.gen?
Sección titulada «¿Por qué los schemas Zod están en @openubl/sdk/zod.gen?»Se generan automáticamente desde openapi.json junto con el cliente y los tipos. Están en un módulo separado (zod.gen) para que solo cargues Zod cuando necesites validación runtime. Si solo usas los helpers, no necesitas importar los schemas.
¿Puedo usar el SDK en el navegador?
Sección titulada «¿Puedo usar el SDK en el navegador?»Teóricamente sí, porque @hey-api/client-fetch usa fetch. Sin embargo, nunca expongas certificados digitales ni claves privadas en el frontend. La firma XML debe hacerse en un backend de confianza (Python o API REST) o, como máximo, enviar los PEM desde el servidor al endpoint /api/v1/sign.