Electronic invoicing in Chile is mandatory for most taxpayers. Integrating it correctly requires understanding the complete flow: from XML generation to status verification.
System Components
┌─────────────┐ ┌──────────────┐ ┌─────────┐
│ Your System │────▶│ XML + Sign │────▶│ SII │
└─────────────┘ └──────────────┘ └─────────┘
│ │ │
│ Digital │
│ Certificate │
▼ ▼
┌─────────────┐ ┌─────────┐
│ PDF │ │ Status │
│ + QR TED │◀────────────────────────│ (SOAP) │
└─────────────┘ └─────────┘
1. Obtain Session Token
SII requires SOAP authentication with your digital certificate:
import zeep
from signxml import XMLSigner
def get_token(cert_path: str, key_path: str) -> str:
"""Obtains session token from SII."""
# 1. Get seed
client = zeep.Client('https://palena.sii.cl/DTEWS/CrSeed.jws?WSDL')
response = client.service.getSeed()
# 2. Sign seed with certificate
signed_seed = sign_xml(response, cert_path, key_path)
# 3. Get token
client = zeep.Client('https://palena.sii.cl/DTEWS/GetTokenFromSeed.jws?WSDL')
token_response = client.service.getToken(signed_seed)
return extract_token(token_response)
2. Build the DTE (Electronic Tax Document)
The XML must follow the exact SII schema:
from lxml import etree
def build_receipt(sale: Sale, folio: int) -> str:
"""Builds Electronic Receipt XML (type 39)."""
dte = etree.Element('DTE', version='1.0')
document = etree.SubElement(dte, 'Documento', ID=f'DTE-{folio}')
# Header
header = etree.SubElement(document, 'Encabezado')
id_doc = etree.SubElement(header, 'IdDoc')
etree.SubElement(id_doc, 'TipoDTE').text = '39' # Receipt
etree.SubElement(id_doc, 'Folio').text = str(folio)
etree.SubElement(id_doc, 'FchEmis').text = sale.date.isoformat()
# Issuer
issuer = etree.SubElement(header, 'Emisor')
etree.SubElement(issuer, 'RUTEmisor').text = sale.company.rut
etree.SubElement(issuer, 'RznSoc').text = sale.company.business_name
# Item details
for i, item in enumerate(sale.items.all(), 1):
detail = etree.SubElement(document, 'Detalle')
etree.SubElement(detail, 'NroLinDet').text = str(i)
etree.SubElement(detail, 'NmbItem').text = item.product.name
etree.SubElement(detail, 'QtyItem').text = str(item.quantity)
etree.SubElement(detail, 'PrcItem').text = str(item.price)
return etree.tostring(dte, encoding='unicode')
3. Sign with Digital Certificate
from signxml import XMLSigner, methods
from cryptography import x509
from cryptography.hazmat.primitives import serialization
def sign_dte(xml: str, cert_path: str, key_path: str) -> str:
"""Signs XML with SII .pfx certificate."""
# Load certificate
with open(cert_path, 'rb') as f:
cert = x509.load_pem_x509_certificate(f.read())
with open(key_path, 'rb') as f:
key = serialization.load_pem_private_key(f.read(), password=None)
# Sign
root = etree.fromstring(xml.encode())
signer = XMLSigner(
method=methods.enveloped,
signature_algorithm='rsa-sha1',
digest_algorithm='sha1'
)
signed = signer.sign(root, key=key, cert=cert)
return etree.tostring(signed, encoding='unicode')
4. Generate TED (Electronic Stamp)
The TED is a code that allows authenticity verification:
import qrcode
from io import BytesIO
def generate_ted(signed_dte: str) -> bytes:
"""Generates QR with TED for printing."""
# Extract TED data from signed XML
root = etree.fromstring(signed_dte.encode())
ted = root.find('.//TED')
ted_string = etree.tostring(ted, encoding='unicode')
# Generate QR
qr = qrcode.QRCode(version=1, box_size=3)
qr.add_data(ted_string)
qr.make(fit=True)
img = qr.make_image()
buffer = BytesIO()
img.save(buffer, format='PNG')
return buffer.getvalue()
5. Verify Status
def verify_status(issuer_rut: str, dte_type: int, folio: int) -> dict:
"""Queries DTE status from SII."""
client = zeep.Client(
'https://palena.sii.cl/DTEWS/QueryEstDte.jws?WSDL'
)
response = client.service.getEstDte(
RutConsultante=issuer_rut,
DvConsultante=calculate_dv(issuer_rut),
RutCompania=issuer_rut,
DvCompania=calculate_dv(issuer_rut),
RutReceptor='66666666', # Receipts go to generic RUT
DvReceptor='6',
TipoDte=dte_type,
FolioDte=folio,
Token=get_token()
)
return {
'status': response.ESTADO,
'message': response.GLOSA_ESTADO,
'accepted': response.ESTADO == 'DOK'
}
Retry System
Submissions can fail. Implement robust retries:
from celery import shared_task
from datetime import timedelta
@shared_task(bind=True, max_retries=3)
def send_dte_async(self, dte_id: int):
try:
dte = DTE.objects.get(id=dte_id)
result = send_to_sii(dte)
dte.status = result['status']
dte.save()
except Exception as e:
# Retry in 30 minutes
raise self.retry(exc=e, countdown=1800)
Conclusion
SII integration is complex but manageable if you divide the problem into clear steps. Most importantly, handle errors well and have a robust retry system.