Skip to main content

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

Certificate Format

SUNAT accepts certificates in PEM format containing:
  1. Private Key (RSA or PKCS#8)
  2. Public Certificate (X.509)
  3. Optional: Certificate chain
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7VJTUt9Us8cKj
... (private key content)
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKL0UG+mRKuLMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
... (certificate content)
-----END CERTIFICATE-----

Certificate Configuration

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:
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:
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:
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)

Hash Extraction

The document hash is extracted from the signed XML for storage:
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)

CodeDescriptionIGV Rate
10Taxable - Domestic sale18%
11Taxable - Free transfer18% (calculated but not charged)
12Taxable - Withdrawal18% (calculated but not charged)
13Taxable - Free samples18% (calculated but not charged)
14Taxable - Bonuses18% (calculated but not charged)
15Taxable - Free services18% (calculated but not charged)
16Taxable - Other free18% (calculated but not charged)
17Taxable - IVAP (rice)4%
CodeDescriptionIGV Rate
20Exonerated0%
30Unaffected0%
31Unaffected - Free transfer0%
32Unaffected - Withdrawal0%
33Unaffected - Free samples0%
34Unaffected - Bonuses0%
35Unaffected - Free services0%
36Unaffected - Other free0%
40Export0%

Validation Checklist

1

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
2

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
3

Signature Elements

  • Signature ID must be “SignSUNAT”
  • SignedInfo must use canonical form
  • DigestValue must be base64 encoded
  • SignatureValue must be base64 encoded
4

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:
ErrorCauseSolution
2800Invalid schemaValidate against UBL 2.1 XSD
2801Invalid signatureCheck certificate and signing process
2802Certificate expiredRenew digital certificate
2803Invalid RUC in certificateCertificate RUC must match company RUC
3000Malformed XMLValidate XML structure
3100Missing required fieldCheck 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

Build docs developers (and LLMs) love