Overview
All electronic documents sent to SUNAT must be digitally signed using an X.509 certificate. The signature ensures document authenticity and integrity according to UBL 2.1 (Universal Business Language) and XMLDSig standards.
Digital Certificate Requirements
SUNAT accepts certificates in PEM format containing:
Private Key (RSA or PKCS#8)
Public Certificate (X.509)
Optional: Certificate chain
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7VJTUt9Us8cKj
... (private key content)
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKL0UG+mRKuLMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
... (certificate content)
-----END CERTIFICATE-----
Certificate Configuration
GreenterService.php:59-78
try {
$certificadoPath = storage_path ( 'app/public/certificado/certificado.pem' );
if ( ! file_exists ( $certificadoPath )) {
throw new Exception ( "Certificate file not found: " . $certificadoPath );
}
$certificadoContent = file_get_contents ( $certificadoPath );
if ( $certificadoContent === false ) {
throw new Exception ( "Could not read certificate file" );
}
$see -> setCertificate ( $certificadoContent );
Log :: info ( "Certificate loaded from file: " . $certificadoPath );
} catch ( Exception $e ) {
Log :: error ( "Error configuring certificate: " . $e -> getMessage ());
throw new Exception ( "Error configuring certificate: " . $e -> getMessage ());
}
Store certificates in storage/app/public/certificado/certificado.pem with permissions 600 or 640 to prevent unauthorized access.
Certificate Validation
The system validates certificate structure before use:
GreenterService.php:674-692
protected function isValidPemStructure ( string $pem ) : bool
{
// Must have private key and certificate (in any order)
$hasPrivateKey = strpos ( $pem , '-----BEGIN PRIVATE KEY-----' ) !== false &&
strpos ( $pem , '-----END PRIVATE KEY-----' ) !== false ;
$hasCertificate = strpos ( $pem , '-----BEGIN CERTIFICATE-----' ) !== false &&
strpos ( $pem , '-----END CERTIFICATE-----' ) !== false ;
// Can also be RSA PRIVATE KEY
$hasRsaPrivateKey = strpos ( $pem , '-----BEGIN RSA PRIVATE KEY-----' ) !== false &&
strpos ( $pem , '-----END RSA PRIVATE KEY-----' ) !== false ;
$hasValidPrivateKey = $hasPrivateKey || $hasRsaPrivateKey ;
Log :: info ( "Validating PEM structure: Certificate={ $hasCertificate }, PrivateKey={ $hasValidPrivateKey }" );
return $hasValidPrivateKey && $hasCertificate ;
}
Certificate Normalization
The system automatically normalizes certificates to ensure compatibility:
GreenterService.php:714-748
protected function reconstructPemCertificate ( string $pem ) : string
{
$output = [];
// Clean content by removing Bag Attributes and other non-essential lines
$cleanedPem = $this -> removeBagAttributes ( $pem );
// Extract private key (PRIVATE KEY or RSA PRIVATE KEY)
$privateKeyExtracted = false ;
if ( preg_match ( '/-----BEGIN PRIVATE KEY-----(. * ?)-----END PRIVATE KEY-----/s' , $cleanedPem , $matches )) {
$privateKey = preg_replace ( '/ \s + /' , '' , $matches [ 1 ]);
$output [] = "-----BEGIN PRIVATE KEY-----" ;
$output [] = chunk_split ( $privateKey , 64 , " \n " );
$output [] = "-----END PRIVATE KEY-----" ;
$privateKeyExtracted = true ;
} elseif ( preg_match ( '/-----BEGIN RSA PRIVATE KEY-----(. * ?)-----END RSA PRIVATE KEY-----/s' , $cleanedPem , $matches )) {
$privateKey = preg_replace ( '/ \s + /' , '' , $matches [ 1 ]);
$output [] = "-----BEGIN RSA PRIVATE KEY-----" ;
$output [] = chunk_split ( $privateKey , 64 , " \n " );
$output [] = "-----END RSA PRIVATE KEY-----" ;
$privateKeyExtracted = true ;
}
// Extract certificate
if ( preg_match ( '/-----BEGIN CERTIFICATE-----(. * ?)-----END CERTIFICATE-----/s' , $cleanedPem , $matches )) {
$certificate = preg_replace ( '/ \s + /' , '' , $matches [ 1 ]);
$output [] = "-----BEGIN CERTIFICATE-----" ;
$output [] = chunk_split ( $certificate , 64 , " \n " );
$output [] = "-----END CERTIFICATE-----" ;
}
Log :: info ( "PEM reconstructed: PrivateKey={ $privateKeyExtracted }, blocks=" . count ( $output ));
return implode ( " \n " , $output );
}
UBL 2.1 XML Structure
Invoice XML Example
SUNAT uses UBL 2.1 (Universal Business Language) format:
<? xml version = "1.0" encoding = "UTF-8" ?>
< Invoice xmlns = "urn:oasis:names:specification:ubl:schema:xsd:Invoice-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#"
xmlns:ext = "urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2" >
<!-- Signature Extension -->
< ext:UBLExtensions >
< ext:UBLExtension >
< ext:ExtensionContent >
< ds:Signature Id = "SignSUNAT" >
<!-- Digital signature elements -->
</ ds:Signature >
</ ext:ExtensionContent >
</ ext:UBLExtension >
</ ext:UBLExtensions >
<!-- Basic Information -->
< cbc:UBLVersionID > 2.1 </ cbc:UBLVersionID >
< cbc:CustomizationID > 2.0 </ cbc:CustomizationID >
< cbc:ID > F001-00000001 </ cbc:ID >
< cbc:IssueDate > 2025-09-02 </ cbc:IssueDate >
< cbc:IssueTime > 10:30:00 </ cbc:IssueTime >
< cbc:InvoiceTypeCode listID = "0101" > 01 </ cbc:InvoiceTypeCode >
< cbc:DocumentCurrencyCode > PEN </ cbc:DocumentCurrencyCode >
<!-- Digital Signature Reference -->
< cac:Signature >
< cbc:ID > IDSignSUNAT </ cbc:ID >
< cac:SignatoryParty >
< cac:PartyIdentification >
< cbc:ID > 20123456789 </ cbc:ID >
</ cac:PartyIdentification >
< cac:PartyName >
< cbc:Name > EMPRESA DEMO SAC </ cbc:Name >
</ cac:PartyName >
</ cac:SignatoryParty >
< cac:DigitalSignatureAttachment >
< cac:ExternalReference >
< cbc:URI > #SignSUNAT </ cbc:URI >
</ cac:ExternalReference >
</ cac:DigitalSignatureAttachment >
</ cac:Signature >
<!-- Supplier (Emisor) -->
< cac:AccountingSupplierParty >
< cac:Party >
< cac:PartyIdentification >
< cbc:ID schemeID = "6" > 20123456789 </ cbc:ID >
</ cac:PartyIdentification >
< cac:PartyLegalEntity >
< cbc:RegistrationName > EMPRESA DEMO SAC </ cbc:RegistrationName >
< cac:RegistrationAddress >
< cbc:ID > 150101 </ cbc:ID >
< cbc:AddressTypeCode > 0000 </ cbc:AddressTypeCode >
< cbc:CityName > LIMA </ cbc:CityName >
< cbc:CountrySubentity > LIMA </ cbc:CountrySubentity >
< cbc:District > LIMA </ cbc:District >
< cac:AddressLine >
< cbc:Line > AV. LIMA 123 </ cbc:Line >
</ cac:AddressLine >
< cac:Country >
< cbc:IdentificationCode > PE </ cbc:IdentificationCode >
</ cac:Country >
</ cac:RegistrationAddress >
</ cac:PartyLegalEntity >
</ cac:Party >
</ cac:AccountingSupplierParty >
<!-- Customer (Cliente) -->
< cac:AccountingCustomerParty >
< cac:Party >
< cac:PartyIdentification >
< cbc:ID schemeID = "6" > 20987654321 </ cbc:ID >
</ cac:PartyIdentification >
< cac:PartyLegalEntity >
< cbc:RegistrationName > CLIENTE DEMO SAC </ cbc:RegistrationName >
</ cac:PartyLegalEntity >
</ cac:Party >
</ cac:AccountingCustomerParty >
<!-- Tax Totals -->
< cac:TaxTotal >
< cbc:TaxAmount currencyID = "PEN" > 180.00 </ cbc:TaxAmount >
< cac:TaxSubtotal >
< cbc:TaxableAmount currencyID = "PEN" > 1000.00 </ cbc:TaxableAmount >
< cbc:TaxAmount currencyID = "PEN" > 180.00 </ cbc:TaxAmount >
< cac:TaxCategory >
< cac:TaxScheme >
< cbc:ID > 1000 </ cbc:ID >
< cbc:Name > IGV </ cbc:Name >
< cbc:TaxTypeCode > VAT </ cbc:TaxTypeCode >
</ cac:TaxScheme >
</ cac:TaxCategory >
</ cac:TaxSubtotal >
</ cac:TaxTotal >
<!-- Legal Monetary Total -->
< cac:LegalMonetaryTotal >
< cbc:LineExtensionAmount currencyID = "PEN" > 1000.00 </ cbc:LineExtensionAmount >
< cbc:TaxInclusiveAmount currencyID = "PEN" > 1180.00 </ cbc:TaxInclusiveAmount >
< cbc:PayableAmount currencyID = "PEN" > 1180.00 </ cbc:PayableAmount >
</ cac:LegalMonetaryTotal >
<!-- Invoice Lines -->
< cac:InvoiceLine >
< cbc:ID > 1 </ cbc:ID >
< cbc:InvoicedQuantity unitCode = "NIU" > 10 </ cbc:InvoicedQuantity >
< cbc:LineExtensionAmount currencyID = "PEN" > 1000.00 </ cbc:LineExtensionAmount >
< cac:PricingReference >
< cac:AlternativeConditionPrice >
< cbc:PriceAmount currencyID = "PEN" > 118.00 </ cbc:PriceAmount >
< cbc:PriceTypeCode > 01 </ cbc:PriceTypeCode >
</ cac:AlternativeConditionPrice >
</ cac:PricingReference >
< cac:TaxTotal >
< cbc:TaxAmount currencyID = "PEN" > 180.00 </ cbc:TaxAmount >
< cac:TaxSubtotal >
< cbc:TaxableAmount currencyID = "PEN" > 1000.00 </ cbc:TaxableAmount >
< cbc:TaxAmount currencyID = "PEN" > 180.00 </ cbc:TaxAmount >
< cac:TaxCategory >
< cbc:Percent > 18.00 </ cbc:Percent >
< cbc:TaxExemptionReasonCode > 10 </ cbc:TaxExemptionReasonCode >
< cac:TaxScheme >
< cbc:ID > 1000 </ cbc:ID >
< cbc:Name > IGV </ cbc:Name >
< cbc:TaxTypeCode > VAT </ cbc:TaxTypeCode >
</ cac:TaxScheme >
</ cac:TaxCategory >
</ cac:TaxSubtotal >
</ cac:TaxTotal >
< cac:Item >
< cbc:Description > PRODUCTO DEMO </ cbc:Description >
< cac:SellersItemIdentification >
< cbc:ID > P001 </ cbc:ID >
</ cac:SellersItemIdentification >
</ cac:Item >
< cac:Price >
< cbc:PriceAmount currencyID = "PEN" > 100.00 </ cbc:PriceAmount >
</ cac:Price >
</ cac:InvoiceLine >
</ Invoice >
XML Generation
Greenter automatically generates UBL 2.1 compliant XML:
GreenterService.php:203-356
public function createInvoice ( array $invoiceData ) : GreenterInvoice
{
$invoice = new GreenterInvoice ();
// Basic configuration
$invoice -> setUblVersion ( $invoiceData [ 'ubl_version' ] ?? '2.1' )
-> setTipoOperacion ( $invoiceData [ 'tipo_operacion' ] ?? '0101' )
-> setTipoDoc ( $invoiceData [ 'tipo_documento' ])
-> setSerie ( $invoiceData [ 'serie' ])
-> setCorrelativo ( $invoiceData [ 'correlativo' ])
-> setFechaEmision ( new \DateTime ( $invoiceData [ 'fecha_emision' ]))
-> setTipoMoneda ( $invoiceData [ 'moneda' ] ?? 'PEN' );
// Payment terms
$formaPago = $this -> getFormaPago ( $invoiceData );
$invoice -> setFormaPago ( $formaPago );
// Company and client
$invoice -> setCompany ( $this -> getGreenterCompany ())
-> setClient ( $this -> getGreenterClient ( $invoiceData [ 'client' ]));
// Amounts - Special handling for exports
$tipoOperacion = $invoiceData [ 'tipo_operacion' ] ?? '0101' ;
if ( $tipoOperacion === '0200' ) {
// Export - use setMtoOperExportacion and DO NOT set other amounts
$invoice -> setMtoOperExportacion ( $invoiceData [ 'valor_venta' ])
-> setMtoIGV ( 0 )
-> setMtoISC ( $invoiceData [ 'mto_isc' ] ?? 0 )
-> setMtoOtrosTributos ( $invoiceData [ 'mto_otros_tributos' ] ?? 0 )
-> setTotalImpuestos ( $invoiceData [ 'mto_isc' ] ?? 0 )
-> setValorVenta ( $invoiceData [ 'valor_venta' ])
-> setSubTotal ( $invoiceData [ 'valor_venta' ])
-> setMtoImpVenta ( $invoiceData [ 'valor_venta' ]);
} else {
// Normal operations
$invoice -> setMtoOperGravadas ( $invoiceData [ 'mto_oper_gravadas' ])
-> setMtoOperExoneradas ( $invoiceData [ 'mto_oper_exoneradas' ])
-> setMtoOperInafectas ( $invoiceData [ 'mto_oper_inafectas' ])
-> setMtoOperGratuitas ( $invoiceData [ 'mto_oper_gratuitas' ])
-> setMtoIGVGratuitas ( $invoiceData [ 'mto_igv_gratuitas' ] ?? 0 )
-> setMtoIGV ( $invoiceData [ 'mto_igv' ])
-> setMtoISC ( $invoiceData [ 'mto_isc' ] ?? 0 )
-> setMtoOtrosTributos ( $invoiceData [ 'mto_otros_tributos' ] ?? 0 )
-> setTotalImpuestos ( $invoiceData [ 'total_impuestos' ])
-> setValorVenta ( $invoiceData [ 'valor_venta' ])
-> setSubTotal ( $invoiceData [ 'sub_total' ])
-> setMtoImpVenta ( $invoiceData [ 'mto_imp_venta' ]);
}
// Details
$details = $this -> createSaleDetails ( $invoiceData [ 'detalles' ]);
$invoice -> setDetails ( $details );
// Legends
if ( isset ( $invoiceData [ 'leyendas' ]) && ! empty ( $invoiceData [ 'leyendas' ])) {
$legends = $this -> createLegends ( $invoiceData [ 'leyendas' ]);
$invoice -> setLegends ( $legends );
}
return $invoice ;
}
XMLDSig Signature Structure
The digital signature follows XML Signature (XMLDSig) standards:
< ds:Signature Id = "SignSUNAT" xmlns:ds = "http://www.w3.org/2000/09/xmldsig#" >
< ds:SignedInfo >
< ds:CanonicalizationMethod Algorithm = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
< ds:SignatureMethod Algorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
< ds:Reference URI = "" >
< ds:Transforms >
< ds:Transform Algorithm = "http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
</ ds:Transforms >
< ds:DigestMethod Algorithm = "http://www.w3.org/2000/09/xmldsig#sha1" />
< ds:DigestValue > BASE64_ENCODED_HASH </ ds:DigestValue >
</ ds:Reference >
</ ds:SignedInfo >
< ds:SignatureValue > BASE64_ENCODED_SIGNATURE </ ds:SignatureValue >
< ds:KeyInfo >
< ds:X509Data >
< ds:X509Certificate > BASE64_ENCODED_CERTIFICATE </ ds:X509Certificate >
</ ds:X509Data >
</ ds:KeyInfo >
</ ds:Signature >
Signature Algorithms
Canonicalization:
C14N (Canonical XML 1.0): http://www.w3.org/TR/2001/REC-xml-c14n-20010315
Signature:
RSA with SHA-1: http://www.w3.org/2000/09/xmldsig#rsa-sha1
RSA with SHA-256: http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 (recommended)
Digest:
SHA-1: http://www.w3.org/2000/09/xmldsig#sha1
SHA-256: http://www.w3.org/2001/04/xmlenc#sha256 (recommended)
The document hash is extracted from the signed XML for storage:
DocumentService.php:640-645
protected function extractHashFromXml ( string $xml ) : ? string
{
// Extract hash from signed XML
preg_match ( '/<ds:DigestValue[^>] * >([^<] + )< \/ ds:DigestValue>/' , $xml , $matches );
return $matches [ 1 ] ?? null ;
}
The hash is stored in the database for:
QR code generation
Document verification
Audit trails
UBL Tax Codes
IGV Affectation Codes (tip_afe_igv)
Code Description IGV Rate 10 Taxable - Domestic sale 18% 11 Taxable - Free transfer 18% (calculated but not charged) 12 Taxable - Withdrawal 18% (calculated but not charged) 13 Taxable - Free samples 18% (calculated but not charged) 14 Taxable - Bonuses 18% (calculated but not charged) 15 Taxable - Free services 18% (calculated but not charged) 16 Taxable - Other free 18% (calculated but not charged) 17 Taxable - IVAP (rice) 4%
Code Description IGV Rate 20 Exonerated 0% 30 Unaffected 0% 31 Unaffected - Free transfer 0% 32 Unaffected - Withdrawal 0% 33 Unaffected - Free samples 0% 34 Unaffected - Bonuses 0% 35 Unaffected - Free services 0% 36 Unaffected - Other free 0% 40 Export 0%
Validation Checklist
Certificate Validation
Verify certificate is in PEM format
Check certificate contains both private key and public certificate
Ensure certificate is not expired
Validate certificate is issued by authorized CA
XML Structure
Confirm UBL version is 2.1
Verify all required namespaces are declared
Check document type code matches series
Validate all amounts have 2 decimal places
Signature Elements
Signature ID must be “SignSUNAT”
SignedInfo must use canonical form
DigestValue must be base64 encoded
SignatureValue must be base64 encoded
Tax Calculations
Verify IGV is 18% of taxable base
Check IVAP is 4% for rice products
Validate ISC calculations if applicable
Confirm ICBPER is S/0.50 per unit
Common XML Errors
These errors will cause SUNAT to reject your documents:
Error Cause Solution 2800 Invalid schema Validate against UBL 2.1 XSD 2801 Invalid signature Check certificate and signing process 2802 Certificate expired Renew digital certificate 2803 Invalid RUC in certificate Certificate RUC must match company RUC 3000 Malformed XML Validate XML structure 3100 Missing required field Check all mandatory UBL elements
Best Practices
Certificate Security
Store certificates encrypted
Use environment variables for passwords
Rotate certificates before expiration
Keep backups in secure location
XML Optimization
Cache Greenter instances
Reuse XML factory
Batch document generation
Compress stored XML files
Validation
Validate locally before sending
Test in Beta environment
Log all signature attempts
Monitor rejection rates
Compliance
Follow SUNAT technical specifications
Keep UBL 2.1 compliant
Store signed XML permanently
Maintain audit logs
Next Steps
SUNAT Integration Learn about endpoints and sending documents
CDR Handling Process and validate SUNAT responses