Comply with SII within your Laravel application.
use Laragear\Dte\Facades\Dte;
$invoice = Dte::invoice()
->receivedBy('76.543.210-K', 'Helados S.A.')
->addItem('Crema de Leche', 12_000)
->create();Tip
Building a Chilean application that manages RUT numbers? Check out Laragear Rut.
Your support allows me to keep this package free, up-to-date, and maintainable. Alternatively, you can spread the word on social media!
- PHP 8.5 or later
- PHP Extensions:
openssl,dom,libxmlandmbstring - Laravel 13.x or later
- Laravel Scheduler and Queue enabled
In 2026, you (still) cannot just connect your application to the SII Servers to push invoices in JSON. SII works using pre-iPhone technologies: XML, SOAP, folio-authorization, digital-signing, and so forth.
To avoid your application being vendor-locked-in with external services, this library handles everything from the document constructions onwards, leaving you with only five tasks ahead:
- Uploading a Folio to authorize your documents
- Create the documents you need (the fun part)
- Read your documents
- Answer incoming ones from other businesses
- Scheduling some tasks to keep a continuous operation
You will require some manual labor due to SII self-imposed limits when you move to production:
- Download CAF and load it into the library.
- Download RCV and load it into the library.
Fire up Composer and require this package in your project.
composer require laragear/dteFirst, publish the configuration file and database migrations:
php artisan vendor:publish --provider="Laragear\Dte\DteServiceProvider"After that, you may migrate your database like always through the Artisan command.
php artisan migrateIf you are new to Chilean tax accounting (SII), the gist is straightforward: documents (DTE) are sent to SII servers in bulk to SOAP/REST endpoints, in XML. To sign, you will need a Digital Certificate (.p12|pfx) you can buy separately.
The implementation is not straightforward. The SII requires your app to comply with seven points:
- Receive a CAF XML that authorizes a DTE number range (folio).
- DTE must use a folio from that CAF XML.
- Send DTE as "envelopes" and track their processing constantly.
- Receive and answer DTE you will receive at your
dte@my-app.cl. - Create a legally correct PDF for your DTEs.
- Send DTE XML to the target business
dte@business.cl. - Store all documents for 6 years.
To deal with this, this library implements the following:
- Automatically loads CAF into the library for the document type.
- Automatically allocates CAF folios for created DTE.
- Automatically fills and sends envelopes, polling their status at SII for updates.
- Automatically reads your
dte@my-app.clvia IMAP, driver or your own custom driver. - Automatically renders PDF for any DTE using a standard design.
- Once envelopes are approved, sends each DTE to the target business using Laravel Mail driver.
- Allows your app to hook up into the
DteAcceptedandEnvelopeAcceptedto save XML payloads on your storage.
Odds are you already know some document types before implementing this library, but if you feel lost, check the glossary section and come back.
You can start to use this library in less than three minutes in your library, or the pizza is free.
By default, this library creates a fake business with all the data required, so there is no need to set your own for development. You can safely skip this step.
On the other hand, if your app retrieves this data dynamically (e.g. from the database), like when the end-user is onboarded into the application, you can use the convenient ConfigurationManager::setCompany() helper to fill the required company data.
use Laragear\Dte\Configuration\ConfigurationManager;
use Laragear\Dte\Certificate\CertificateResolver;
use Laragear\Dte\Data\CompanyData;
use Laragear\Dte\Data\IssuerData;
use Laragear\Dte\Certificate\DigitalCertificate;
use Laragear\Rut\Rut;
public function boot()
{
// Resolve the Company dynamically for a single tenant
ConfigurationManager::setCompany(function () {
$settings = Settings::first();
if (! $settings) {
return;
}
return CompanyData::make(
issuer: IssuerData::make(
rut: $settings->rut,
legalName: $settings->legal_name,
businessActivity: $settings->business_activity,
economicActivity: $settings->economic_activity,
address: $settings->address,
commune: $settings->commune,
city: $settings->city,
resolutionDate: $settings->resolution_date,
resolutionNumber: $settings->resolution_number,
),
// Optional, defaults to the issuer RUT
senderRut: $settings->sender_rut
);
});
// Resolve the digital certificate
CertificateResolver::resolveUsing(function () {
$cert = Certificate::first();
if (! $cert) {
return;
}
return DigitalCertificate($cert->p12, $cert->password);
});
}Note
If you plan to use this library on multi-tenant environments (multiple business), don't skip the quickstart. Once you understand how the library works, proceed to the Multi-Tenancy Configuration section.
For local development, generate a fake certificate and a default CAF for invoices. These will allow you to create DTE without having to buy a real certificate or hit the SII servers.
php artisan dte:make-fake-cert
php artisan dte:make-fake-cafYou can run the dte:make-fake-caf again for Receipts (Code 39).
php artisan dte:make-fake-caf --type=39In certification/production, you will be required to download the real CAF from SII and upload it instead.
Note
If you set a company previously, it will use the company data for these fake files. Alternatively, use --rut with the RUT of the company on both commands so files match.
Schedule these Artisan commands in your routes/console.php. You can read see Artisan Commands Reference for details about what does what, but consider these commands the bare minimum.
use Illuminate\Support\Facades\Schedule;
// Checks for CAFs depletion
Schedule::command('dte:check-cafs')->everyTwoHours();
// Packs signed DTE into envelopes so these are ready to be sent.
Schedule::command('dte:process-envelope')->everyTenMinutes();
// Poll the email for unanswered DTE.
Schedule::command('dte:fetch-mailbox')->hourly();
// Poll the SII for the envelope status (accepted or rejected).
Schedule::command('dte:poll-track-status')->hourly();
// Reject phantom invoices sent to your app before the deadline (recommended).
Schedule::command('dte:reject-phantom-invoices')->twiceDaily();You can now create your own documents using the Dte facade.
use Laragear\Dte\Facades\Dte;
$invoice = Dte::invoice()
->receivedBy('76.543.210-K', 'Helados S.A.')
->addItem('Crema de Leche', 12_000)
->create();The library supports most used SII documents types via dedicated builders, all sharing a fluent API:
| Builder | Type Code | Description |
|---|---|---|
Invoice |
33 / 34 | Electronic invoice / exempt |
Receipt |
39 / 41 | Electronic receipt (boleta) |
CreditNote |
61 | Credit note — references a prior invoice to reverse it |
DebitNote |
56 | Debit note — adjusts amounts on a prior document |
DispatchGuide |
52 | Dispatch guide (guía de despacho) |
InvoiceLiquidation |
43 | Invoice liquidation |
PurchaseInvoice |
46 | Purchase invoice (factura de compra) |
AecBuilder |
— | AEC (Acuse Electrónico de Cargo), factoring/cession |
Important
The library is missing some documents, and this is by scope. Additional support will be based on sponsorship number.
Caution
Never create a document using the Eloquent Model directly except in testing environments, otherwise you risk legal data corruption. Always use the builder.
Every builder is reached through the Dte facade (or the underlying Builder instance). The builder methods are fluent; each call returns the builder, so you can freely chain the properties of the document.
You're required to set who receives the DTE using receivedBy() with the RUT and legal name of the person. For businesses (like in invoices), you will require the ReceiverData object.
use Laragear\Dte\Data\ReceiverData;
use Laragear\Dte\Facades\Dte;
$receiver = ReceiverData::make(
rut: '76.123.456-0',
legalName: 'Ferretería Pérez Ltda.',
businessActivity: 'Compra venta de artículos de construcción',
email: 'compras@feperez.cl',
address: 'Avenida Principal 48',
commune: 'Osorno',
city: 'Osorno',
);
$invoice = Dte::invoice()
->receivedBy($receiver);Chances are that you will have an Eloquent Model that you will want to use as a receiver. In that case, use Receivable contract on the model you want, and it will be used as a receiver magically.
use Illuminate\Database\Eloquent\Model;
use Laragear\Dte\Contracts\Receivable;
use Laragear\Dte\Data\ReceiverData;
class Business extends Model implements Receivable
{
// ...
public function toReceiver(): ReceiverData
{
return ReceiverData::make(
// ...
);
}
}
use Laragear\Dte\Facades\Dte;
$invoice = Dte::invoice()
->receivedBy(Business::find(66));Once your document is ready, use the ->create() method to persist the DTE to the database. It returns a SiiDte model you can use to inspect its status later using the primary key.
Meanwhile, a queued job will be dispatched to compile the document XML, sign it, and be ready to be sent to SII through an envelope, where other similar DTE will be inserted. If you need the XML ready immediately (like for Receipts), especially for printing a PDF, use the sync argument with a value that evaluates to true or false, or a callback.
use Laragear\Dte\Facades\Dte;
$receipt = Dte::receipt()
->addItem('Queso Ranco', 7_490)
->create(sync: fn() => true);
return $receipt->pdf()->generate();Important
When doing building the XL in sync, your app will take time to properly sign the XML, which also involves folio reservation. This shouldn't take more than a few seconds. The DTE will still be queued to be sent later through an envelope.
You can add an item using a name and a total through the addItem() method.
use Laragear\Dte\Facades\Dte;
$invoice = Dte::invoice()
->receivedBy('76.543.210-K', 'Helados S.A.')
->addItem('Crema de Leche', 12_000)
->create();Use addItem($name, $amount, isExempt: true) for lines that carry no tax (IVA). Exempt items still count toward the document total but are excluded from the default 19% tax calculation.
use Laragear\Dte\Facades\Dte;
$invoice = Dte::invoice()
->receivedBy('76.543.210-K', 'Helados S.A.')
->addItem('Crema de Leche', 12_000, isExempt: true)
->create();If you require more control on the itemization, like quantity, description, special codes, or percentage discounts, use the Item object directly.
use Laragear\Dte\Data\Item;
use Laragear\Dte\Facades\Dte;
$item = Item::make(
name: 'Crema de Leche',
unitPrice: 1_200,
quantity: 10,
discountPercentage: 0.15,
taxes: [15 => 1_900] // Automatically subtracts Retention Code 15
);
$invoice = Dte::invoice()
->receivedBy('76.543.210-K', 'Helados S.A.')
->addItem($item)
->create();The taxes parameter accepts a dictionary of SII tax and retention codes assigned to their absolute amounts. Retentions (like 'IVA Retenido', codes 14-19, 30+) are automatically subtracted from the document total, while Ad-Valorem additions natively evaluate positively.
You can define commercial discounts and surcharges using the globalDiscount() or globalSurcharge() methods on the document builder before creation.
To dictate which accounting block the modifier mathematically targets, pass a ModifierTarget enum (Net, Exempt, or NonTaxable).
use Laragear\Dte\Enums\ModifierTarget;
use Laragear\Dte\Facades\Dte;
$invoice = Dte::invoice()
->receivedBy('76.543.210-K', 'Helados S.A.')
->addItem('Crema de Leche', 12_000)
// Applies a 10% global discount mathematically targeting the Net amount
->globalDiscount(10, isPercent: true, target: ModifierTarget::Net, description: 'Descuento Global Primavera')
->create();References link DTE with other documents, like your own purchase orders or other DTE invoices. You need to get this clear from the get-go:
- Purchase Orders (
801) & Contracts (803): The$folioparameter is the internal application ID or commercial document number from your own or your customer's system (e.g.'PO-2026-99','CONT-001'). Multiple references are supported. - Previous Invoices / DTEs (
33,34,52, etc.): The$folioparameter IS the official SII DTE Folio number of the previously issued tax document being referenced, amended, discounted, or nullified. Only a single reference is supported.
Warning
Reason has a 90-character limit, so don't put entire biographies there.
Link documents to customer Purchase Orders or commercial contracts:
use Laragear\Dte\Data\ReferenceData;
use Laragear\Dte\Enums\ReferenceType;
use Laragear\Dte\Facades\Dte;
$reference = ReferenceData::make(
documentType: ReferenceType::PurchaseOrder,
folio: '100000513',
date: '2026-04-01', // Our purchase order date
reason: 'Requiere artículo usado para mostrar helados'
);
$invoice = Dte::purchaseInvoice()
->receivedBy('18685226-5', 'Adrián Roberto Pérez González')
->addItem('Vitrina', 140_000)
->addReference($reference)
->create();Warning
Folio IDs have an 18-character limit. If you're using UUID, ULID, or something larger, create a numeric/alphanumeric "Tracking ID" for your documents to link these instead.
When creating Credit Notes (61) or Debit Notes (56) to modify previously issued invoices, use the explicit correction methods for nullifying (canceling), amending (correcting text), or charging/discounting (modifying amounts).
Tip
As a rule of thumb:
- If you made a critical typo on the document, amend with a Credit Note.
- If the customer needs to pay less, use a Credit Note.
- If the customer needs to pay more, use a Debit Note.
- If a document needs to disappear completely, use a Credit Note, unless the document you are making disappear is already a Credit Note.
For convenience, issue the SiiDte instance you want to alter directly and fill the remaining data.
use Laragear\Dte\Data\Item;
use Laragear\Dte\Enums\DteType;
use Laragear\Dte\Facades\Dte;
use Laragear\Dte\Models\SiiDte;
// Cancel a previous invoice (Reason Code 1: Anula Documento de Referencia)
$creditNote = Dte::creditNote()
->receivedBy('76.482.465-2', 'Cliente SpA')
->annul(SiiDte::invoices()->find(56), reason: 'Mercancía perdida en el camino')
->create();
// Cancel a previous invoice (Reason Code 2: Corrige text)
$creditNote = Dte::creditNote()
->receivedBy('76.482.465-2', 'Cliente SpA')
// Add the correction as an Item (per SII instructions).
->addItem(new Item('Corrección', 0, description: 'Debería ser "Cliente SpA".'))
->amend(SiiDte::invoices()->find(56), reason: 'Corrección de Razón social')
->create();AEC cessions are meant for transferring a document's receivable to a third party. The most common use is for invoices to be paid later (30/60/90 days): transfer the invoice to a factoring business, receive part of the money now, and the other business receives the full amount later.
While you can use the Dte::aec() method to create one manually, the best course of action is to find the invoice you want to cede and use the cede() method to fluently build and send the cession document.
use Laragear\Dte\Models\SiiDte;
use Laragear\Dte\Facades\Dte;
use Laragear\Dte\Certificate\DigitalCertificate;
use Laragear\Dte\Data\CessionData;
use Laragear\Rut\Rut;
// The invoice to cede. Must be an invoice.
$invoice = SiiDte::invoices()->find(1);
$cession = $invoice->cede()
->to(rut: '76.543.210-K', name: 'Factoring Bank S.A.')
->address('Av. El Golf 123, Las Condes', 'cesiones@factoringbank.cl')
->authorizedBy(rut: '12.345.678-9', name: 'Juan Perez', email: 'contact@mycompany.cl')
->amount($invoice->amount_total) // Optional, defaults to total, fails if it exceeds total.
->dueDate('2026-12-31') // When the cession is due
->create();As with SII DTE, a cession document gets queued for building and sending to the SII in bulk through a DTE Envelope.
All documents persisted are just SiiDte models waiting to be sent to SII in XML. The latter is handled async by your application queue.
Since SiiDte is an Eloquent Model, you can query freely as with any other model. As a single model manages multiple types, you can filter the types using their respective local scope:
| Method | Document Type |
|---|---|
invoices() |
Electronic invoice |
exemptInvoices() |
Electronic exempt invoice |
receipts() |
Electronic receipt (boleta) |
creditNotes() |
Credit note — references a prior invoice to reverse it |
debitNotes() |
Debit note — adjusts amounts on a prior document |
dispatchGuides() |
Dispatch guide (guía de despacho) |
invoiceLiquidations() |
Invoice liquidation |
purchaseInvoices() |
Purchase invoice (factura de compra) |
The only recommended action to do over models is to check their status through the withStatus() local scope, or the $status attribute. Statuses flow through DteStatus enum (pending → sent → accepted/rejected).
use Laragear\Dte\Enums\DteStatus;
use Laragear\Dte\Models\SiiDte;
// Using a Local Scope
SiiDte::invoices()->whereStatus(DteStatus::Accepted)->first();
// Using the `status` attribute.
if (SiiDte::find(1)->status === DteStatus::Accepted) {
return 'The document was accepted, all good with the SII.';
}In any case, terminal statuses cannot be transitioned from. Attempting to do so will throw a nasty exception.
When the library polls the SII for the processing status of your DTE Envelopes—transparently using either the legacy SOAP API for Invoices or the modern REST API for Boletas—it correctly separates structural errors from business logic errors:
- Envelope-level Rejections (Structural): If the envelope fails structurally (e.g., bad signature or schema), the DTEs inside were not processed. The library will automatically detach them and schedule them to be repacked into a new envelope (up to
max_retriesconfigured inconfig/dte.php), saving valid folios from being burned. - DTE-level Rejections (Business): If the envelope was processed properly but specific DTEs contain bad data (e.g., mismatched receiver RUT), the SII permanently rejects them. The library updates the model to
Rejectedand fires theDteRejectedevent.
Since a DTE with a business error will keep failing if automatically retried, you must resolve the data yourself. You can catch the event, correct the data on your end, and try again using $dte->replicateForRetry(), which safely clones the payload and readies the document to acquire a fresh folio.
use Laragear\Dte\Events\DteRejected;
public function handle(DteRejected $event)
{
// Fix the underlying problem...
// $event->dte->payload...
// Safely replicate the document to be processed and sent with a new folio
$newDte = $event->dte->replicateForRetry();
}Similar to Envelopes, individual DTEs can also be accepted but contain repairs (e.g. DOK with <GLOSA> discrepancies). The SiiDte model shares the same helpers and scopes as Envelopes:
use Laragear\Dte\Models\SiiDte;
if ($dte->isAcceptedWithRepairs()) {
$repairs = $dte->repairs;
$rawResponse = $dte->payload->sii_response;
}
$dte->isNotAcceptedWithRepairs();
SiiDte::whereHasRepairs()->get();
SiiDte::whereDoesntHaveRepairs()->get();The friendly parsed comments are available in the $dte->repairs array, and the raw XML/JSON response is safely stored in $dte->payload->sii_response.
If a folio was skipped, damaged, or cannot be used, you must report it to the SII to keep your records aligned. You can use the FolioAnnulmentService to report these folios directly to the SII:
use Laragear\Dte\Services\FolioAnnulmentService;
use Laragear\Dte\Models\SiiDte;
$service = app(FolioAnnulmentService::class);
// Report a damaged or annulled DTE folio
$service->annul(SiiDte::find(1));While Facturas are grouped into a signed DTE Envelope and sent to SII in bulk via the standard SOAP API, Boletas (receipts), however, are grouped into a specialized <EnvioBOLETA> envelope and transmitted to the modern SII REST API (api.sii.cl). The library handles both types of endpoints transparently via the same dte:pack-ready command.
Receipts do not require receiver data, these use an "anonymous consumer", but it is recommended when amounts are large in case of SII audits (e.g., CLP$ 200.000 or more).
use Laragear\Dte\Facades\Dte;
// The library will automatically group and pack receipts into an <EnvioBOLETA> envelope
$receipt = Dte::receipt()
->addItem('Bebida energética', 3_490)
->create();Receipts are created as models, but their XML payload is queued to be sent later to SII servers.
Your application will require Digital Certificates to operate completely with the SII. On multi-tenant applications, you will be required to resolve these by the RUT of the issuer.
The DigitalCertificate class loads a PKCS#12 (.p12|pfx) file into memory and exposes its public key, private key, and validity dates.
The most direct approach to resolve certificates is just using CertificateResolver::resolveUsing() with a callback that receives the target RUT, and should return a DigitalCertificate instance or null if not found.
For example, you may save certificates in your database, local filesystem, or even cloud storage. You can conveniently instantiate certificates from strings or files using the Certificate facade, and register a custom resolver callback to locate them:
use Illuminate\Support\Facades\Storage;
use Laragear\Dte\Certificate\CertificateResolver;
use Laragear\Dte\Certificate\DigitalCertificate;
use Laragear\Rut\Rut;
CertificateResolver::resolveUsing(function (Rut $rut) {
$company = Company::where('rut', $rut->formatRaw())->first();
return $company ? new DigitalCertificate(
Storage::disk($company->cert_disk)->get($company->cert_path),
$company->cert_password,
) : null;
});For more advanced scenarios, you can create your own CertificateResolverInterface that resolves by RUT, and instances certificates from a file location or a string. Once done, bind it to the Service Container.
$this->app->bind(
CertificateResolverInterface::class,
MyCertificateResolver::class
);SII CAF documents authorize your DTE to be issued to SII with a controlled Folio numbering. SII forces you to download this document manually and load it into this library through your app.
Note
You may see third party web applications automatizing this. Don't be fooled, there is no SII API to request CAF (in 2026!), these apps use headless browser automation to navigate to the SII and download it (and charge for the privilege).
In production, you will be required to download the CAF XML from SII website, and load it into the library through the Caf facade.
The library automatically validates the CAF's folio range, expiration date, and issuer RUT before accepting it.
use Laragear\Dte\Facades\Caf;
use Illuminate\Support\Facades\Storage;
// If you have the binary XML string:
$caf = Caf::store(Storage::get('76123456-7/33.xml'));
// If you are uploading a CAF file directly via an HTTP Request:
$caf = Caf::storeFile($request->file('caf'));
// Or using an absolute path:
$caf = Caf::storeFile(storage_path('app/cafs/76123456-7/33.xml'));The dte:check-cafs command scans all loaded CAFs and dispatches a CafNearDepleted event when remaining folios drop below the configured threshold. Schedule this command daily so you can request new folio blocks before running out. Depending on your DTE output, you may want to tight the interval to twice a day or even hours.
// In routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('dte:check-cafs')->daily();Envelopes are the core of the library's operations with the SII. The lifecycle of an envelope operates entirely in the background through a series of scheduled commands and queued jobs:
- Pending: When a document is first created and ready to be signed, it starts as
Pending. - Signed: The document is processed, signed with your digital certificate, and its status changes to
Signed. - Packed: A scheduled command (
dte:pack-ready) groups theseSigneddocuments by issuer and packs them into anSiiDteEnvelope. - Sent: For each envelope, a background queued job is dispatched. This job signs the envelope and uploads it to the SII. Once uploaded, the SII returns a Track ID and the envelope is marked as
Uploaded. - Polling: Another scheduled command (
dte:poll-track-status) periodically streams allUploadedenvelopes and dispatches background jobs to query the SII for their processing status. - Resolution: The background workers receive the status from the SII and finally mark the envelope as either
AcceptedorRejected.
This architecture ensures that the application remains fast and responsive, and prevents hammering the SII servers by smoothly throttling requests through your queues.
When the SII processes an envelope, it may return a status of EPR (Envío Procesado), which marks the envelope as Accepted. However, the envelope might be partially accepted—meaning some internal DTEs had discrepancies or "repairs" (Reparos or Rechazos).
To check if an envelope was accepted with repairs, you can use the model helpers:
if ($envelope->isAcceptedWithRepairs()) {
$repairs = $envelope->repairs; // Returns a JSON array of the parsed SII statistics/repairs
$rawResponse = $envelope->payload->sii_response; // Returns the raw XML or JSON from the SII
}
if ($envelope->isNotAcceptedWithRepairs()) {
// 100% clean acceptance
}You can also filter envelopes using the provided local scopes:
use Laragear\Dte\Models\SiiDteEnvelope;
SiiDteEnvelope::whereHasRepairs()->get();
SiiDteEnvelope::whereDoesntHaveRepairs()->get();This library provides a robust Cuadratura Engine that parses official SII Registro de Compras y Ventas CSV exports to securely synchronize and reconcile your database automatically.
You can execute this engine programmatically using the Laragear\Dte\Actions\SyncRcv action, which accepts a file path, string payload, or uploaded file:
use Laragear\Dte\Actions\SyncRcv;
use Laragear\Dte\Enums\RcvType;
public function sync(Request $request, SyncRcv $syncRcv)
{
$stats = $syncRcv->handle(
source: $request->file('rcv_document'),
type: RcvType::Purchases,
issuer: '76.123.456-0'
);
return response()->json($stats);
}The Cuadratura engine safely updates the statuses of your local records:
- gracefully marking missing inbounds as
PhantomPending, - downgrading orphaned sent DTEs to
Rejected, and - updating commercial acceptances.
The engine explicitly avoids blindly mutating sensitive amounts or generating outbound payloads autonomously. When numeric or outbound discrepancies exist, it simply alerts you via Events to maintain business integrity natively.
Depending on what happened, the following are the recommended courses of action:
If a sale exists in your system but not in the SII's RCV, it means the SII either never received the envelope or rejected it.
- Solution: You must check the Track ID (
track_id) status of that envelope. If it was rejected by the SII, you must fix the errors and re-emit/re-send the DTE. If it was never sent, you must send it.
If the sale appears in both your system and the RCV but the amounts, dates, or client data are wrong, you cannot just "edit" the RCV record.
- Solution: You must emit a Nota de Crédito (Credit Note - 61) to annul or discount the erroneous invoice, or a Nota de Débito (Debit Note - 56) to increase the value. Note that the RCV will automatically balance out the totals for that month.
If the discrepancy comes from non-electronic documents (like physical paper boletas or summary voucher sales that weren't emitted as electronic boletas), the SII allows you to Complement the RCV.
- Solution: Go to the SII portal and manually enter these non-electronic sales into the RCV, or use the portal's "Carga Masiva" feature to upload a CSV file containing those missing paper records. This must be done before declaring the monthly Formulario 29.
If the month has already been closed, the taxes were paid, and you subsequently notice a discrepancy between the local ERP and what was declared based on the RCV (often resulting in an "LM" observation from the SII):
- Solution: Go to the SII portal and Rectify the Formulario 29 (F29). The RCV of that past month remains as-is, but the F29 is updated to pay the correct tax difference (along with applicable fines/interest).
When satisfying complex Libro de Compras setups with "Proportional IVA" requirements (like during certification steps for IVA Uso Común), you can flag transient properties dynamically on your intercepted collections before passing them to the generator.
Tip
The IVA Uso Común is a special tax applied to the purchase of goods and services that cannot be determined if these will be directly to generate sales, exempt or not. For example, paying electricity bills, office rent, cleaning and office supplies, etc.
It's calculated this way:
IVA × (Taxable Sales ÷ Total Sales) = IVA Uso Común
For example, imagine you make $500.000 in total sales; of these $400.000 is taxable. This means an 80% of total sales made IVA. If you pay the electricity bill for $23.800, only 80% of IVA becomes fiscal credit.
From $3.800 of IVA from the bill, $3.040 becomes Fiscal Credit, and $760 is cost. The latter goes into Impuesto de Primera Categoría since it's assumed cost.
Assign the iva_uso_comun flag, and map the custom IecvProperty::CommonIvaFactor property so the IecvBuilder can correctly remap traditional nodes towards <TotOpIVAUsoComun>, <TotCredIVAUsoComun> and other advanced retention structures natively.
use Laragear\Dte\Certification\IecvBuilder;
use Laragear\Dte\Certification\IecvProperty;
use Laragear\Dte\Certification\IecvType;
$invoice->iva_uso_comun = true;
$xml = app(IecvBuilder::class)->build(
dtes: $dtes,
type: IecvType::Compras,
period: '2024-03',
resolutionDate: '2024-01-01',
resolutionNumber: 123,
senderRut: $rut,
properties: [
IecvProperty::CommonIvaFactor->of(0.60) // Proportional scale Factor
]
);SII forces business to use a specific email address to send/receive DTE, called the DTE Interchange Mailbox (Correo Electŕonico de Intercambio de DTE).
Incoming SII responses (ACK, respuesta) and B2B DTEs from other companies arrive via email to the DIM, while outbound interchange envelopes and commercial receipts must be emailed back to them.
The library separates between reading (Mailbox Drivers) and sending (Laravel Mailers) these emails. You could, for example, use AWS S3 to process incoming emails but use Mailchimp to send outbound responses.
The library ships four mailbox drivers to fetch and parse unread emails:
imap: standard IMAP (default, slow)microsoft: Microsoft 365 / Exchangegoogleworkspace: Google Workspace APIaws_ses: AWS SES incoming mail via S3
Important
This library does not support legacy pop3 inboxes for reading.
Configure your inbound driver under the dte.mailbox configuration key. The MailboxManager automatically maps these emails and extracts the XML documents.
If you have a service not covered by the drivers, you may create your own by extending the MailboxManager like any other Laravel Manager and register it after your application boots.
use App\Sii\Mailbox\Tuta;
use Laragear\Dte\Mailbox\MailboxManager;
app(MailboxManager::class)->extend('rut', function () {
return new Tuta(config('services.tuta.key'));
});Outbound interchange emails leverage your standard Laravel Mail architecture (config/mail.php).
To prevent your DTE emails from interfering with your application's default transactional emails (e.g. password resets), you should define a dedicated Laravel mailer connection exclusively for DTE within the dte.dim.mailer configuration key.
'dim' => [
// The Laravel Mailer connection (from config/mail.php) used to SEND interchange emails.
// Set to null to use your application's default mailer.
'mailer' => env('DTE_DIM_MAILER'),
],The dte:fetch-mailbox command reads new messages, auto-acknowledges them, and routes SII responses and vendor DTEs into the document status flow using the InboundDteProcessor.
You should schedule this command to run frequently to keep your records updated:
// In routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('dte:fetch-mailbox')->hourly();The interval shouldn't matter for single businesses, as responses are expected to be received during the day or may be never (at the eight day these are considered approved). For multi-tenancy, you should make the interval shorter to avoid bottlenecking (receiving more documents than the app can process).
For inbound documents from other businesses (purchase-side), the InboundDteProcessor parses the received DTE and creates a SiiInboundDocument in your database. You can query these inbound documents to display them in your application and decide if you want to Accept or Reject them.
Tip
Under Ley 19.983, after 8 consecutive days (calendar days) of its reception by the SII, the document is legally considered tacitly accepted ("Aceptación Tácita") and acquires executive merit ("Mérito Ejecutivo"). You don't need to accept each inbound document.
Use the Claim facade or the convenience methods directly on the SiiInboundDocument model to reject or accept it. Note that commercially accepting a document requires your digital certificate to sign the generated Receipt XML.
use Laragear\Dte\Facades\Claim;
use Laragear\Dte\Facades\Certificate;
use Laragear\Dte\Models\SiiInboundDocument;
$document = SiiInboundDocument::find(1);
// You can use the Claim facade directly:
$certificate = Certificate::resolve($document->receiver_rut);
$receiptXml = Claim::accept($document, $document->receiver_rut, 'Santiago', $certificate);
// Or conveniently through the document model:
$receiptXml = $document->accept($document->receiver_rut, 'Santiago', $certificate);
// Alternatively, reject it due to missing goods (RFT)
// Rejections only contact the SII Webservice, and do not generate an XML receipt.
$document->rejectGoods('Faltan 2 cajas de leche');By default, the library automatically emails the generated RespuestaDTE or commercial receipt XML ("Acuse de Recibo") back to the vendor when you accept the invoice. You can opt out of this automatic emailing behavior via the dte.dim.auto_email_receipts configuration option.
Since the interchange system relies on email, malicious actors or systems with faulty integrations might send you documents that are fake (forged signatures) or sent to you but never actually uploaded to the SII (and thus invalid for tax credit).
To protect your business, follow these practices:
- Cryptographic Validation: The library automatically validates the XML signature of all inbound DTEs. If the signature is broken or manipulated, the document status is automatically set to
Forged(or you will receive an exception). You should safely ignore these documents. - SII Consistency Check: Some vendors email their DTE before the SII actually accepts it. If the SII subsequently rejects their envelope, you have an invoice they consider "sent" but the SII considers non-existent. Never pay invoices immediately. Wait at least 48-72 hours and verify that the document appears in your SII Registro de Compras (RCV) before issuing commercial acceptance or payment.
- Guardrails Enforced: Once you accept or reject a document, you cannot do it again. The library enforces this to prevent duplicate claims and keep your system completely in sync with the SII.
Important
You must install the Dompdf package into your application to use the default configuration:
composer require dompdf/dompdfIf you need special DTE designs with advanced CSS (Flexbox/Grid) or JavaScript, use another robust driver, like Browsershot, Gotenberg, or Cloudflare Browser Run, among others.
Most of the time you will require offering a DTE as a PDF, and to move into certification, you will be required to do so. This library allows you to easily create a PDF, or automate this behavior if you require.
To render the PDFs, this library uses spatie/laravel-pdf under the hood. By default, it is configured to use the dompdf driver for simple, table-based layouts without requiring Node.js or headless browsers installed separately, in exchange of using the application memory to build it.
To build a PDF, use the pdf() method of the SiiDte instance. From there, the generate() method will create the PDF if it does not exist. You will receive a Laragear\Dte\Data\PdfData instance with the information of the PDF location (disk and path). By default, it will use your local disk and save these at storage/app/private/dte/pdf/ directory.
use Illuminate\Support\Facades\Storage;
use Laragear\Dte\Models\SiiDte;
$pdf = SiiDte::find(1)->pdf()->generate();
$disk = $pdf->disk;
$path = $pdf->path;
return Storage::disk($disk)->download($path);With the disk and path, you can retrieve the PDF data using the Storage facade of Laravel, for example, to attach to an email.
use App\Mail\InvoiceCreated;
use Illuminate\Support\Facades\Mail;
use Laragear\Dte\Models\SiiDte;
$document = SiiDte::find(1);
Mail::to('ventas@empresa.cl')
->send(new InvoiceCreated($document, $document->pdf()->generate()));PDFs are generated using the {issuer-rut}_{type}_{folio}_{dte_created_at}.pdf pattern, making it easy to search and sort. For example, 76543210-K_33_1045_2026-05-01_193254.pdf.
The generation uses the default disk set in the configuration file of the library. You may also control where the PDF is saved programmatically by issuing the disk and the path as the disk and as methods.
use Laragear\Dte\Models\SiiDte;
SiiDte::find(1)->pdf()->disk('public')->as('my_invoice_33.pdf')->generate();When you call generate(), the PDF won't be overwritten if it already exists. You can overwrite the file using the force() method. It also accepts a method with a condition if you want.
use Laragear\Dte\Models\SiiDte;
SiiDte::find(1)->pdf()->force(fn () => true)->generate();If you want the HTML view used to generate the PDF, without generating the PDF file itself, use the view() method. This is great when you want only to show the PDF as HTML in a controller response, so the user prints it using its browser or through window.print();.
use Laragear\Dte\Models\SiiDte;
public function view(SiiDte $document)
{
return $document->pdf()->view();
}The view also accepts a custom view and data to merge.
use Laragear\Dte\Models\SiiDte;
public function view(SiiDte $document)
{
return $document->pdf()->view('custom-view', ['with-background' => true]);
}Note
Thermal Printers and Tickets: Generating a PDF using a specific ticket width (like 57mm or 80mm) with variable height is not supported by the PDF format. For robust Point of Sale (POS) environments, do not rely on printing PDFs. Instead, print the data natively using raw thermal commands through libraries like mike42/escpos-php which communicate directly with the ESC/POS printer protocol and offer PDF417 2D barcode compliance.
The PDF builder instance is Responsable. If you return it in a controller, it will automatically initialize a download instead of showing an HTML view.
use Laragear\Dte\Models\SiiDte;
public function view(SiiDte $document)
{
// return Storage::disk($document->pdf()->disk)->download($document->pdf()->path);
return $document->pdf();
}Alternatively, you can use the download() method to alter the download, like suggesting a filename or changing the headers.
return $document->pdf()->download(name: 'my_invoice.pdf', headers: ['X-SII' => 'INVOICE']);You may also use the url() to send the URL of the resource to download, or temporaryUrl() to return an URL that only lasts for a minute (by default) on supported filesystems. This is great if you want to pass the URL to an email instead of attaching the PDF, send it through instant messaging, or just push the download to the web server instead.
use Laragear\Dte\Models\SiiDte;
$document = SiiDte::find(1);
return $document->pdf()->url();
return $document->pdf()->temporaryUrl(now()->plus(minutes: 5));If you want total control on how the PDF is rendered, use the customize method with a callback to alter the underlying Spatie\LaravelPdf\PdfBuilder instance.
use Spatie\LaravelPdf\PdfBuilder;
use Laragear\Dte\Models\SiiDte;
$document = SiiDte::find(1);
$document->pdf()->customize(function (PdfBuilder $pdf) {
$pdf->paperSize(210, 297, 'mm')
->margins(10, 10, 10, 10);
})->generate();Warning
PDF Generation uses storage directly because the DTE raw + xml payload takes memory, holding the entire PDF file in memory. Be careful, large documents (or handling multiple) may trigger Out Of Memory (OOM) fatal errors.
If you need the raw PDF content (for example, to stream it to another service without writing to the disk), use the binary() method.
use Laragear\Dte\Models\SiiDte;
$content = SiiDte::find(1)->pdf()->binary();If you want to delete a PDF because you want to reclaim space or just only keep a fresh set of PDF, use the delete() method of the PDF without generating it. If the PDF never existed, it won't raise any exceptions.
use Laragear\Dte\Models\SiiDte;
return SiiDte::find(1)->pdf()->delete();If the PDF was generated using another disk or path, use disk() and as() respectively.
return $document->pdf()->disk('s3')->as('your-invoice.pdf')->delete();You can listen to events like DteCompiled or EnvelopeAccepted to automatically generate the PDF of the documents.
use App\Models\Business;
use App\Jobs\SendDtePdfToBusiness;
use Illuminate\Support\Facades\Event;
use Laragear\Dte\Events\DteCreated;
use Laragear\Dte\Models\SiiDte;
Event::listen(DteCreated::class, function (DteCreated $event) {
$business = Business::findByRut($event->dte->receiver_rut);
$pdfLocation = $event->dte->pdf()->generate();
$business->notify(new InvoiceReady($pdfLocation));
});This library brings some validation rules to use when uploading digital certificates and CAF, which are required to work with SII endpoints and require constant renewal.
Use the sii_certificate rule to validate the uploaded certificate. It will check the mime type (com.rsa.pkcs-12), use the password input to decrypt it, and check the expiration date.
public function upload(Request $request)
{
$request->validate([
'certificate' => 'required|sii_certificate',
'password' => 'required|string',
]);
// ...
}Important
If the password of the certificate is empty, the validation will fail.
When the password is somewhere else in the input, you can use an argument to tell the validation rule where to find it.
$request->validate([
'cert' => 'required|sii_certificate:cert_pass',
'cert_pass' => 'required|string',
]);Use the sii_caf rule to validate the CAF. It will check in the database if the CAF already exists for the target business, document, and folio. If it already exists, it will be invalid.
public function upload(Request $request)
{
$request->validate([
'caf' => 'required|sii_caf',
]);
}To avoid forging the CAF issuer RUT, you can use a parameter to check if the CAF is assigned to the given RUT. If the CAF Issuer RUT is different, the CAF will be invalid.
$request->validate([
'caf' => 'required|sii_caf:76.123.456-0',
]);By default, this package works in a single-business mode using static configuration. To support multi-tenancy seamlessly, you should use the Dynamic Configuration pattern. This ensures all automated background tasks (like packing envelopes, sending DTEs, generating PDFs, and interacting with the DIM) automatically resolve the correct business details and certificates for the document being processed.
If you are not using the dynamic configuration closures or need to override the globally resolved issuer for a specific document, you can use the issuedBy() method from any document builder to explicitly set which business is issuing the document.
use Laragear\Dte\Facades\Dte;
use Laragear\Dte\Data\IssuerData;
$business = IssuerData::make(
'76.123.456-0',
'Panadería LTDA',
'Amasandería',
'Venta de productos comestibles',
// ...
);
$invoice = Dte::invoice()
->issuedBy($business)
->receivedBy('18685226-5', 'Adrián Roberto Pérez González')
->addItem('Vitrina', 140_000)
->create();To support incoming multi-tenant documents, implement and bind the Laragear\Dte\Contracts\TenantResolverInterface. The library uses this interface to securely route and map incoming XML envelopes to the correct tenant model in your application by its receiver RUT, while discarding those without a tenant.
use App\Models\Business;
use Laragear\Dte\Contracts\TenantResolverInterface;
class DatabaseResolver implements TenantResolverInterface
{
public function resolve(Rut $rut): ?object
{
return Business::whereRut($rut)->first();
}
}
$this->app->bind(TenantResolverInterface::class, function () {
return new DatabaseResolver;
});Warning
This library does not support fetching mails from multiple sources, like for each tenant. For multi-tenant applications, require your tenants to redirect/register their DIM to a universal app mailbox, e.g. dte@my-app.com.
This is a limitation on email technologies due to SII shortsightedness: polling multiple mailboxes via IMAP does not scale, and using WebHooks is sketchy in terms of security (who sends it) and requires multiple parsing drivers (each email service sends data with different schemas).
This library brings a lot of commands, but these are required since working with SII requires a lot of automation.
| Command | Description |
|---|---|
dte:make-fake-cert |
Generate a self-signed dummy PKCS#12 (.p12) digital certificate for local/testing environments. |
dte:make-fake-caf |
Generate a dummy SII CAF XML file containing valid structural tags and a newly minted RSA keypair. |
dte:check-cafs |
Check for active CAFs nearing folio depletion and dispatch events |
dte:poll-track-status |
Queries SII for TrackID status, updating envelope to Accepted or Rejected |
dte:fetch-mailbox |
Poll configured DTE mailbox for UNREAD messages and process them |
dte:reject-phantom-invoices |
Reject PhantomPending invoices nearing the automatic acceptance deadline |
dte:compile {dte_id} |
Compile a DTE XML from its model |
dte:pack-ready |
Packs signed DTEs into envelopes and dispatches processing. |
dte:process-envelope {envelope_id} |
Process an envelope, signs it, and send it to the SII |
You can listen to these events in your EventServiceProvider to trigger notifications or custom logic:
| Event | Dispatched when... |
|---|---|
AecCessionCreating |
Before an AEC Cession is built and signed |
AecCessionCreated |
After an AEC Cession is generated |
CafDepleted |
When a CAF has no available folios |
CafExpiring |
When a CAF has 7 or fewer days before expiration |
CafLoaded |
When a CAF is successfully loaded into the database |
CafNearDepleted |
When check-cafs finds folios below threshold |
DteAccepted |
When a DTE is successfully processed and accepted by the SII |
DteAltered |
When a synced Cuadratura record has distinct float totals (amountTotal) than the local DB bounds |
DteCompiled |
Dispatched containing the generated raw XML right before transmission (Ideal for Storage Backups) |
DteCompiling |
Before a DTE XML is compiled |
DteCreated |
After a document is persisted (success or failure) |
DteCreating |
Before a document is built and signed |
DteRejected |
When a DTE inside a rejected envelope fails to be accepted |
DteUnregistered |
When the Cuadratura matches an Outbound DTE found in SII but completely missing in the DB |
EnvelopeAccepted |
After the SII processes and accepts an Envelope |
EnvelopeRejected |
After the SII processes and rejects an Envelope |
EnvelopeSending |
Before sending an envelope to the SII |
EnvelopeSent |
After successfully sending an envelope to the SII |
InboundDteAcknowledged |
When an inbound DTE is commercially acknowledged |
InboundDteAnswered |
When an inbound DTE is accepted or rejected |
InboundDteReceived |
When an inbound DTE envelope is received |
InboundForgedDteReceived |
When an inbound DTE is found to be tampered or forged |
Note
Accepted Events and Repairs: The EnvelopeAccepted and DteAccepted events are dispatched unconditionally when the SII confirms the document was processed (e.g., EPR or DOK), even if there are repairs or rejections inside it. If you need to log or react to discrepancies, check for repairs directly inside your event listener:
use Laragear\Dte\Events\EnvelopeAccepted;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
Event::listen(function (EnvelopeAccepted $event) {
if ($event->envelope->isAcceptedWithRepairs()) {
Log::warning('Envelope accepted with repairs!', [
'repairs' => $event->envelope->repairs,
'raw_xml' => $event->envelope->payload->sii_response,
]);
}
});use App\Notifications\CafDepletedWarning;
use Laragear\Dte\Events\CafNearDepleted;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Mail;
Event::listen(function (CafNearDepleted $event) {
Mail::to('admin@company.com')->send(new CafDepletedWarning($event->caf));
});During testing, the library detects your environment as testing automatically. Alternatively, you can force the environment in your phpunit.xml:
<phpunit>
<php>
<env name="APP_ENV" value="testing"/>
<env name="DTE_ENV" value="testing"/>
</php>
</phpunit>In testing and local environments, the library incorporates safety mechanisms to guarantee tests run instantly and never hit the real SII servers:
- Envelope Uploads instantly return a generated fake string (e.g.,
fake-track-id-123). - SOAP Gateways intentionally throw a
RuntimeExceptionto prevent hanging scripts and external requests.
Because the library relies heavily on Queues and Events for asynchronous operations, there are two ways to test this library:
- Directly mock the
Laragear\Dte\Builderclass (recommended). - Use Laravel's built-in fakes.
Instead of executing the whole pipelines for signing documents, you can mock one of the document builders by setting specific expectations, ensuring your application creates exactly what you require.
use Laragear\Dte\Builders\InvoiceBuilder;
use Laragear\Dte\Models\SiiDte;
public function test_document_is_queued_for_upload()
{
// 1. Set expectations of the invoice to create.
$this->mock(InvoiceBuilder::class, function ($mock) {
$mock->expects('receivedBy')->with('76.123.456-0', 'Tienda Agrícola S.A.')->andReturnSelf();
$mock->expects('addItem')->with('Leche fresca', 12_600)->andReturnSelf();
$mock->expects('create')->andReturn(SiiDte::factory()->invoice()->create());
})
// 2. Hit the controller to create the DTE and assert there are no errors.
$this->post('checkout/return')->assertOk();
}If you want to test how your application reacts when the SII rejects a document, fake the event system directly.
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Notification;
use Laragear\Dte\Events\DteRejected;
use Laragear\Dte\Models\SiiDte;
use App\Notifications\DocumentRejectedNotification;
public function test_user_is_notified_when_document_is_rejected()
{
Notification::fake();
Event::fake([DteRejected::class]);
$dte = SiiDte::factory()->invoice()->create();
// Dispatch the event manually as if the Polling command received an RSC from SII
DteRejected::dispatch($dte);
// Assert your application's listeners ran (e.g., Notification sent)
Notification::assertSentTo($user, DocumentRejectedNotification::class);
}If you need to test the exact internal polling logic, you can mock the Gateway classes directly in the service container.
use Laragear\Dte\Gateways\SoapGateway;
use Laragear\Dte\Jobs\PollEnvelopeTrackIdJob;
use Laragear\Dte\Models\SiiDteEnvelope;
use Laragear\Dte\Enums\EnvelopeStatus;
public function test_polling_updates_envelope_status()
{
// Mock the SOAP gateway to return a forced 'Accepted' (EPR) response
$mockGateway = $this->mock(SoapGateway::class, function ($mock) {
$mock->shouldReceive('query')
->withArgs(fn($rut, $service, $action, $args) => $args['TrackId'] === '12345')
->andReturn('<ESTADO>EPR</ESTADO>');
});
$envelope = SiiDteEnvelope::factory()->create(['track_id' => '12345']);
// Run the job with the mocked gateway
(new PollEnvelopeTrackIdJob($envelope))->handle($mockGateway);
$this->assertEquals(EnvelopeStatus::Accepted, $envelope->fresh()->status);
}Caution
For Certification and Production, you require a real certificate, which can be bought separately. Do not proceed until it's made available to the library.
To operate with the SII, the Certification Process is mandatory. SII will test your application if it complies with the basic and legal procedures to manage DTE. Luckily for you, this library makes this process simple.
Switch environments via the DteEnvironment enum (local, testing, certification, production) in your config or .env. Then, you can build a GUI in your application that delegates the handshake process with SII's Maullín endpoint to the CertificationManager.
DTE_ENV=certificationYou will be faced with several tasks to comply with the certification process, all handled via the manager:
- Test Set: The SII will instruct you to create a list of DTE in your application (amounts, receiver RUT, etc.). The manager will take care of sending them in an envelope to SII Servers, and generate the IECV XML for manual upload.
- Simulate: The manager automatically sends a batch of 10-100 recent/real documents to simulate continuous operation.
- Interchange: The manager automatically ingests an uploaded Interchange DTE XML, validates it, and mails the response back to the SII.
- PDF Printing: The manager will automatically generate PDF for all the test files. If SII requires a single PDF, you will need to concatenate it. Don't worry; there are free online services to concatenate PDF like: I Love PDF, BentoPDF, EmbedPDF, PrivatePDF Merge, Pipefile, Toolflic, and many more.
- Compliance: The SII will let you digitally sign your compliance to operate on production servers.
use Laragear\Dte\Certification\CertificationManager;
public function certify(CertificationManager $manager)
{
// Step 1: Send the Test Set envelope and get the IECV XML
$data = $manager->testSet('76.123.456-0', dteIds: [1, 2, 3]);
// Step 2: Send a Simulation envelope of 10 documents
$data = $manager->simulate('76.123.456-0', quantity: 10);
// Step 3: Handle the Interchange XML from SII
$data = $manager->interchange('76.123.456-0', xmlContent: '...', location: 'Santiago');
// Step 4: Generate PDFs for the test documents
$data = $manager->printSample('76.123.456-0');
// After you are done with the certification, securely wipe the certification data!
$manager->purgeDatabase();
}Once your application is prepared to operate with real transactions, move the package to production:
DTE_ENV=productionWarning
Once you sign, you will no longer be able to access the SII Web App to manage DTE. Back up all your historical data before. If you consider this a drawback, desist and use this library as a way to manually mirror your data in SII.
The SII requires businesses to safely back up generated DTE documents as a contingency measure for 6 years. This library delegates the backup logic to your application, allowing you to use your preferred cloud storage.
To automate backups, listen to the DteCompiled or EnvelopeSent events and push a queued job to save the document once it's compiled:
namespace App\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Storage;
use Laragear\Dte\Events\DteCompiled;
class BackupDteDocument implements ShouldQueue
{
public function handle(DteCompiled $event): void
{
Storage::disk('cold-storage')->put(
"dte/backups/{$event->dte->issuer_rut}/{$event->dte->document_type->value}-{$event->dte->folio}.xml",
$event->dte->payload->xml
);
}
}Remember to register this listener in your EventServiceProvider or let Laravel discover it automatically.
This package can be configured either statically (via a configuration file) or dynamically (via database or runtime settings).
For simple setups, you can publish the static configuration using Artisan:
php artisan vendor:publish --provider="Laragear\Dte\DteServiceProvider" --tag="config"You will receive the config/dte.php config file. Due to the length of the file, everything is explained there instead of re-explained here.
In most real-world scenarios, applications expect the end-user to fill in their company details (RUT, resolution number, certificate, etc.) from a GUI. Relying on static configuration files makes it hard to update or initialize this data dynamically.
You can configure the library dynamically using closures in your AppServiceProvider via the static helpers in ConfigurationManager and CertificateResolver. The library will evaluate these resolvers at runtime before falling back to the config/dte.php defaults.
If your application handles multiple businesses (Multi-Tenancy), you should use the granular resolvers. This guarantees that automated background tasks (like queuing DTE envelopes) can pass the contextual RUT to resolve the exact tenant dynamically.
use Laragear\Dte\Configuration\ConfigurationManager;
use Laragear\Dte\Certificate\CertificateResolver;
use Laragear\Dte\Data\IssuerData;
use Laragear\Dte\Certificate\DigitalCertificate;
use Laragear\Rut\Rut;
public function boot()
{
// Resolve the Issuer (business data) dynamically
ConfigurationManager::resolveIssuerUsing(function (?Rut $rut) {
$settings = $rut ? Settings::where('rut', $rut->formatRaw())->first() : Settings::first();
return $settings ? IssuerData::make(
rut: $settings->rut,
legalName: $settings->legal_name,
businessActivity: $settings->business_activity,
economicActivity: $settings->economic_activity,
address: $settings->address,
commune: $settings->commune,
city: $settings->city,
resolutionDate: $settings->resolution_date,
resolutionNumber: $settings->resolution_number,
) : null;
});
// Resolve the Sender RUT (the authorized signer)
ConfigurationManager::resolveSenderUsing(function (Rut $issuerRut) {
return Settings::where('rut', $issuerRut->formatRaw())->value('sender_rut');
});
// Resolve the digital certificate
CertificateResolver::resolveUsing(function (Rut $signerRut) {
$cert = Certificate::where('rut', $signerRut->formatRaw())->first();
return $cert ? new DigitalCertificate($cert->p12, $cert->password) : null;
});
}- There are no singletons using a stale application instance.
- There are no singletons using a stale config instance.
- There are no singletons using a stale request instance.
- The
Pdf417Generatoruses a scopedPDF417dependency. - All external I/O goes through proxy classes (
OpenSslProxy,SoapProxy,ImapProxy, etc.) that are resolved fresh per request, so no stale handles or leaked connections survive across requests.
This library is 100% compatible with Laravel Octane (Swoole, Roadrunner & FrankenPHP).
This package includes Laravel Boost AI Guidelines for your agents. Also included are the following AI Skills:
- Set up
- Document building (Basic)
- PDF generation
After installing this package, ensure your Laravel Boost files are updated using the boost:update command:
php artisan boost:updateThe following security consideration has been made while creating this Laravel package.
- DTE uses a numbered folio that starts from zero. Using UUID to obfuscate the number is a moot point; business activity can always be inferred legally.
- Every XML document is validated against SII's XSD schemas before signing, preventing malformed output from reaching SII.
- Status transitions are guarded: once a document reaches a terminal status (accepted/rejected), it cannot be modified.
- RCV Sync sends events when discrepancies are found.
If you discover any security-related issues, Report a Vulnerability in the repository instead of using the issue tracker.
Digital certificates (.p12|pfx) and CAF private keys must never be committed to version control. Add them to .gitignore individually, or ignore the entire target folder. Location will depend on your configuration.
/storage/app/private/dte/*Clone this repository, make your changes, and send a PR. Few rules, though:
- Don't make huge rewrites. If I can't understand it on a Friday afternoon, I'll close it.
- Don't push AI slop. Follow styles and conventions as I would write it, not you or your agent.
- Don't extend it for only you. Features should benefit everyone, not only your use case.
This library is made available for free, don't act like anyone owes you anything.
- I messed up the price on an invoice I just created. Can I just find the
SiiDtemodel and update the database or delete the row?
Absolutely not. You must emit a Nota de Crédito (Credit Note - Code 61) to annul or discount the erroneous invoice, or a Nota de Débito (Debit Note - Code 56) to increase the value.
By the moment you notice, the XML will be already reserved it's folio. It's just better to accept the error, or provide drafting in your app before commiting data to the library.
- Why is this library forcing me to use Queues and background jobs just to create an XML? Can't I just build and send it synchronously in my controller?
Because XML compilation is computationally costly.
You can use the ->create(sync: true) argument to get the XML immediately (useful for printing receipts), but you shouldn't bypass the envelope and polling architecture. Use it only when required (like for immediately printing PDF) and sparringly.
- How do I automate the download of the CAF (Folios) so my users don't have to upload XML files manually? Is there a REST endpoint for that?
The SII does not have REST/SOAP endpoints for CAF handling. You must download the CAF XML manually from the SII portal and load it into the library.
Some web applications do this by using through headless browser automation (Playwright, Puppeteer, Selenium, etc), and charging for the privilege.
This library may include this in the future based on support.
- I need to print a Boleta (Receipt) on an 80mm thermal POS printer. How do I make the library's PDF generator format the paper size correctly?
Don't use PDFs for thermal receipts, these have a static height. Use a raw thermal command library (like mike42/escpos-php) instead.
- Can I just save up all my Boletas (Receipts) for the day and send a single summary to the SII at midnight to save server resources?
No. Electronic receipts (Codes 39 and 41) must be transmitted immediately. You can only do that with the remaining DTE types.
- I use UUIDs for all my database primary keys. Can I just drop the customer's cart UUID into the reference Folio field?
No, references are character-limited. Use an alternative reference instead (random string, integer, etc.)
- I'm building a multi-tenant SaaS. Can I configure the mailbox listener to poll
dte@client-a.cl,dte@client-b.cl, anddte@client-c.cl?
No, this library does not support fetching emails from multiple sources. Instead, point your customers to an unified dte@your-app.cl and fetch from there.
- I wrote an event listener to automatically accept incoming vendor invoices the second they hit the mailbox. Good idea?
Terrible idea! Never pay invoices immediately upon email receipt. If you didn't put money on it, the invoice may be a scam.
- I tried streaming 100+ invoice PDFs at once using the
binary()method to send to a third-party API, and my server crashed with an Out Of Memory (OOM) error. Is the package leaking memory?
It's not a leak. You're just holding massive amounts of data in RAM. Stop using binary() for large operations. Use a single queued-job for each PDF.
- If I install this on my application, I can instantly issue DTE legally?
No, you need to be certified by SII. This library helps to do that.
- If I build this automated integration, can my client still log into the free SII web portal to manually issue a quick invoice if my app goes down?
No. Once they sign to production with your software, they are locked out of the free SII tool.
- I want to charge clients for my web app. Do I have to open-source my entire codebase if I use this? I heard Chilean DTE libraries enforce this.
No, you can keep your application closed-source and commercial, or even totally private.
LibreDTE uses the AGPL License, which imposes restrictions on usage and distribution. This library does not.
This is a small glossary for some concepts with library works with.
- CAF (Código de Autorización de Folios): An official XML file granted by SII containing an authorized range of serial numbers (folios) and an RSA private key for a specific document type. A valid CAF block must be loaded in the database before issuing documents.
- Digital Certificate: A PKCS#12 (
.p12|pfx) digital signature, file that authorizes your business to sign tax documents and communicate with SII endpoints. - DTE (Documento Tributario Electrónico): A digitally signed XML document representing a legally binding tax event (Invoice, Receipt, Credit Note, etc.).
- DTE Envelope: A signed XML that contains all your signed DTE to send to SII in bulk per-business. Boletas (Receipts) use a specialized
<EnvioBOLETA>envelope that groups up to 500 documents. - PDF: Visual (and legal) representation of the DTE XML that includes a PDF417 2D barcode. Not legally required to be sent to the business on production, but mandatory for certification. Respectable business always sends these.
Note
Daily receipt (boletas) summary reports were abolished on August 1, 2022 (SII Res. Ex. N° 53 de 2022). Receipts are transmitted to SII via envelopes and populate the taxpayer's sales registry (Registro de Compras y Ventas, RCV) automatically.
This library is not affiliated with SII, nor the Chilean Government, or any of the businesses that implement SII connectors, ERP, or both.
This package is open-sourced software licensed under the MIT license.
Laravel is a Trademark of Taylor Otwell. Copyright © 2011–2026 Laravel LLC.
