Ir al contenido

Preguntas frecuentes

Esta página responde preguntas frecuentes basadas en el comportamiento real del código de 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",
)
  • 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 calculadoFórmula
fechaEmisionFecha actual del sistema
valorVenta (línea)cantidad × precio
igv (línea)valorVenta × igvTasa (solo si tipoAfectacionIGV == "10")
precioVenta (línea)valorVenta + igv
valorVentaTotalSuma de valorVenta de todas las líneas
igvTotalSuma de igv de todas las líneas
importeTotalvalorVentaTotal + 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 httpx
from 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"])

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:

ValorComportamiento
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.
falseGenera 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 httpx
from 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"])

¿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 Path
from 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-----

Internamente usa cryptography.hazmat.primitives.serialization.pkcs12 para:

  1. Descifrar el contenedor PKCS#12 con la contraseña proporcionada.
  2. Exportar la clave privada en formato PEM tradicional (TraditionalOpenSSL).
  3. 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)

¿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:

AspectoResumen Diario (SummaryDocuments)Comunicación de Baja (VoidedDocuments)
TipoRCRA
IdentificadorRC-YYYYMMDD-NNNNRA-YYYYMMDD-NNNN
¿Para qué sirve?Declarar boletas y notas asociadas emitidas en un díaAnular facturas o boletas antes de que sean declaradas
Documentos afectadosBoletas (03) y notas de crédito/débito asociadasFacturas (01) o boletas (03) no comunicadas
FrecuenciaDiariaEventual, 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))

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)

El código realiza lo siguiente:

  1. Consulta GET /api/v1/version.
  2. Obtiene la versión reportada por la API.
  3. La compara con la versión del SDK.
  4. Devuelve un objeto con:
CampoDescripción
okTrue si las versiones coinciden, False en caso contrario
sdk_version / sdkVersionVersión instalada del SDK
api_version / apiVersionVersió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.

La tasa de IGV por defecto es 0.18 (18 %), definida en src/openubl/models/defaults.py:

from pydantic import BaseModel
from decimal import Decimal
class Defaults(BaseModel):
igvTasa: Decimal = Decimal("0.18")
icbTasa: Decimal = Decimal("0.2")

Para usar una tasa diferente, crea una instancia de Defaults y pásala al ContentEnricher:

from decimal import Decimal
from openubl.enricher import ContentEnricher
from 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.00
print(invoice.importeTotal) # 110.00

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érico
const { 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.

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.