Skip to main content

Tipos de Envío

SUNAT maneja dos modalidades de envío según el tipo de comprobante:

Envío Síncrono (SOAP)

Respuesta inmediata con CDR. Documentos:
  • Facturas (01)
  • Notas de Crédito (07)
  • Notas de Débito (08)
Flujo:
Sistema → SUNAT → CDR inmediato

Envío Asíncrono (Tickets)

Requiere polling posterior. Documentos:
  • Boletas (03) - Vía Resumen Diario
  • Guías de Remisión (09, 31) - Vía API REST GRE
  • Comunicación de Baja (anulación de facturas)
Flujo:
Sistema → SUNAT → Ticket
Sistema → Consultar Ticket → CDR (cuando esté listo)
Tiempo de procesamiento de tickets:
  • Resumen Diario de Boletas: 5-30 minutos
  • Guías de Remisión: 2-10 segundos
  • Comunicación de Baja: 5-15 minutos
El código 98 indica “en proceso”.

Envío de Facturas (Síncrono)

1

Generar XML firmado

Al crear una venta (factura), el sistema puede generar el XML automáticamente o hacerlo bajo demanda.Endpoint:
POST /api/ventas/{id_venta}/generar-xml
Authorization: Bearer {token}
Proceso interno (SunatService::generarXml() línea 159-247):
  1. Carga la venta con relaciones:
    $venta->load(['cliente', 'empresa', 'productosVentas', 'tipoDocumento', 'cuotas']);
    
  2. Construye objetos Greenter (Invoice, Company, Client, SaleDetail)
  3. Calcula montos con IGV:
    $igvRate = 0.18;  // 18%
    $montoGravada = $total / ($igvRate + 1);  // Base imponible
    $igvMonto = $montoGravada * $igvRate;
    
  4. Añade leyenda en letras:
    (new Legend())
        ->setCode('1000')
        ->setValue('SON QUINIENTOS CINCUENTA SOLES')
    
  5. Firma el XML con el certificado:
    $see = $this->getSee($empresa);
    $xmlContent = $see->getXmlSigned($invoice);
    
  6. Guarda en storage/app/sunat/xml/{ruc}/{nombre_archivo}.xml
  7. Extrae el hash del XML firmado:
    preg_match('/<ds:DigestValue>([^<]+)<\/ds:DigestValue>/', $xml, $matches);
    $hash = $matches[1];
    
  8. Actualiza la venta:
    $venta->update([
        'hash_cpe' => $hash,
        'xml_url' => "sunat/xml/{$ruc}/{nombreArchivo}.xml",
        'nombre_xml' => $nombreArchivo,
    ]);
    
Nombre del archivo XML:
{RUC}-{TipoDoc}-{Serie}-{Numero}.xml

Ejemplo:
20612706702-01-F001-00000045.xml
2

Enviar a SUNAT

Una vez generado el XML, envíealo:Endpoint:
POST /api/ventas/{id_venta}/enviar-sunat
Authorization: Bearer {token}
Proceso interno (SunatService::enviarComprobante() línea 249-317):
  1. Lee el XML desde storage:
    $xmlPath = storage_path("app/{$venta->xml_url}");
    $xmlContent = file_get_contents($xmlPath);
    
  2. Envía vía SOAP:
    $see = $this->getSee($empresa);
    $result = $see->sendXml(Invoice::class, $nombreArchivo, $xmlContent);
    
  3. Si es exitoso:
    • Extrae el CDR (Constancia de Recepción)
    • Guarda el CDR en storage/app/sunat/cdr/{ruc}/R-{nombreArchivo}.zip
    • Actualiza la venta:
      $venta->update([
          'estado_sunat' => '1',  // Aceptado
          'cdr_url' => "sunat/cdr/{$ruc}/R-{nombreArchivo}.zip",
          'codigo_sunat' => $cdr->getCode(),  // Ej: 0 (aceptado)
          'mensaje_sunat' => $cdr->getDescription(),
      ]);
      
  4. Si es rechazado:
    • Registra el error en logs:
      Log::error('SUNAT - Comprobante rechazado', [
          'venta' => $venta->serie . '-' . $venta->numero,
          'codigo' => $error->getCode(),
          'mensaje' => $error->getMessage(),
      ]);
      
    • Actualiza la venta:
      $venta->update([
          'estado_sunat' => '3',  // Rechazado
          'codigo_sunat' => $error->getCode(),
          'mensaje_sunat' => $error->getMessage(),
          'intentos' => ($venta->intentos ?? 0) + 1,
      ]);
      
3

Descargar CDR

El CDR (Constancia de Recepción) es un archivo ZIP que contiene:
  • XML de respuesta de SUNAT
  • Código de aceptación (0, 0001, etc.)
  • Observaciones o errores
Descarga desde la UI: En la lista de ventas, haga clic en el botón Descargar CDR de la venta enviada.URL directa:
/storage/sunat/cdr/{ruc}/R-20612706702-01-F001-00000045.zip

Envío de Notas de Crédito/Débito

El flujo es similar al de facturas:
POST /api/notas-credito/{id_nota}/generar-xml
POST /api/notas-credito/{id_nota}/enviar-sunat
Diferencias clave:
  1. El XML incluye referencia al documento afectado:
    $note->setTipDocAfectado('01')  // Tipo de doc original
         ->setNumDocfectado('F001-00000045')  // Serie-Número
         ->setCodMotivo('01')  // Código de motivo SUNAT
         ->setDesMotivo('Anulación de la operación');
    
  2. Al aceptarse, la venta original se marca como anulada:
    $nota->venta->update([
        'estado' => '2',  // Anulado
        'estado_sunat' => '2',
    ]);
    

Envío de Guías de Remisión (Asíncrono)

Las guías usan la API REST GRE de SUNAT con autenticación OAuth.
1

Generar XML de guía

POST /api/guias-remision/{id_guia}/generar-xml
En SunatService::generarGuiaRemisionXml() (línea 567-666):
  • Construye objeto Despatch con envio (Shipment)
  • Incluye conductor, vehículo, direcciones de partida/llegada
  • Tipo doc: ‘09’ (remitente) o ‘31’ (transportista)
2

Enviar a API GRE

POST /api/guias-remision/{id_guia}/enviar-sunat
En SunatService::enviarGuiaRemision() (línea 766-842):
  1. Obtener token OAuth:
    $authApi = new AuthApi();
    $token = $authApi->getToken(
        'password',
        'https://api-cpe.sunat.gob.pe',
        $clientId,
        $clientSecret,
        $username,  // {RUC}{usuario_sol}
        $password   // clave_sol
    );
    
  2. Crear ZIP del XML:
    $zip = new ZipArchive();
    $zip->addFromString("{nombreXml}.xml", $xmlContent);
    
  3. Enviar vía REST:
    $cpeApi = new CpeApi();
    $cpeApi->setAccessToken($token->getAccessToken());
    
    $archivo = new CpeDocumentArchivo();
    $archivo->setNomArchivo("{nombreXml}.zip");
    $archivo->setArcGreZip(base64_encode($zipContent));
    $archivo->setHashZip(hash('sha256', $zipContent));
    
    $response = $cpeApi->enviarCpe($nombreXml, $cpeDoc);
    
  4. Guardar ticket:
    $ticket = $response->getNumTicket();
    $guia->update([
        'estado' => 'enviado',
        'ticket_sunat' => $ticket,
    ]);
    
3

Consultar ticket

Después de unos segundos, consulte el estado:
POST /api/guias-remision/{id_guia}/ticket
En SunatService::consultarTicketGuia() (línea 844-942):
$status = $cpeApi->consultarEnvio($ticket);
$codRespuesta = $status->getCodRespuesta();

if ($codRespuesta === '0') {
    // Aceptado - descargar CDR
    $cdrBase64 = $status->getArcCdr();
    $cdrContent = base64_decode($cdrBase64);
    // Guardar CDR...
} elseif ($codRespuesta === '98') {
    // Aún en proceso - esperar y reintentar
} else {
    // Rechazado - registrar error
}

Estados de Envío (estado_sunat)

CódigoSignificadoDescripción
nullSin enviarAún no se ha intentado el envío
0GeneradoXML generado pero no enviado
1AceptadoSUNAT aceptó el comprobante
2AnuladoComprobante anulado (por nota de crédito o comunicación de baja)
3Rechazado/En procesoRechazado por SUNAT o esperando ticket

Estructura del CDR

El CDR es un ZIP que contiene un XML de respuesta:
<?xml version="1.0" encoding="UTF-8"?>
<ApplicationResponse>
  <DocumentResponse>
    <Response>
      <ResponseCode>0</ResponseCode>
      <Description>La Factura numero F001-00000045, ha sido aceptada</Description>
    </Response>
  </DocumentResponse>
</ApplicationResponse>
Códigos de respuesta comunes:

Reintentos Automáticos

El sistema registra intentos fallidos en el campo intentos:
$venta->update([
    'intentos' => ($venta->intentos ?? 0) + 1,
]);
Puede configurar un worker de cola para reintentar envíos rechazados:
// En un Job programado
$ventasFallidas = Venta::where('estado_sunat', '3')
    ->where('intentos', '<', 3)
    ->get();

foreach ($ventasFallidas as $venta) {
    dispatch(new ReintentarEnvioSunat($venta));
}

Envío Masivo

Para enviar múltiples comprobantes:
POST /api/ventas/enviar-masivo
Content-Type: application/json

{
  "ids": [45, 46, 47, 48]
}
El endpoint debe iterar y enviar cada uno:
foreach ($ids as $id) {
    $venta = Venta::find($id);
    
    if (!$venta->xml_url) {
        $this->sunatService->generarXml($venta);
    }
    
    $resultado = $this->sunatService->enviarComprobante($venta);
    // Almacenar resultados...
}

Solución de Problemas

El campo xml_url de la venta está vacío. Debe generar el XML antes de enviar:
POST /api/ventas/{id_venta}/generar-xml
Problema con el certificado digital:
  1. Verifique que el .pem exista en storage/app/sunat/certificados/{ruc}-cert.pem
  2. Verifique que no haya expirado: openssl x509 -in cert.pem -noout -dates
  3. Verifique que coincida con el RUC
Ver: Configurar Certificados
Problema de conectividad con SUNAT:
  1. Verifique conexión a internet del servidor
  2. Verifique que el endpoint sea correcto en config/sunat.php
  3. Pruebe acceso manual: curl https://e-factura.sunat.gob.pe
Si el rechazo fue por un error corregible:
  1. Corrija el error en la venta (ej: datos del cliente)
  2. Regenere el XML: POST /api/ventas/{id}/generar-xml
  3. Reenvíe: POST /api/ventas/{id}/enviar-sunat
Si el error es grave (ej: monto incorrecto), debe anular con nota de crédito y crear una nueva venta.

Logs de Envío

Los errores se registran en storage/logs/laravel.log:
[2026-03-06 14:30:45] local.ERROR: SUNAT - Comprobante rechazado
{
  "venta": "F001-00000045",
  "codigo": "2010",
  "mensaje": "El RUC del cliente no existe"
}
Para habilitar logging detallado de SOAP, configure en .env:
LOG_LEVEL=debug

Próximos Pasos

Build docs developers (and LLMs) love