Skip to main content

Overview

Comunicaciones de Baja (Voided Documents) are special documents used to annul or void previously issued electronic documents. When you need to cancel an invoice, boleta, credit note, or debit note that was already sent to SUNAT, you must send a voided document communication.
You cannot simply delete an electronic document. Once sent to SUNAT, all annulments must be done through proper voided document communications.

When to Use Voided Documents

  • Customer cancels order after invoice was issued
  • Document issued with incorrect data (cannot be corrected with a note)
  • Duplicate documents sent by error
  • Document issued to wrong client
  • Price or quantity errors that exceed note correction limits
  • Cannot void documents older than allowed timeframe
  • Cannot void documents already referenced by credit/debit notes
  • Cannot void voided documents (no double voiding)
  • Must keep voided documents in records for 5+ years

Document Structure

Naming Convention

RA-{YYYYMMDD}-{NNN}

Examples:
RA-20250902-001  (First voided doc for September 2, 2025)
RA-20250902-002  (Second voided doc for September 2, 2025)
The date in the identifier (YYYYMMDD) refers to the emission date of the documents being voided, not the voided document’s creation date.

Creating Voided Documents

Greenter Implementation

public function createVoidedDocument(array $voidedData): Voided
{
    $voided = new Voided();
    
    // Basic configuration
    $voided->setCorrelativo($voidedData['correlativo'])
           ->setFecGeneracion(new \DateTime($voidedData['fecha_referencia'])) // Date of docs to annul
           ->setFecComunicacion(new \DateTime($voidedData['fecha_emision']))  // Communication date
           ->setCompany($this->getGreenterCompany());
    
    // Create details of documents to annul
    $details = [];
    foreach ($voidedData['detalles'] as $detalle) {
        $detail = new VoidedDetail();
        $detail->setTipoDoc($detalle['tipo_documento'])
               ->setSerie($detalle['serie'])
               ->setCorrelativo($detalle['correlativo'])
               ->setDesMotivoBaja($detalle['motivo_especifico']);
        
        $details[] = $detail;
    }
    
    $voided->setDetails($details);
    
    return $voided;
}

Voided Document Data Structure

correlativo
string
required
Sequential number for the voided document (001, 002, etc.)
fecha_referencia
date
required
Emission date of the documents being voided (must match original docs)
fecha_emision
date
required
Date when the voided document is being sent (communication date)
detalles
array
required
Array of documents to void. Each detail includes:
  • tipo_documento - Document type (01, 03, 07, 08)
  • serie - Document series
  • correlativo - Document correlative number
  • motivo_especifico - Specific reason for annulment

Sending to SUNAT

Asynchronous Processing

Like daily summaries, voided documents use asynchronous processing with a ticket:
public function sendVoidedDocument(Voided $voided)
{
    try {
        $see = $this->initializeSee();
        $result = $see->send($voided);
        
        return [
            'success' => $result->isSuccess(),
            'xml' => $see->getFactory()->getLastXml(),
            'ticket' => $result->isSuccess() ? $result->getTicket() : null,
            'error' => $result->isSuccess() ? null : $result->getError()
        ];
    } catch (Exception $e) {
        return [
            'success' => false,
            'xml' => null,
            'ticket' => null,
            'error' => (object)[
                'code' => 'EXCEPTION',
                'message' => $e->getMessage()
            ]
        ];
    }
}

Complete Workflow

1

Identify Documents to Void

Find documents to annul (must have same emission date):
$invoicesToVoid = Invoice::where('fecha_emision', '2025-09-02')
    ->whereIn('id', [123, 124, 125])
    ->where('estado_sunat', 'ACEPTADO')
    ->get();
2

Create Voided Document

Prepare voided document data:
$voidedData = [
    'correlativo' => '001',
    'fecha_referencia' => '2025-09-02', // Date of docs being voided
    'fecha_emision' => now()->toDateString(), // Today
    'detalles' => [
        [
            'tipo_documento' => '01',
            'serie' => 'F001',
            'correlativo' => '00000123',
            'motivo_especifico' => 'Error en datos del cliente'
        ],
        [
            'tipo_documento' => '01',
            'serie' => 'F001',
            'correlativo' => '00000124',
            'motivo_especifico' => 'Duplicado por error'
        ]
    ]
];

$greenterService = new GreenterService($company);
$voidedDoc = $greenterService->createVoidedDocument($voidedData);
3

Send and Receive Ticket

Send to SUNAT and get ticket:
$result = $greenterService->sendVoidedDocument($voidedDoc);

if ($result['success']) {
    $ticket = $result['ticket'];
    // Save ticket for later status checking
}
4

Poll for CDR

Wait and check status (typically 1-5 minutes):
// After 30-60 seconds
$statusResult = $greenterService->checkVoidedDocumentStatus($ticket);

if ($statusResult['success']) {
    // Voided document accepted
    $cdr = $statusResult['cdr_response'];
}
5

Update Original Documents

Mark original documents as voided:
foreach ($invoicesToVoid as $invoice) {
    $invoice->update([
        'estado_sunat' => 'ANULADO',
        'fecha_anulacion' => now(),
        'voided_document_id' => $voidedDocument->id
    ]);
}

Annulment Reasons

Provide clear, specific reasons for each voided document:
  • Error en datos del cliente - Error in client data
  • Error en montos o cantidades - Error in amounts or quantities
  • Documento duplicado - Duplicate document
  • Emitido a cliente incorrecto - Issued to wrong client
  • Cancelación de operación - Operation cancelled
  • Emisión por error - Issued by mistake
  • Error en descripción de productos - Error in product descriptions
  • Cliente solicita anulación - Client requests annulment
  1. Be specific - General reasons may be questioned
  2. Be brief - SUNAT may have character limits
  3. Be consistent - Use standard reason codes in your system
  4. Document internally - Keep detailed records beyond SUNAT requirement
  5. Avoid ambiguity - Clear reasons help with audits

Voided vs. Credit Notes

Understand when to use voided documents versus credit notes:
ScenarioUseReason
Total cancellation same dayVoided DocumentCompletely cancels the original
Partial refundCredit NoteAdjusts amount, doesn’t cancel
Price correctionCredit NoteCorrects specific items
Wrong clientVoided DocumentCannot correct client with note
After deadlineCredit NoteVoiding deadline passed
Return of goodsCredit NoteProper accounting treatment

Database Schema

class VoidedDocument extends Model
{
    protected $fillable = [
        'company_id',
        'branch_id',
        'correlativo',
        'fecha_referencia',    // Date of documents being voided
        'fecha_emision',       // Communication date
        'detalles',           // JSON array of documents to void
        'ticket',             // SUNAT ticket for async processing
        'xml_path',
        'cdr_path',
        'estado_sunat',       // PENDIENTE, PROCESANDO, ACEPTADO, RECHAZADO
        'respuesta_sunat',    // JSON response
        'codigo_hash',
        'usuario_creacion',
    ];
    
    protected $casts = [
        'fecha_referencia' => 'date',
        'fecha_emision' => 'date',
        'detalles' => 'array',
        'respuesta_sunat' => 'array',
    ];
    
    public function company()
    {
        return $this->belongsTo(Company::class);
    }
    
    public function voidedInvoices()
    {
        return $this->hasMany(Invoice::class, 'voided_document_id');
    }
    
    public function voidedBoletas()
    {
        return $this->hasMany(Boleta::class, 'voided_document_id');
    }
}

Service Implementation

public function createVoidedDocument(array $data): VoidedDocument
{
    return DB::transaction(function () use ($data) {
        $company = Company::findOrFail($data['company_id']);
        $branch = Branch::where('company_id', $company->id)
                       ->where('id', $data['branch_id'])
                       ->firstOrFail();
        
        // Get next correlative for voided documents
        $correlativo = $this->getNextVoidedCorrelative(
            $company->id, 
            $data['fecha_referencia']
        );
        
        $voidedDocument = VoidedDocument::create([
            'company_id' => $company->id,
            'branch_id' => $branch->id,
            'correlativo' => $correlativo,
            'fecha_referencia' => $data['fecha_referencia'],
            'fecha_emision' => now()->toDateString(),
            'detalles' => $data['detalles'],
            'estado_sunat' => 'PENDIENTE',
            'usuario_creacion' => $data['usuario_creacion'] ?? null,
        ]);
        
        return $voidedDocument;
    });
}

public function sendVoidedDocumentToSunat(VoidedDocument $voidedDocument): array
{
    try {
        $company = $voidedDocument->company;
        $greenterService = new GreenterService($company);
        
        // Prepare data
        $voidedData = [
            'correlativo' => $voidedDocument->correlativo,
            'fecha_referencia' => $voidedDocument->fecha_referencia->toDateString(),
            'fecha_emision' => $voidedDocument->fecha_emision->toDateString(),
            'detalles' => $voidedDocument->detalles,
        ];
        
        // Create Greenter document
        $greenterVoided = $greenterService->createVoidedDocument($voidedData);
        
        // Send to SUNAT
        $result = $greenterService->sendVoidedDocument($greenterVoided);
        
        if ($result['success']) {
            // Save XML and update status
            $xmlPath = $this->fileService->saveXml($voidedDocument, $result['xml']);
            
            $voidedDocument->update([
                'xml_path' => $xmlPath,
                'estado_sunat' => 'PROCESANDO',
                'ticket' => $result['ticket'],
                'codigo_hash' => $this->extractHashFromXml($result['xml']),
            ]);
            
            return [
                'success' => true,
                'document' => $voidedDocument->fresh(),
                'ticket' => $result['ticket']
            ];
        } else {
            $voidedDocument->update([
                'estado_sunat' => 'RECHAZADO',
                'respuesta_sunat' => json_encode($result['error'])
            ]);
            
            return [
                'success' => false,
                'document' => $voidedDocument->fresh(),
                'error' => $result['error']
            ];
        }
        
    } catch (Exception $e) {
        $voidedDocument->update([
            'estado_sunat' => 'RECHAZADO',
            'respuesta_sunat' => json_encode(['message' => $e->getMessage()])
        ]);
        
        return [
            'success' => false,
            'document' => $voidedDocument->fresh(),
            'error' => (object)['message' => $e->getMessage()]
        ];
    }
}

protected function getNextVoidedCorrelative(int $companyId, string $fechaReferencia): string
{
    $lastVoided = VoidedDocument::where('company_id', $companyId)
                               ->where('fecha_referencia', $fechaReferencia)
                               ->orderBy('correlativo', 'desc')
                               ->first();
    
    if (!$lastVoided) {
        return '001';
    }
    
    $nextCorrelativo = intval($lastVoided->correlativo) + 1;
    return str_pad($nextCorrelativo, 3, '0', STR_PAD_LEFT);
}

Polling for Status

public function checkVoidedDocumentStatus(VoidedDocument $voidedDocument): array
{
    try {
        if (empty($voidedDocument->ticket)) {
            return [
                'success' => false,
                'error' => 'No ticket available'
            ];
        }
        
        $company = $voidedDocument->company;
        $greenterService = new GreenterService($company);
        
        $result = $greenterService->checkVoidedDocumentStatus($voidedDocument->ticket);
        
        if ($result['success'] && $result['cdr_response']) {
            // Save CDR
            $cdrPath = $this->fileService->saveCdr($voidedDocument, $result['cdr_zip']);
            
            // Update voided document
            $voidedDocument->update([
                'cdr_path' => $cdrPath,
                'estado_sunat' => 'ACEPTADO',
                'respuesta_sunat' => json_encode([
                    'code' => $result['cdr_response']->getCode(),
                    'description' => $result['cdr_response']->getDescription()
                ])
            ]);
            
            // Update all documents referenced in the voided document
            $this->updateVoidedDocuments($voidedDocument);
            
            return [
                'success' => true,
                'document' => $voidedDocument->fresh(),
                'cdr_response' => $result['cdr_response']
            ];
        } else {
            // Error or still processing
            if (isset($result['error']->code) && $result['error']->code === '98') {
                // Still processing, keep PROCESANDO status
                return [
                    'success' => false,
                    'processing' => true,
                    'error' => $result['error']
                ];
            } else {
                // Actual error
                $voidedDocument->update([
                    'estado_sunat' => 'RECHAZADO',
                    'respuesta_sunat' => json_encode($result['error'])
                ]);
                
                return [
                    'success' => false,
                    'processing' => false,
                    'document' => $voidedDocument->fresh(),
                    'error' => $result['error']
                ];
            }
        }
        
    } catch (Exception $e) {
        return [
            'success' => false,
            'error' => $e->getMessage()
        ];
    }
}

protected function updateVoidedDocuments(VoidedDocument $voidedDocument): void
{
    foreach ($voidedDocument->detalles as $detalle) {
        $tipoDoc = $detalle['tipo_documento'];
        $serie = $detalle['serie'];
        $correlativo = $detalle['correlativo'];
        
        // Find and update the original document
        $model = match($tipoDoc) {
            '01' => Invoice::class,
            '03' => Boleta::class,
            '07' => CreditNote::class,
            '08' => DebitNote::class,
            default => null
        };
        
        if ($model) {
            $model::where('serie', $serie)
                ->where('correlativo', $correlativo)
                ->update([
                    'estado_sunat' => 'ANULADO',
                    'voided_document_id' => $voidedDocument->id,
                    'fecha_anulacion' => now()
                ]);
        }
    }
}

API Endpoints

use App\Http\Controllers\VoidedDocumentController;

Route::middleware(['auth:sanctum'])->group(function () {
    // Create voided document
    Route::post('/voided-documents', [VoidedDocumentController::class, 'store']);
    
    // Send to SUNAT
    Route::post('/voided-documents/{voidedDocument}/send', 
        [VoidedDocumentController::class, 'send']);
    
    // Check status
    Route::get('/voided-documents/{voidedDocument}/status', 
        [VoidedDocumentController::class, 'checkStatus']);
    
    // List voided documents
    Route::get('/voided-documents', [VoidedDocumentController::class, 'index']);
    
    // Get details
    Route::get('/voided-documents/{voidedDocument}', 
        [VoidedDocumentController::class, 'show']);
});

Best Practices

  1. Act quickly - Void invoices same day if possible
  2. Monitor deadlines - Track 7-day limit for boletas
  3. Batch voiding - Group multiple documents in one voided document
  4. Off-peak hours - Send during low-traffic times
  5. Alert system - Notify when approaching deadlines
  1. Clear reasons - Use specific, professional motivos
  2. Internal tracking - Keep detailed records beyond SUNAT
  3. Audit trail - Log all voiding activities
  4. Communication - Notify clients of annulments
  5. Financial records - Update accounting systems
  1. Validate before sending - Check document status
  2. Verify dates - Ensure fecha_referencia matches
  3. Check permissions - Verify user authorization
  4. Prevent duplicates - Check if already voided
  5. Test thoroughly - Use Beta environment
  1. Async processing - Handle ticket-based workflow
  2. Polling strategy - Check status periodically
  3. Update cascades - Update all related documents
  4. Status consistency - Maintain data integrity
  5. Reconciliation - Verify voiding completed successfully

Monitoring and Reporting

-- Voided documents in last 30 days
SELECT 
    vd.id,
    vd.correlativo,
    vd.fecha_referencia,
    vd.fecha_emision,
    vd.estado_sunat,
    JSON_LENGTH(vd.detalles) as docs_voided,
    c.razon_social as company
FROM voided_documents vd
JOIN companies c ON c.id = vd.company_id
WHERE vd.fecha_emision >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
ORDER BY vd.fecha_emision DESC;

-- Documents voided by type
SELECT 
    JSON_UNQUOTE(JSON_EXTRACT(detalle.value, '$.tipo_documento')) as tipo_doc,
    COUNT(*) as total_voided,
    DATE(vd.fecha_emision) as fecha
FROM voided_documents vd
CROSS JOIN JSON_TABLE(
    vd.detalles, 
    '$[*]' COLUMNS(
        value JSON PATH '$'
    )
) as detalle
WHERE vd.estado_sunat = 'ACEPTADO'
  AND vd.fecha_emision >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
GROUP BY tipo_doc, DATE(vd.fecha_emision)
ORDER BY fecha DESC, tipo_doc;

Common Errors

Error CodeDescriptionSolution
2324Invalid RUCVerify company RUC
2800Invalid schemaValidate XML structure
2801Invalid signatureCheck certificate
2117Date not allowedCheck voiding deadline
2118Document already voidedCannot void twice
2119Document referenced in noteCannot void document with notes
2335Document doesn’t existVerify document was sent to SUNAT

Next Steps

Daily Summaries

Learn about sending boletas in batches

CDR Handling

Process SUNAT responses

SUNAT Integration

Understand SUNAT communication

XML Signing

Digital signature implementation

Build docs developers (and LLMs) love