Comprobante de Retención
El Comprobante de Retención Electrónico (tipo 20) registra la retención de IGV aplicada a compras o pagos a proveedores. Es un documento adicional a la factura de compra.
Campos requeridos
Sección titulada «Campos requeridos»| Campo | Tipo | Descripción | Validación |
|---|---|---|---|
serie | string | Serie del comprobante | Patrón ^R\d{3}$. Ej: R001, R002. |
numero | integer | Correlativo | Mayor a 0. |
fechaEmision | date | Fecha de emisión | YYYY-MM-DD. |
proveedor | Proveedor | Datos del agente de retención (emisor) | ruc de 11 dígitos y razonSocial obligatorios. |
cliente | Cliente | Datos del retenido (proveedor) | Campos obligatorios del cliente. |
importeTotalRetenido | Decimal | Total retenido | Debe ser positivo. |
importeTotalPagado | Decimal | Total pagado (monto neto) | Debe ser positivo. |
tipoRegimen | string | Régimen de retención | Catálogo N.° 23: 01, 02, 03. |
tipoRegimenPorcentaje | Decimal | Porcentaje del régimen | Ej: 0.03 para 3%. |
operaciones | PercepcionRetencionOperacion[] | Operaciones retenidas | Al menos una operación. |
Campos opcionales
Sección titulada «Campos opcionales»El modelo Retention no tiene campos opcionales a nivel raíz.
Operación de retención (PercepcionRetencionOperacion)
Sección titulada «Operación de retención (PercepcionRetencionOperacion)»| Campo | Tipo | Descripción | Validación |
|---|---|---|---|
numeroOperacion | int | Número de operación | Mayor a 0. |
fechaOperacion | date | Fecha de la operación | YYYY-MM-DD. |
importeOperacion | Decimal | Importe retenido en esta operación | Debe ser positivo. |
comprobante | ComprobanteAfectado | Comprobante afectado | Ver detalle abajo. |
ComprobanteAfectado
Sección titulada «ComprobanteAfectado»| Campo | Tipo | Descripción |
|---|---|---|
tipoComprobante | string | Tipo del comprobante afectado (Catálogo N.° 01). |
serieNumero | string | Serie-número del comprobante. |
fechaEmision | date | Fecha de emisión. |
importeTotal | Decimal | Importe total del comprobante. |
moneda | string | Moneda del comprobante. |
Reglas SUNAT aplicables
Sección titulada «Reglas SUNAT aplicables»| Código / Regla | Descripción |
|---|---|
2074 (Perception/Retention) | UBLVersionID debe ser 2.0. |
2072 (Perception/Retention) | CustomizationID debe ser 1.0. |
1001 | El ID debe tener formato serie-número. |
1007 | schemeID del agente de retención debe ser 6 (RUC). |
1008 | RUC del agente de retención debe tener 11 dígitos. |
1037 | RegistrationName del agente es obligatorio. |
| Serie | Debe iniciar con R. |
| Total | importeTotalPagado = importeTotalComprobante - importeTotalRetenido (debe cuadrar por operación). |
Regímenes de retención (Catálogo N.° 23)
Sección titulada «Regímenes de retención (Catálogo N.° 23)»| Código | Descripción | Tasa típica |
|---|---|---|
01 | Tasa 3% | 3% |
02 | Tasa 6% | 6% |
03 | Tasa mixta | Variable |
Ejemplo completo
Sección titulada «Ejemplo completo»from openubl.models import ( Retention, PercepcionRetencionOperacion, ComprobanteAfectado, Proveedor, Cliente,)from openubl.renderer import render_retentionfrom openubl.validator import SunatValidatorfrom decimal import Decimalfrom datetime import date
retention = Retention( serie="R001", numero=1, fechaEmision=date(2025, 6, 10), proveedor=Proveedor( ruc="20100100100", razonSocial="Mi Empresa S.A.C.", ), cliente=Cliente( nombre="Comercial del Sur S.A.", numeroDocumentoIdentidad="20100200200", tipoDocumentoIdentidad="6", # RUC ), importeTotalRetenido=Decimal("18.00"), importeTotalPagado=Decimal("282.00"), tipoRegimen="01", # Tasa 3% tipoRegimenPorcentaje=Decimal("0.03"), operaciones=[ PercepcionRetencionOperacion( numeroOperacion=1, fechaOperacion=date(2025, 6, 10), importeOperacion=Decimal("18.00"), comprobante=ComprobanteAfectado( tipoComprobante="01", serieNumero="F005-20", fechaEmision=date(2025, 6, 10), importeTotal=Decimal("300.00"), moneda="PEN", ), ), ],)
xml = render_retention(retention)errors = SunatValidator().validate_schema( xml, "sunat_schemas/xsd_2.1/2.0/maindoc/UBLPE-Retention-1.0.xsd",)assert errors == []print(xml)import { createRetention } from "@openubl/sdk";import { zRetention } from "@openubl/sdk/zod.gen";
const retention = zRetention.parse({ serie: "R001", numero: 1, fechaEmision: "2025-06-10", proveedor: { ruc: "20100100100", razonSocial: "Mi Empresa S.A.C." }, cliente: { nombre: "Comercial del Sur S.A.", numeroDocumentoIdentidad: "20100200200", tipoDocumentoIdentidad: "6" }, importeTotalRetenido: "18.00", importeTotalPagado: "282.00", tipoRegimen: "01", tipoRegimenPorcentaje: "0.03", operaciones: [{ numeroOperacion: 1, fechaOperacion: "2025-06-10", importeOperacion: "18.00", comprobante: { tipoComprobante: "01", serieNumero: "F005-20", fechaEmision: "2025-06-10", importeTotal: "300.00", moneda: "PEN" } }]});
const { data, error } = await createRetention({query: { validate: true }, body: retention,});
if (error) { throw new Error(JSON.stringify(error));}
console.log(data.xml);curl -X POST "http://localhost:8000/api/v1/retention/create?validate=true" \ -H "Content-Type: application/json" \ -d '{ "serie": "R001", "numero": 1, "fechaEmision": "2025-06-10", "proveedor": { "ruc": "20100100100", "razonSocial": "Mi Empresa S.A.C." }, "cliente": { "nombre": "Comercial del Sur S.A.", "numeroDocumentoIdentidad": "20100200200", "tipoDocumentoIdentidad": "6" }, "importeTotalRetenido": "18.00", "importeTotalPagado": "282.00", "tipoRegimen": "01", "tipoRegimenPorcentaje": "0.03", "operaciones": [{ "numeroOperacion": 1, "fechaOperacion": "2025-06-10", "importeOperacion": "18.00", "comprobante": { "tipoComprobante": "01", "serieNumero": "F005-20", "fechaEmision": "2025-06-10", "importeTotal": "300.00", "moneda": "PEN" } }] }'Nota sobre enriquecimiento
Sección titulada «Nota sobre enriquecimiento»ContentEnricher no enriquece Retention. Los totales y los importes por operación deben proporcionarse explícitamente.
Endpoint
Sección titulada «Endpoint»from openubl.models import ( Retention, PercepcionRetencionOperacion, ComprobanteAfectado, Proveedor, Cliente,)from openubl.renderer import render_retentionfrom decimal import Decimalfrom datetime import date
retention = Retention( serie="R001", numero=1, fechaEmision=date(2025, 6, 10), proveedor=Proveedor( ruc="20100100100", razonSocial="Mi Empresa S.A.C.", ), cliente=Cliente( nombre="Comercial del Sur S.A.", numeroDocumentoIdentidad="20100200200", tipoDocumentoIdentidad="6", ), importeTotalRetenido=Decimal("18.00"), importeTotalPagado=Decimal("282.00"), tipoRegimen="01", tipoRegimenPorcentaje=Decimal("0.03"), operaciones=[ PercepcionRetencionOperacion( numeroOperacion=1, fechaOperacion=date(2025, 6, 10), importeOperacion=Decimal("18.00"), comprobante=ComprobanteAfectado( tipoComprobante="01", serieNumero="F005-20", fechaEmision=date(2025, 6, 10), importeTotal=Decimal("300.00"), moneda="PEN", ), ), ],)
xml = render_retention(retention)print(xml)import { createRetention } from "@openubl/sdk";import { zRetention } from "@openubl/sdk/zod.gen";
const retention = zRetention.parse({ serie: "R001", numero: 1, fechaEmision: "2025-06-10", proveedor: { ruc: "20100100100", razonSocial: "Mi Empresa S.A.C." }, cliente: { nombre: "Comercial del Sur S.A.", numeroDocumentoIdentidad: "20100200200", tipoDocumentoIdentidad: "6" }, importeTotalRetenido: "18.00", importeTotalPagado: "282.00", tipoRegimen: "01", tipoRegimenPorcentaje: "0.03", operaciones: [{ numeroOperacion: 1, fechaOperacion: "2025-06-10", importeOperacion: "18.00", comprobante: { tipoComprobante: "01", serieNumero: "F005-20", fechaEmision: "2025-06-10", importeTotal: "300.00", moneda: "PEN" } }]});
const { data, error } = await createRetention({ body: retention,});
if (error) { throw new Error(JSON.stringify(error));}
console.log(data.xml);curl -X POST "http://localhost:8000/api/v1/retention/create?validate=true" \-H "Content-Type: application/json" \-d '{ "serie": "R001", "numero": 1, "fechaEmision": "2025-06-10", "proveedor": { "ruc": "20100100100", "razonSocial": "Mi Empresa S.A.C." }, "cliente": { "nombre": "Comercial del Sur S.A.", "numeroDocumentoIdentidad": "20100200200", "tipoDocumentoIdentidad": "6" }, "importeTotalRetenido": "18.00", "importeTotalPagado": "282.00", "tipoRegimen": "01", "tipoRegimenPorcentaje": "0.03", "operaciones": [{ "numeroOperacion": 1, "fechaOperacion": "2025-06-10", "importeOperacion": "18.00", "comprobante": { "tipoComprobante": "01", "serieNumero": "F005-20", "fechaEmision": "2025-06-10", "importeTotal": "300.00", "moneda": "PEN" } }]}'Casos especiales
Sección titulada «Casos especiales»Múltiples operaciones de retención
Sección titulada «Múltiples operaciones de retención»Un comprobante de retención puede incluir varias operaciones sobre facturas de compra distintas.
from openubl.models import ( Retention, PercepcionRetencionOperacion, ComprobanteAfectado, Proveedor, Cliente,)from openubl.renderer import render_retentionfrom openubl.validator import SunatValidatorfrom decimal import Decimalfrom datetime import date
retention = Retention( serie="R001", numero=2, fechaEmision=date(2025, 6, 10), proveedor=Proveedor(ruc="20100100100", razonSocial="Mi Empresa S.A.C."), cliente=Cliente( nombre="Comercial del Sur S.A.", numeroDocumentoIdentidad="20100200200", tipoDocumentoIdentidad="6", ), importeTotalRetenido=Decimal("33.00"), importeTotalPagado=Decimal("967.00"), tipoRegimen="01", tipoRegimenPorcentaje=Decimal("0.03"), operaciones=[ PercepcionRetencionOperacion( numeroOperacion=1, fechaOperacion=date(2025, 6, 10), importeOperacion=Decimal("18.00"), comprobante=ComprobanteAfectado( tipoComprobante="01", serieNumero="F005-20", fechaEmision=date(2025, 6, 10), importeTotal=Decimal("300.00"), moneda="PEN", ), ), PercepcionRetencionOperacion( numeroOperacion=2, fechaOperacion=date(2025, 6, 10), importeOperacion=Decimal("15.00"), comprobante=ComprobanteAfectado( tipoComprobante="01", serieNumero="F005-21", fechaEmision=date(2025, 6, 10), importeTotal=Decimal("250.00"), moneda="PEN", ), ), ],)
xml = render_retention(retention)errors = SunatValidator().validate_schema( xml, "sunat_schemas/xsd_2.1/2.0/maindoc/UBLPE-Retention-1.0.xsd",)assert errors == [], errorsprint(xml)import { createRetention } from "@openubl/sdk";import { zRetention } from "@openubl/sdk/zod.gen";
const retention = zRetention.parse({ serie: "R001", numero: 2, fechaEmision: "2025-06-10", proveedor: { ruc: "20100100100", razonSocial: "Mi Empresa S.A.C." }, cliente: { nombre: "Comercial del Sur S.A.", numeroDocumentoIdentidad: "20100200200", tipoDocumentoIdentidad: "6", }, importeTotalRetenido: "33.00", importeTotalPagado: "967.00", tipoRegimen: "01", tipoRegimenPorcentaje: "0.03", operaciones: [ { numeroOperacion: 1, fechaOperacion: "2025-06-10", importeOperacion: "18.00", comprobante: { tipoComprobante: "01", serieNumero: "F005-20", fechaEmision: "2025-06-10", importeTotal: "300.00", moneda: "PEN", }, }, { numeroOperacion: 2, fechaOperacion: "2025-06-10", importeOperacion: "15.00", comprobante: { tipoComprobante: "01", serieNumero: "F005-21", fechaEmision: "2025-06-10", importeTotal: "250.00", moneda: "PEN", }, }, ],});
const { data, error } = await createRetention({ body: retention,});
if (error) { throw new Error(JSON.stringify(error));}
console.log(data.xml);curl -X POST "http://localhost:8000/api/v1/retention/create" \ -H "Content-Type: application/json" \ -d '{ "serie": "R001", "numero": 2, "fechaEmision": "2025-06-10", "proveedor": { "ruc": "20100100100", "razonSocial": "Mi Empresa S.A.C." }, "cliente": { "nombre": "Comercial del Sur S.A.", "numeroDocumentoIdentidad": "20100200200", "tipoDocumentoIdentidad": "6" }, "importeTotalRetenido": "33.00", "importeTotalPagado": "967.00", "tipoRegimen": "01", "tipoRegimenPorcentaje": "0.03", "operaciones": [ { "numeroOperacion": 1, "fechaOperacion": "2025-06-10", "importeOperacion": "18.00", "comprobante": { "tipoComprobante": "01", "serieNumero": "F005-20", "fechaEmision": "2025-06-10", "importeTotal": "300.00", "moneda": "PEN" } }, { "numeroOperacion": 2, "fechaOperacion": "2025-06-10", "importeOperacion": "15.00", "comprobante": { "tipoComprobante": "01", "serieNumero": "F005-21", "fechaEmision": "2025-06-10", "importeTotal": "250.00", "moneda": "PEN" } } ] }'Respuesta esperada
Sección titulada «Respuesta esperada»{ "xml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>..."}Si validate=true y el cuerpo no cumple las reglas, la API devuelve un error 422 con el detalle de la validación.