What is a CDR?
The CDR (Constancia de Recepción) is SUNAT’s official digital response that confirms receipt and validation of your electronic document. It serves as legal proof that SUNAT accepted your invoice, note, or other electronic document.
The CDR is a digitally signed XML file delivered in a ZIP archive. It contains SUNAT’s validation results, acceptance status, and any observations or warnings.
CDR Structure
ZIP Archive
SUNAT returns CDRs as ZIP files with this naming convention:
R-{RUC}-{TipoDoc}-{Serie}-{Correlativo}.zip
Example: R-20123456789-01-F001-00000123.zip
CDR XML Content
Inside the ZIP, the XML follows this structure:
<? xml version = "1.0" encoding = "UTF-8" ?>
< ApplicationResponse xmlns = "urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2"
xmlns:cac = "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc = "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
xmlns:ds = "http://www.w3.org/2000/09/xmldsig#" >
<!-- CDR Identifier -->
< cbc:ID > 20123456789-01-F001-00000123 </ cbc:ID >
< cbc:IssueDate > 2025-09-02 </ cbc:IssueDate >
< cbc:IssueTime > 10:35:12 </ cbc:IssueTime >
<!-- Document Reference -->
< cac:DocumentResponse >
< cac:Response >
< cbc:ResponseCode > 0 </ cbc:ResponseCode >
< cbc:Description > La Factura numero F001-00000123, ha sido aceptada </ cbc:Description >
</ cac:Response >
< cac:DocumentReference >
< cbc:ID > F001-00000123 </ cbc:ID >
< cbc:DocumentTypeCode > 01 </ cbc:DocumentTypeCode >
< cac:IssuerParty >
< cac:PartyIdentification >
< cbc:ID schemeID = "6" > 20123456789 </ cbc:ID >
</ cac:PartyIdentification >
</ cac:IssuerParty >
</ cac:DocumentReference >
</ cac:DocumentResponse >
<!-- Digital Signature (SUNAT) -->
< ds:Signature >
<!-- SUNAT's digital signature -->
</ ds:Signature >
</ ApplicationResponse >
Processing CDR Responses
Saving CDR Files
FileService.php:18-24
FileService.php:34-62
public function saveCdr ( $document , string $cdrContent ) : string
{
$this -> ensureDirectoryExists ( $document , 'zip' );
$path = $this -> generatePath ( $document , 'zip' );
Storage :: disk ( 'public' ) -> put ( $path , $cdrContent );
return $path ;
}
CDR Storage Structure
Example directory structure:
storage/app/public/
├── facturas/
│ ├── xml/
│ │ └── 02092025/
│ │ └── F001-00000123.xml
│ ├── cdr/
│ │ └── 02092025/
│ │ └── R-F001-00000123.zip
│ └── pdf/
│ └── 02092025/
│ └── F001-00000123.pdf
├── boletas/
│ ├── xml/
│ ├── cdr/
│ └── pdf/
└── notas-credito/
├── xml/
├── cdr/
└── pdf/
Parsing CDR Response
DocumentService.php:216-228
if ( $result [ 'success' ] && $result [ 'cdr_zip' ]) {
$cdrPath = $this -> fileService -> saveCdr ( $document , $result [ 'cdr_zip' ]);
$document -> cdr_path = $cdrPath ;
$document -> estado_sunat = 'ACEPTADO' ;
$document -> respuesta_sunat = json_encode ([
'id' => $result [ 'cdr_response' ] -> getId (),
'code' => $result [ 'cdr_response' ] -> getCode (),
'description' => $result [ 'cdr_response' ] -> getDescription (),
'notes' => $result [ 'cdr_response' ] -> getNotes (),
]);
// Get hash from XML
$xmlSigned = $greenterService -> getXmlSigned ( $greenterDocument );
if ( $xmlSigned ) {
$document -> codigo_hash = $this -> extractHashFromXml ( $xmlSigned );
}
}
CDR Response Object
The cdr_response object provides these methods:
CDR unique identifier (format: {RUC}-{TipoDoc}-{Serie}-{Correlativo})
Response code:
0 = Accepted
98 = Accepted with observations
Other codes indicate rejection
Human-readable response message from SUNAT
Array of observation or warning messages (even when code is 0)
CDR Response Codes
Success Codes
Meaning: Document was accepted without issues.Action: Store CDR, update document status to ACEPTADO, and proceed normally.{
"code" : "0" ,
"description" : "La Factura numero F001-00000123, ha sido aceptada" ,
"notes" : []
}
Code 98 - Accepted with Observations
Meaning: Document was accepted but SUNAT found minor issues (non-blocking).Action: Store CDR, update status to ACEPTADO, but review notes for warnings.{
"code" : "98" ,
"description" : "La Factura ha sido aceptada con observaciones" ,
"notes" : [
"El campo [cbc:DocumentCurrencyCode] tiene un valor diferente al esperado" ,
"El RUC del receptor no está activo en SUNAT"
]
}
While code 98 documents are legally valid, you should investigate and fix the issues noted to avoid future problems.
Rejection Codes
Code 2000-2999: Schema and Validation Errors
Code Description Solution 2324 Invalid RUC Verify company RUC is correct and registered 2335 Duplicate document Document with same series-correlative already exists 2336 Invalid correlative Correlative must be sequential 2800 Invalid XML schema Validate against UBL 2.1 XSD 2801 Invalid signature Check certificate validity and signing process 2802 Certificate expired Renew digital certificate
Code 1000-1999: Business Logic Errors
Code Description Solution 1032 RUC suspended Company has tax irregularities - resolve with SUNAT 1033 RUC not authorized Company not authorized for electronic invoicing 1034 Invalid client RUC Client RUC is invalid or doesn’t exist
Code 3000-3999: Content Errors
Code Description Solution 3000 Malformed XML Check XML structure and encoding 3100 Missing required field Add all mandatory UBL elements 3200 Invalid tax calculation Verify IGV, ISC, ICBPER calculations
Handling Rejection
Error Response Processing
DocumentService.php:234-259
else {
$document -> estado_sunat = 'RECHAZADO' ;
// Handle different error types
$errorCode = 'UNKNOWN' ;
$errorMessage = 'Unknown error' ;
if ( is_object ( $result [ 'error' ])) {
if ( method_exists ( $result [ 'error' ], 'getCode' )) {
$errorCode = $result [ 'error' ] -> getCode ();
} elseif ( property_exists ( $result [ 'error' ], 'code' )) {
$errorCode = $result [ 'error' ] -> code ;
}
if ( method_exists ( $result [ 'error' ], 'getMessage' )) {
$errorMessage = $result [ 'error' ] -> getMessage ();
} elseif ( property_exists ( $result [ 'error' ], 'message' )) {
$errorMessage = $result [ 'error' ] -> message ;
}
}
$document -> respuesta_sunat = json_encode ([
'code' => $errorCode ,
'message' => $errorMessage ,
]);
}
Retry Logic
Example Retry Implementation
public function retryRejectedDocument ( $document , int $maxRetries = 3 ) : array
{
$attempt = 0 ;
$lastError = null ;
while ( $attempt < $maxRetries ) {
$attempt ++ ;
Log :: info ( "Retry attempt { $attempt }/{ $maxRetries }" , [
'document_id' => $document -> id ,
'serie_correlativo' => $document -> numero_completo
]);
// Re-send to SUNAT
$result = $this -> sendToSunat ( $document , 'invoice' );
if ( $result [ 'success' ]) {
return [
'success' => true ,
'message' => "Document accepted on attempt { $attempt }" ,
'attempts' => $attempt
];
}
$lastError = $result [ 'error' ];
// Check if error is retryable
$errorCode = $lastError -> code ?? 'UNKNOWN' ;
if ( ! $this -> isRetryableError ( $errorCode )) {
break ; // Don't retry non-retryable errors
}
// Exponential backoff
if ( $attempt < $maxRetries ) {
sleep ( pow ( 2 , $attempt )); // 2, 4, 8 seconds
}
}
return [
'success' => false ,
'message' => "Document rejected after { $attempt } attempts" ,
'error' => $lastError ,
'attempts' => $attempt
];
}
protected function isRetryableError ( string $code ) : bool
{
// Network errors and temporary issues
$retryableCodes = [
'SOAP-ERROR' , // Network/connection errors
'TIMEOUT' , // Request timeout
'1004' , // Service temporarily unavailable
'1005' , // Service overloaded
];
return in_array ( $code , $retryableCodes );
}
CDR Validation
Verifying CDR Signature
SUNAT signs all CDRs with their own digital certificate. You can validate the signature to ensure authenticity.
Example CDR Signature Validation
use RobRichards\XMLSecLibs\ XMLSecurityDSig ;
use RobRichards\XMLSecLibs\ XMLSecurityKey ;
public function validateCdrSignature ( string $cdrXml ) : bool
{
try {
$doc = new \DOMDocument ();
$doc -> loadXML ( $cdrXml );
$objXMLSecDSig = new XMLSecurityDSig ();
$objDSig = $objXMLSecDSig -> locateSignature ( $doc );
if ( ! $objDSig ) {
throw new \Exception ( 'Signature not found in CDR' );
}
$objXMLSecDSig -> canonicalizeSignedInfo ();
$objKey = $objXMLSecDSig -> locateKey ();
if ( ! $objKey ) {
throw new \Exception ( 'Key not found in signature' );
}
// Load the public key from certificate
$objKey -> loadKey ( $objXMLSecDSig -> getCertificate ());
// Verify signature
$isValid = $objXMLSecDSig -> verify ( $objKey );
Log :: info ( 'CDR signature validation' , [
'is_valid' => $isValid
]);
return $isValid ;
} catch ( \ Exception $e ) {
Log :: error ( 'CDR signature validation failed' , [
'error' => $e -> getMessage ()
]);
return false ;
}
}
Querying Document Status
You can query the status of previously sent documents:
ConsultaCpeService.php:36-60
ConsultaCpeService.php:259-272
public function consultarComprobante ( $documento ) : array
{
try {
// Get valid token
$token = $this -> obtenerTokenValido ();
if ( ! $token ) {
return [
'success' => false ,
'message' => 'Could not obtain authentication token' ,
'data' => null
];
}
// Configure query API
$config = Configuration :: getDefaultConfiguration ()
-> setAccessToken ( $token )
-> setHost ( $this -> getApiHost ());
$apiInstance = new ConsultaApi ( new Client (), $config );
// Create query filter
$cpeFilter = $this -> crearFiltroCpe ( $documento );
// Perform query
$result = $apiInstance -> consultarCpe ( $this -> company -> ruc , $cpeFilter );
// ...
}
}
Status Query Response
Document status code:
0 = Does not exist
1 = Accepted
2 = Annulled
3 = Authorized
-1 = Query error
Issuer RUC status (e.g., “ACTIVO”, “SUSPENDIDO”)
Tax domicile condition (e.g., “HABIDO”, “NO HABIDO”)
CDR Download Endpoints
FileService.php:141-151
Example API Endpoint
public function downloadCdr ( $document )
{
if ( ! $document -> cdr_path || ! Storage :: disk ( 'public' ) -> exists ( $document -> cdr_path )) {
return null ;
}
return Storage :: disk ( 'public' ) -> download (
$document -> cdr_path ,
'R-' . $document -> numero_completo . '.zip'
);
}
Database Schema
Storing CDR information:
Invoice Model Fields
Example Query
protected $fillable = [
// ... other fields
'cdr_path' , // Path to CDR ZIP file
'estado_sunat' , // SUNAT status: PENDIENTE, ACEPTADO, RECHAZADO
'respuesta_sunat' , // JSON: {code, description, notes}
'codigo_hash' , // Document hash from signature
'consulta_cpe_estado' , // Status from query
'consulta_cpe_respuesta' , // Full query response (JSON)
'consulta_cpe_fecha' , // Last query timestamp
];
Best Practices
Never delete CDRs - They are legal proof of acceptance (required for 5+ years)
Backup regularly - Store CDRs in multiple locations
Archive old CDRs - Move older CDRs to cold storage after 1-2 years
Verify integrity - Periodically check CDR files are not corrupted
Document metadata - Store response codes and descriptions in database
Parse error codes - Different codes require different actions
Implement retry logic - Only for transient errors (network, timeout)
Log all attempts - Track submission history for debugging
Alert on patterns - Monitor rejection rates and common errors
Provide user feedback - Show clear, actionable error messages
Validate signatures - Verify SUNAT’s signature on CDRs
Audit trail - Log all CDR processing activities
Status tracking - Maintain document lifecycle status
Reconciliation - Periodically reconcile local status with SUNAT
Documentation - Keep records of rejection reasons and resolutions
Monitoring and Alerting
Example Monitoring Implementation
use Illuminate\Support\Facades\ Cache ;
use Illuminate\Support\Facades\ Log ;
class CdrMonitoringService
{
public function trackRejection ( string $errorCode , string $documentType ) : void
{
$key = "cdr_rejection_{ $documentType }_{ $errorCode }_" . date ( 'Y-m-d' );
Cache :: increment ( $key , 1 );
Cache :: expire ( $key , 86400 * 7 ); // Keep for 7 days
// Alert if rejection rate is high
$count = Cache :: get ( $key , 0 );
if ( $count >= 10 ) {
$this -> alertHighRejectionRate ( $errorCode , $documentType , $count );
}
}
public function getRejectionStats ( string $documentType , int $days = 7 ) : array
{
$stats = [];
for ( $i = 0 ; $i < $days ; $i ++ ) {
$date = date ( 'Y-m-d' , strtotime ( "-{ $i } days" ));
$stats [ $date ] = $this -> getDayStats ( $documentType , $date );
}
return $stats ;
}
protected function alertHighRejectionRate ( string $code , string $type , int $count ) : void
{
Log :: alert ( 'High CDR rejection rate detected' , [
'error_code' => $code ,
'document_type' => $type ,
'count_today' => $count ,
'threshold' => 10
]);
// Send notification (email, Slack, etc.)
// Notification::send(...);
}
}
Next Steps
Daily Summaries Learn about sending boletas via daily summaries
Voided Documents Annul documents with comunicaciones de baja
SUNAT Integration Understand SUNAT endpoints and communication
XML Signing Deep dive into digital signatures