Skip to main content
POST
/
api
/
comprobantes
/
enviar
/
{ventaId}
Enviar Venta a SUNAT
curl --request POST \
  --url https://api.example.com/api/comprobantes/enviar/{ventaId}
{
  "success": true,
  "codigo": "<string>",
  "mensaje": "<string>",
  "cdr_url": "<string>"
}

Endpoint

POST /api/comprobantes/enviar/{ventaId}

Autenticación

Requiere token de autenticación Bearer:
Authorization: Bearer {token}

Descripción

Envía un comprobante electrónico (Factura o Boleta) a SUNAT para su validación. Realiza las siguientes operaciones:
  1. Genera XML usando Greenter con los datos de la venta
  2. Firma digitalmente con certificado PEM de la empresa
  3. Envía a SUNAT vía SOAP (web service)
  4. Recibe CDR (Constancia de Recepción) si es aceptado
  5. Actualiza venta con hash, URLs y estado SUNAT
Este endpoint está implementado en ComprobanteElectronicoController y utiliza SunatService para la integración.

Parámetros de URL

ventaId
integer
required
ID de la venta a enviar (id_venta)

Flujo de Envío

Paso 1: Generación de XML

Se ejecuta automáticamente antes del envío:
// SunatService::generarXml()
- Carga venta con relaciones (cliente, empresa, productos, cuotas)
- Construye objeto Invoice de Greenter
- Calcula montos gravados/exonerados según IGV
- Genera detalles de productos con códigos SUNAT
- Añade forma de pago (contado/crédito con cuotas)
- Convierte monto a letras ("SON QUINIENTOS SOLES")
- Firma con certificado PEM
- Guarda XML en storage/app/sunat/xml/{ruc}/

Paso 2: Envío a SUNAT

// SunatService::enviarComprobante()
- Lee XML del storage
- Conecta a endpoint SOAP de SUNAT
- Usa credenciales SOL (RUC + Usuario + Clave)
- Envía XML firmado
- Espera respuesta síncrona

Paso 3: Procesamiento de Respuesta

Si es aceptado:
  • Descarga CDR (ZIP con respuesta XML de SUNAT)
  • Guarda en storage/app/sunat/cdr/{ruc}/R-{nombre}.zip
  • Actualiza venta: estado_sunat = '1', codigo_sunat, mensaje_sunat, cdr_url
Si es rechazado:
  • Registra código y mensaje de error
  • Actualiza venta: estado_sunat = '3', incrementa intentos

Campos Generados en XML

Estructura del Comprobante

Cálculo de Montos con IGV

Con IGV (tipo_afectacion_igv = ‘10’):

$igvRate = 0.18; // 18%
$precioConIGV = 100.00;

$valorUnitario = round($precioConIGV / 1.18, 2);  // 84.75
$valorVenta = $valorUnitario * $cantidad;
$igv = round($valorVenta * 0.18, 2);              // 15.25
$total = $valorVenta + $igv;                      // 100.00

// XML:
$invoice->setMtoOperGravadas($valorVenta)
        ->setMtoIGV($igv)
        ->setTotalImpuestos($igv)
        ->setValorVenta($valorVenta)
        ->setMtoImpVenta($total)
        ->setSubTotal($total);

Sin IGV (tipo_afectacion_igv = ‘20’):

$precioSinIGV = 100.00;

// XML:
$invoice->setMtoOperExoneradas($precioSinIGV)
        ->setMtoIGV(0)
        ->setTotalImpuestos(0)
        ->setValorVenta($precioSinIGV)
        ->setMtoImpVenta($precioSinIGV)
        ->setSubTotal($precioSinIGV);

Respuesta Exitosa (200)

success
boolean
true si SUNAT aceptó el comprobante
codigo
string
Código de respuesta de SUNAT (generalmente “0” para aceptado)
mensaje
string
Mensaje descriptivo de SUNAT (ej: “La Factura numero F001-349, ha sido aceptada”)
cdr_url
string
Ruta relativa al archivo CDR descargadoFormato: sunat/cdr/{ruc}/R-{ruc}-{tipoDoc}-{serie}-{numero}.zip

Ejemplo de Respuesta Exitosa

{
  "success": true,
  "codigo": "0",
  "mensaje": "La Factura numero F001-00000349, ha sido aceptada",
  "cdr_url": "sunat/cdr/20612706702/R-20612706702-01-F001-00000349.zip"
}

Errores

XML No Encontrado (400)

{
  "success": false,
  "message": "XML no encontrado. Genere el XML primero."
}
Solución: Generar XML primero con POST /api/comprobantes/generar-xml/{ventaId}

Rechazo de SUNAT (200 con success=false)

{
  "success": false,
  "codigo": "2324",
  "message": "El numero de RUC del emisor no existe"
}
Códigos de error comunes:
CódigoDescripción
2324RUC del emisor no existe
2335Numeración ya existe
2800Serie no corresponde al tipo de documento
3035Debe usar SOL de producción (no beta)
4000Error en la estructura del XML
1033Certificado revocado o inválido

Error de Conexión (500)

{
  "success": false,
  "message": "Error al enviar el comprobante a SUNAT",
  "codigo": "HTTP_ERROR"
}
Ocurre cuando:
  • SUNAT no responde (downtime)
  • Timeout de conexión
  • Error de red

Actualización de Base de Datos

Si es aceptado:

$venta->update([
  'estado_sunat' => '1',         // Aceptado
  'hash_cpe' => $hashFromXml,    // Hash del digest XML
  'cdr_url' => 'sunat/cdr/...',  // Ruta del CDR
  'codigo_sunat' => '0',         // Código de aceptación
  'mensaje_sunat' => 'La Factura...', // Mensaje de SUNAT
]);

Si es rechazado:

$venta->update([
  'estado_sunat' => '3',         // Rechazado
  'codigo_sunat' => $errorCode,  // Código de error
  'mensaje_sunat' => $errorMsg,  // Mensaje de error
  'intentos' => $intentos + 1,   // Incrementa contador
]);

Modo Beta vs Producción

Modo Beta (empresa.modo != ‘production’):

RUC: 20000000001
Usuario SOL: MODDATOS
Clave SOL: moddatos
Endpoint: https://e-beta.sunat.gob.pe/ol-ti-itcpfegem-beta/billService

Modo Producción:

RUC: {empresa.ruc}
Usuario SOL: {empresa.user_sol}
Clave SOL: {empresa.clave_sol}
Endpoint: https://e-factura.sunat.gob.pe/ol-ti-itcpfegem/billService

Certificado Digital

El certificado PEM debe estar en:
storage/app/sunat/certificados/{ruc}-cert.pem
O usar certificado global de pruebas:
config('sunat.certificado_prueba')

Ejemplo de Solicitud

curl -X POST "https://facturacion.santodomingo.pe/api/comprobantes/enviar/126" \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc..."

Requisitos Previos

✅ Venta creada con estado activo
✅ Empresa configurada con credenciales SOL
✅ Certificado PEM válido instalado
✅ Serie configurada en documentos_empresas

Consideraciones para Boletas

Las BOLETAS requieren un paso adicional:
  1. Enviar a SUNAT: POST /api/comprobantes/enviar/{ventaId}
  2. Enviar Resumen Diario: POST /api/resumen-diario ⚠️
Sin el Resumen Diario, la boleta NO es válida ante SUNAT aunque el envío directo sea exitoso.Las FACTURAS solo requieren el envío directo.

Archivos Generados

XML Firmado:

storage/app/sunat/xml/{ruc}/{ruc}-{tipoDoc}-{serie}-{numero}.xml
Ejemplo: 20612706702-01-F001-00000349.xml

CDR (Constancia):

storage/app/sunat/cdr/{ruc}/R-{ruc}-{tipoDoc}-{serie}-{numero}.zip
Ejemplo: R-20612706702-01-F001-00000349.zip El CDR contiene:
  • Respuesta XML firmada por SUNAT
  • Código de aceptación
  • Observaciones (si las hay)

Códigos de Tipo de Documento SUNAT

CódigoDescripción
01Factura
03Boleta de Venta
07Nota de Crédito
08Nota de Débito
09Guía de Remisión Remitente
31Guía de Remisión Transportista

Logging

Errores se registran en logs con contexto:
Log::error('SUNAT - Comprobante rechazado', [
  'venta' => $venta->serie . '-' . $venta->numero,
  'codigo' => $error->getCode(),
  'mensaje' => $error->getMessage(),
]);

Código de Referencia

  • XML Generation: app/Services/SunatService.php:159 método generarXml()
  • Envío: app/Services/SunatService.php:249 método enviarComprobante()
  • Controller: app/Http/Controllers/ComprobanteElectronicoController.php método enviar()

Temas Relacionados

Build docs developers (and LLMs) love