---
title: "Boleta de principio a fin"
description: "Qué tienen de distinto las boletas y un programa completo en Python que construye, valida, envía al SII, consulta el estado y genera el PDF de una boleta electrónica."
type: "academy"
category: "lesson"
tags: []
authors: [Anonymous]
date: "2026-11-05"
last_update: "2026-11-05"
time_minutes: 9
draft: false
unlisted: false
url: "https://www.apigateway.cl/academy/emision-de-dte/casos-practicos/boletas"
---

# Boleta de principio a fin

La **boleta electrónica** (`39`) y la **boleta exenta** (`41`) son los documentos para ventas a **consumidores finales**. Siguen los mismos seis pasos que una factura, pero con dos diferencias importantes: algunos datos del documento cambian, y **el envío al SII y la consulta del estado usan otras operaciones**, porque el SII recibe las boletas por un servicio propio.

## Qué cambia respecto de una factura

| Aspecto | Factura (`33`) | Boleta (`39`) |
|---------|----------------|---------------|
| Precio en el detalle (`PrcItem`) | Neto, sin IVA | **Con IVA incluido** |
| Receptor | Datos completos de la empresa | Opcionales. Si el cliente no da su RUT, se usa `66666666-6`. |
| `GiroRecep`, `Acteco`, `FmaPago` | Se envían | No se usan |
| CAF | De facturas (`33`) | De boletas (`39`, o `41` para la exenta) |
| Sobre (paso 3) | `EnvioDTE` | `EnvioBOLETA` (la API lo crea sola) |
| Enviar al SII (paso 4) | `sii_dte/sendXmlDocument` | **`sii_boleta/sendXmlDocument`** |
| Consultar el estado (paso 5) | `sii_dte/checkXmlDocumentSentStatus` | **`sii_boleta/checkXmlDocumentSentStatus`** |
| Verificar un documento | `/api/v2/sii/dte/emitidos/verificar` | **`sii_boleta/validateDocument`** |
| Copia cedible del PDF | Sí | No |

El resto es igual: `builder/build`, las validaciones, `dispatcher/create` y `renderer/render` reciben los mismos parámetros. En cada paso, esto es lo que corresponde a las boletas:

| Paso | Dónde está |
|------|----------------------|
| 1. Construir | [1.2. Boletas](https://www.apigateway.cl/academy/emision-de-dte/emision/paso-1-construir-el-dte#12-boletas): los datos y el CAF |
| 2. Validar | [Las mismas validaciones](https://www.apigateway.cl/academy/emision-de-dte/emision/paso-2-validar) |
| 3. Armar el sobre | [Un sobre por tipo de documento](https://www.apigateway.cl/academy/emision-de-dte/emision/paso-3-armar-el-sobre#un-sobre-por-tipo-de-documento) |
| 4. Enviar al SII | [4.2. Boletas](https://www.apigateway.cl/academy/emision-de-dte/emision/paso-4-enviar-al-sii#42-boletas) |
| 5. Consultar el estado | [5.2. Boletas](https://www.apigateway.cl/academy/emision-de-dte/emision/paso-5-consultar-el-estado#52-boletas) |
| 6. Generar el PDF | [Copia tributaria](https://www.apigateway.cl/academy/emision-de-dte/emision/paso-6-generar-el-pdf#copia-tributaria-y-copia-cedible) |

## Lo que necesitas

| Archivo o dato | Dónde se explica |
|----------------|------------------|
| Token de API Gateway | [Primeros pasos](https://www.apigateway.cl/academy/primeros-pasos) |
| Certificado digital en PEM: `cert.pem` y `key.pem` | [El certificado digital](https://www.apigateway.cl/academy/emision-de-dte/preparacion/certificado-digital) |
| CAF de boletas (`39`) del ambiente de certificación | [Folios y CAF](https://www.apigateway.cl/academy/emision-de-dte/preparacion/folios-y-caf) |
| Fecha de la resolución en certificación | [Resolución de autorización](https://www.apigateway.cl/academy/emision-de-dte/fundamentos/resolucion-de-autorizacion) |

> [!TIP] ¿Aún no tienes CAF de certificación?
>
> Puedes probar los pasos 1, 2, 3 y 6 con un CAF y un certificado de prueba. Te explicamos cómo en [Probar sin el SII](#probar-sin-el-sii).

## El programa

Instala `requests` (`pip install requests`) y define la variable de entorno `API_TOKEN`. Deja en la misma carpeta los archivos `cert.pem`, `key.pem` y `caf_39.xml`.

```python
"""Emite una boleta electrónica en el ambiente de certificación del SII."""
import base64
import os
import time

import requests

API_URL = 'https://app.apigateway.cl/api/v2/libredte'
HEADERS = {
    'Authorization': f'Token {os.environ["API_TOKEN"]}',
    'Content-Type': 'application/json',
}

AMBIENTE = 1  # 1: certificación, 0: producción.
SII_RUT = '60803000-K'

# Estados del envío que ya no cambian (ver el paso 5). Los demás significan
# que el SII sigue procesando, o que la consulta falló y hay que repetirla.
ESTADOS_FINALES = {'EPR', 'LOK', 'RPR', 'RLV', 'RCH', 'RCO', 'RFR', 'RSC', 'RCT', 'RPT', 'VOF'}
ESTADOS_ACEPTADOS = {'EPR', 'LOK', 'RPR', 'RLV'}


def libredte(operacion: str, parameters: dict) -> dict | list | None:
    """Ejecuta una operación de LibreDTE y entrega el contenido de `data`."""
    response = requests.post(
        f'{API_URL}/{operacion}',
        json={'parameters': parameters},
        headers=HEADERS,
        timeout=120,
    )
    if response.status_code != 200:
        try:
            detalle = response.json().get('detail', response.text)
        except ValueError:  # La respuesta de error no viene en JSON.
            detalle = response.text
        raise RuntimeError(f'{operacion}: HTTP {response.status_code}: {detalle}')
    return response.json()['data']


def leer_texto(ruta: str) -> str:
    """Lee un archivo de texto, como el certificado o la llave en PEM."""
    with open(ruta, encoding='ascii') as archivo:
        return archivo.read()


def leer_base64(ruta: str) -> str:
    """Lee un archivo y lo entrega codificado en base64."""
    with open(ruta, 'rb') as archivo:
        return base64.b64encode(archivo.read()).decode()


def guardar_base64(ruta: str, contenido: str) -> None:
    """Decodifica un base64 y lo guarda como archivo, sin tocar sus bytes."""
    with open(ruta, 'wb') as archivo:
        archivo.write(base64.b64decode(contenido))


# Datos que normalmente vienen de tu base de datos.
CERTIFICADO = {
    'certificate': leer_texto('cert.pem'),
    'privateKey': leer_texto('key.pem'),
}
CAF = leer_base64('caf_39.xml')
EMISOR = {
    'rut': '88888888-8',
    'razon_social': 'Mi Empresa SpA',
    'autorizacion_dte': {
        'fecha_resolucion': '2025-11-20',  # Fecha de autorización en certificación.
        'numero_resolucion': 0,            # En certificación siempre es 0.
    },
}
FOLIO = 1  # El siguiente folio libre del CAF de boletas.

# Paso 1: construir la boleta. Los precios van con IVA incluido.
datos = {
    'Encabezado': {
        'IdDoc': {'TipoDTE': 39, 'Folio': FOLIO},
        'Emisor': {
            'RUTEmisor': EMISOR['rut'],
            'RznSoc': EMISOR['razon_social'],
            'GiroEmis': 'Venta de artículos de computación',
            'DirOrigen': 'Av. Siempre Viva 742',
            'CmnaOrigen': 'Providencia',
        },
        'Receptor': {
            'RUTRecep': '66666666-6',  # Cliente sin RUT.
            'RznSocRecep': 'Cliente final',
            'DirRecep': 'Santiago',
            'CmnaRecep': 'Santiago',
        },
    },
    'Detalle': [
        {'NmbItem': 'Mouse inalámbrico', 'QtyItem': 2, 'PrcItem': 11900},
    ],
}

boleta = libredte('billing/document/builder/build', {
    'bag': {'parsedData': datos, 'caf': CAF, 'certificate': CERTIFICADO},
})
xml_boleta = boleta['document_xml']
guardar_base64(f'boleta_39_{FOLIO}.xml', xml_boleta)
totales = boleta['document_normalized']['Encabezado']['Totales']
print('Boleta construida:', boleta['document_id'], 'total', totales['MntTotal'])

# Paso 2: validar el esquema antes de enviar.
libredte('billing/document/validator/validateSchema', {'source': xml_boleta})

# Paso 3: armar el sobre dirigido al SII. La API crea un EnvioBOLETA.
sobre = libredte('billing/document/dispatcher/create', {
    'bag': {
        'xmlDocument': xml_boleta,
        'certificate': CERTIFICADO,
        'emisor': EMISOR,
        'receptor': {'rut': SII_RUT},
    },
})
print('Sobre:', sobre['tag'])  # EnvioBOLETA
guardar_base64(f'sobre_sii_39_{FOLIO}.xml', sobre['xml'])

# Paso 4: enviar al SII con el servicio de boletas.
request_sii = {'certificate': CERTIFICADO, 'options': {'environment': AMBIENTE}}
envio = libredte('billing/integration/sii_boleta/sendXmlDocument', {
    'request': request_sii,
    'doc': sobre['xml'],
    'company': EMISOR['rut'],
})
track_id = envio['track_id']
print('Enviado al SII, Track ID:', track_id)  # ¡Guárdalo en tu base de datos!

# Paso 5: consultar el estado hasta tener un resultado final. La primera
# espera respeta los segundos que pidió el SII al recibir el envío.
estado = None
esperas = [max(30, envio['retry_after'] or 0), 60, 120, 300, 600]
for espera in esperas:
    time.sleep(espera)
    try:
        estado = libredte('billing/integration/sii_boleta/checkXmlDocumentSentStatus', {
            'request': request_sii,
            'trackId': track_id,
            'company': EMISOR['rut'],
        })
    except RuntimeError as error:
        # La consulta falló (por ejemplo, el SII no respondió): se reintenta.
        print('Falló la consulta, se reintenta:', error)
        continue
    print('Estado:', estado['status'], estado['description'])
    if estado['status'] in ESTADOS_FINALES:
        break

resumen = estado['resume'] if estado else {}
if not estado or estado['status'] not in ESTADOS_FINALES:
    print('Sin estado final todavía. Vuelve a consultar más tarde con el Track ID', track_id)
elif estado['status'] == 'RPT':
    print('El SII ya había recibido este sobre: consulta el estado con el Track ID del primer envío.')
elif estado['status'] == 'VOF':
    print('El SII no encontró el archivo: envía de nuevo el mismo sobre (no lo construyas de nuevo).')
elif estado['status'] in ESTADOS_ACEPTADOS and resumen.get('rejected', 0) == 0:
    resultado = 'aceptada con reparos' if resumen.get('repairs') else 'aceptada'
    print('Resultado:', resultado)

    # Paso 6: generar el PDF (las boletas solo tienen copia tributaria).
    pdf = libredte('billing/document/renderer/render', {
        'bag': {
            'xmlDocument': xml_boleta,
            'emisor': EMISOR,  # Con la resolución, que se imprime en el PDF.
            'options': {'renderer': {'format': 'pdf'}},
        },
    })
    for archivo in pdf['renderings']:
        guardar_base64(archivo['filename'], archivo['content'])
        print('PDF generado:', archivo['filename'])
else:
    print('La boleta fue rechazada:', estado['status'], estado['description'])
    for detalle in estado['details']:
        print('Folio', detalle['folio'], detalle['status'], detalle['description'])
```

## Probar sin el SII

Si todavía no tienes un CAF y un certificado reales del ambiente de certificación, puedes probar los pasos 1, 2, 3 y 6 con datos de prueba. Los pasos 4 y 5 no funcionan con ellos: el SII rechaza la autenticación y el programa se detiene en el envío.

1. **El certificado:** genera uno de prueba (ver [Un certificado de prueba](https://www.apigateway.cl/academy/emision-de-dte/preparacion/certificado-digital#un-certificado-de-prueba)) y guarda `cert` como `cert.pem` y `pkey` como `key.pem`.
2. **El CAF:** genera uno de prueba y guárdalo como `caf_39.xml`. Ejecuta esto una vez, con las funciones `libredte()` y `guardar_base64()` del programa. El CAF viene en base64 en el campo `xml`, y `guardar_base64()` lo decodifica al guardarlo:

```python
caf = libredte('billing/identifier/caf_faker/create', {
    'emisor': {'rut': '88888888-8', 'razon_social': 'Mi Empresa SpA'},
    'codigoDocumento': 39,
    'folioDesde': 1,
    'folioHasta': 100,
})
guardar_base64('caf_39.xml', caf['xml'])
```

3. **El programa:** borra todo lo que sigue a `# Paso 4: enviar al SII con el servicio de boletas.` y pon en su lugar solo el paso 6, sin la condición de que el SII haya aceptado:

```python
# Paso 6: generar el PDF, sin pasar por el SII.
pdf = libredte('billing/document/renderer/render', {
    'bag': {
        'xmlDocument': xml_boleta,
        'emisor': EMISOR,
        'options': {'renderer': {'format': 'pdf'}},
    },
})
for archivo in pdf['renderings']:
    guardar_base64(archivo['filename'], archivo['content'])
    print('PDF generado:', archivo['filename'])
```

La boleta que obtienes no tiene validez tributaria: el CAF es ficticio. Sirve para revisar los datos, los totales y el PDF.

## Qué hace, en orden

| Paso | Llamada | Qué guarda |
|------|---------|------------|
| 1 | `builder/build` | `boleta_39_1.xml`: la boleta timbrada y firmada |
| 2 | `validator/validateSchema` | (nada; falla si el XML no es válido) |
| 3 | `dispatcher/create` con `receptor` del SII | `sobre_sii_39_1.xml`, un `EnvioBOLETA` |
| 4 | `sii_boleta/sendXmlDocument` | El Track ID |
| 5 | `sii_boleta/checkXmlDocumentSentStatus` | El estado final |
| 6 | `renderer/render` | El PDF (copia tributaria) |

Para llevarlo a tu sistema, sepáralo igual que la factura: emitir y enviar en línea, consultar el estado en segundo plano y generar el PDF cuando la boleta es aceptada (ver [Factura de principio a fin](https://www.apigateway.cl/academy/emision-de-dte/casos-practicos/factura-de-principio-a-fin#para-llevarlo-a-tu-sistema)).

## Boleta exenta

La boleta exenta (`41`) se construye igual, con su propio CAF (`41`). Marca cada ítem como exento con **`IndExe: 1`**:

```json
{
    "Encabezado": {
        "IdDoc": { "TipoDTE": 41, "Folio": 1 },
        "Emisor": {
            "RUTEmisor": "88888888-8",
            "RznSoc": "Mi Empresa SpA",
            "GiroEmis": "Venta de libros",
            "DirOrigen": "Av. Siempre Viva 742",
            "CmnaOrigen": "Providencia"
        },
        "Receptor": {
            "RUTRecep": "66666666-6",
            "RznSocRecep": "Cliente final",
            "DirRecep": "Santiago",
            "CmnaRecep": "Santiago"
        }
    },
    "Detalle": [
        { "IndExe": 1, "NmbItem": "Libro", "QtyItem": 1, "PrcItem": 15000 }
    ]
}
```

La API calcula `MntExe` $15.000 y `MntTotal` $15.000, sin IVA. El sobre, el envío y la consulta del estado son los mismos de la boleta afecta.

## Reglas al agrupar boletas

- Un sobre `EnvioBOLETA` lleva solo boletas (`39` y `41`). Las facturas, notas y guías van en un `EnvioDTE` aparte, que se envía con `sii_dte`.
- Puedes enviar muchas boletas en un sobre con `dispatcher/createMany` (ver [Varios documentos en un sobre](https://www.apigateway.cl/academy/emision-de-dte/emision/paso-3-armar-el-sobre#varios-documentos-en-un-sobre)). Si el SII rechaza alguna, el detalle de la consulta del estado indica su folio.

## Nota de crédito de una boleta

Una boleta se anula o corrige con una nota de crédito (`61`), con referencia `TpoDocRef: 39`. La nota de crédito **no es una boleta**: va en un sobre `EnvioDTE` y se envía con `sii_dte`, como una factura (ver [Nota de crédito](https://www.apigateway.cl/academy/emision-de-dte/casos-practicos/nota-de-credito)).


---
Última actualización el 05/11/2026

