Sobres
El SII no recibe un documento suelto: recibe un sobre (EnvioDTE, o EnvioBOLETA para boletas) que contiene uno o más documentos firmados bajo una carátula (Caratula) y que va firmado a su vez. createEnvelope lo construye desde tus documentos firmados y los datos de la carátula, signEnvelope lo firma con el certificado de quien lo envía, y encodeLatin1 convierte el XML en los bytes ISO-8859-1 que necesita el envío.
Esta página también cubre los helpers de serialización (codificación de texto y helpers XML) porque el sobre es donde importan. Todo vive en la entrada dte-sdk.
Los sobres validan contra los esquemas oficiales EnvioDTE y EnvioBOLETA
en los tests, pero ninguno se ha enviado todavía al SII real. Mira
Estado.
Uso#
import { readFileSync } from 'node:fs'
import {
createEnvelope,
encodeLatin1,
issueDocument,
loadCertificateFromPkcs12,
parseCaf,
signEnvelope,
verifyEnvelopeSignature,
} from 'dte-sdk'
const certificate = loadCertificateFromPkcs12(readFileSync('firma.pfx'), process.env.PFX_PASSWORD!)
const caf = parseCaf(readFileSync('caf-33.xml', 'utf8'))
const invoice = issueDocument({ document: input, caf, certificate })
const envelope = signEnvelope(
createEnvelope({
kind: 'dte',
documents: [invoice],
cover: {
issuerRut: '76543210-3',
senderRut: certificate.rut!,
resolutionDate: '2026-10-31',
resolutionNumber: 0,
},
}),
certificate,
)
envelope.setId // 'DTE_SetDoc_15_11_2026_15_30_00_000'
envelope.cover.subtotals // [{ type: 33, count: 1 }]
envelope.xml // '<?xml version="1.0" encoding="ISO-8859-1"?>\n<EnvioDTE …'
verifyEnvelopeSignature(envelope.xml).valid // true
const bytes = encodeLatin1(envelope.xml) // los bytes del archivo que recibe el SIIPara enviarlo, entrégale el sobre firmado al cliente del SII (mira Servicios web del SII).
Crear un sobre#
createEnvelope({ kind, documents, cover, now? }) agrupa documentos firmados bajo una carátula. Valida el tipo de sobre, normaliza los RUT y la fecha de resolución, cuenta los documentos por tipo para la carátula (SubTotDTE) y genera el id del set. Devuelve un Envelope (kind, setId, cover, documents), todavía sin firmar.
Tipos de sobre#
kind | Elemento raíz | Contiene | Esquema |
|---|---|---|---|
'dte' | EnvioDTE | Facturas, liquidaciones y facturas de compra, guías y notas (33, 34, 43, 46, 52, 56, 61). | EnvioDTE_v10.xsd |
'receipt' | EnvioBOLETA | Boletas (39 y 41). | EnvioBOLETA_v11.xsd |
envelopeRootName(kind) devuelve el nombre del elemento raíz de cada tipo.
Las boletas y el resto nunca viajan juntos. Un sobre 'dte' con una boleta, o un sobre 'receipt' con una factura, lanza ENVELOPE_KIND_MISMATCH (details: kind y el type que sobra). Emite las boletas en un sobre y los demás documentos en otro. Una lista documents vacía lanza ENVELOPE_EMPTY.
La carátula#
cover es un EnvelopeCoverInput:
| Campo | Etiqueta SII | Obligatorio | Significado |
|---|---|---|---|
issuerRut | RutEmisor | sí | RUT de la empresa emisora. |
senderRut | RutEnvia | sí | RUT de quien firma y envía el sobre: el titular del certificado (certificate.rut). |
resolutionDate | FchResol | sí | Fecha de la resolución del SII que te autorizó como emisor. AAAA-MM-DD o DD-MM-AAAA. |
resolutionNumber | NroResol | sí | Número de esa resolución. Es 0 en el ambiente de certificación. |
receiverRut | RutReceptor | no | RUT del receptor del sobre. Por defecto, el SII (60803000-K, SII_RUT). Siempre el SII en los sobres de boletas. |
signedAt | TmstFirmaEnv | no | Marca de tiempo de la firma, AAAA-MM-DDTHH:MM:SS. Por defecto, ahora en hora de Chile. |
setId | SetDTE/@ID | no | Identificador del set. Se genera cuando se omite. |
La EnvelopeCover normalizada que vuelve tiene todos ellos, con los RUT en forma SII (76543210-3), resolutionDate como AAAA-MM-DD y subtotals, la cantidad de documentos de cada tipo ({ type, count }, TpoDTE y NroDTE).
En un sobre 'dte' los subtotales listan 33, luego 61, luego 56, y después los demás tipos en orden ascendente. Un sobre 'receipt' lista los tipos en el orden en que aparecen por primera vez.
Las marcas de tiempo son hora de Chile (America/Santiago), nunca UTC: un TmstFirmaEnv armado con el reloj UTC deja el sobre unas tres horas en el futuro ("Error en Carátula"). El SDK lo arma con la hora de Chile de now, así que no tienes que hacerlo tú.
La resolución (fecha y número) pertenece a la empresa y al ambiente: el SII te da una cuando te autoriza como emisor, y el número es 0 en certificación. El SDK solo comprueba que senderRut sea un RUT válido; usa el RUT del titular del certificado que firma el sobre.
El id del set#
generateSetId(prefix, now?) construye el identificador del SetDTE: PREFIJO_dd_mm_aaaa_hh_mm_ss_mmm, en hora de Chile:
import { generateSetId } from 'dte-sdk'
generateSetId('LOTE', new Date('2026-11-15T18:30:05.007Z')) // 'LOTE_15_11_2026_15_30_05_007'createEnvelope lo usa con el prefijo DTE_SetDoc en un sobre 'dte' y ENVIOBOLETA en uno 'receipt', salvo que pases cover.setId. El id es único dentro de un proceso (lleva los milisegundos); si varios procesos arman sobres en el mismo instante, o quieres encontrar después un sobre por su id, entrega tu propio setId.
Errores#
| Código | Cuándo |
|---|---|
ENVELOPE_EMPTY | No hay documentos. |
ENVELOPE_KIND_MISMATCH | Una boleta en un sobre 'dte', o cualquier otro documento en un sobre 'receipt'. |
INVALID_RUT | issuerRut, senderRut o receiverRut no es un RUT válido (details.field es cover.issuerRut y así). |
INVALID_DATE | resolutionDate no es una fecha real. |
Renderizar y firmar#
signEnvelope(envelope, certificate) renderiza el XML del sobre y firma el elemento SetDTE con el certificado de quien envía. Devuelve un SignedEnvelope: el sobre más xml (el XML firmado, con su declaración XML) y signature ({ digestValue, signatureValue }).
buildEnvelopeXml(envelope) renderiza el XML sin firma, que es lo que llama signEnvelope:
- Abre con la declaración
<?xml version="1.0" encoding="ISO-8859-1"?>. - El raíz (
EnvioDTEoEnvioBOLETA) lleva el namespace del SII,xmlns:xsi, elxsi:schemaLocationdel esquema yversion="1.0". SetDTElleva elIDy contiene laCaratulay los documentos firmados, uno por línea.- Cada documento conserva sus propios namespaces, porque su digest depende de ellos, y pierde su propia declaración si la tenía.
Los documentos de adentro son exactamente el xml de cada SignedDocument: no se vuelven a firmar, y cualquier cambio en ellos después de firmar rompe su firma.
verifyEnvelopeSignature(xml) verifica la firma del SetDTE de un XML EnvioDTE o EnvioBOLETA, propio o recibido, y devuelve la misma SignatureVerification que verifyDocumentSignature: referenceId (el id del set), digestValid, signatureValid y valid. Lanza XML_PARSE_FAILED para un XML mal formado o un raíz incorrecto y SIGNATURE_NOT_FOUND cuando no hay firma. Cambiar cualquier valor de la carátula (el número de resolución, por ejemplo) la rompe. No verifica las firmas de los documentos de adentro: hazlo con verifyDocumentSignature sobre cada xml. Para un sobre que envió un proveedor, verifyReceivedDocuments revisa el sobre y cada documento de adentro, firma, timbre y CAF, en una sola llamada (revisa Verificar documentos recibidos).
Mezclar boletas y documentos#
Los dos tipos son sobres distintos con canales distintos. Un sobre de boletas (EnvioBOLETA) siempre va dirigido al SII y usa el prefijo de set ENVIOBOLETA:
const receipts = signEnvelope(
createEnvelope({
kind: 'receipt',
documents: [receipt1, receipt2],
cover: { issuerRut: '76543210-3', senderRut: certificate.rut!, resolutionDate: '2026-10-31', resolutionNumber: 0 },
}),
certificate,
)
receipts.cover.receiverRut // '60803000-K', aunque la carátula dijera otra cosaSi un mismo lote de trabajo tiene facturas y boletas, separa documents con isReceipt(document.header.type) y arma un sobre para cada tipo.
Serializar para el envío#
El SII lee el envío como bytes ISO-8859-1. Eso tiene tres consecuencias, y el SDK se hace cargo de las dos primeras:
- El texto de un documento debe caber en ISO-8859-1.
createDocumentelimina de los textos libres todo carácter sobre U+00FF (emoji, comillas curvas). Las tildes y lañdel español están dentro de ISO-8859-1 y se mantienen. - El XML debe comenzar con una declaración que nombre ISO-8859-1. El sobre y los libros que arma el SDK la llevan (
<?xml version="1.0" encoding="ISO-8859-1"?>). Las funciones de envío la comprueban y rechazan cualquier otra cosa conINVALID_OPTIONydetails.field: 'xml'antes de enviar un solo byte; el espacio antes de la declaración se descarta. Sin la declaración, XML asume UTF-8 y el SII leería un texto distinto del que se firmó. - Los bytes son uno por carácter.
encodeLatin1(text)devuelve unUint8Arraycon los bytes ISO-8859-1;decodeLatin1(bytes)hace el camino inverso (un carácter por byte).
import { decodeLatin1, encodeLatin1 } from 'dte-sdk'
encodeLatin1('Ñuñoa') // Uint8Array [209, 117, 241, 111, 97]
decodeLatin1(new Uint8Array([209, 117, 241, 111, 97])) // 'Ñuñoa'
encodeLatin1('precio €') // lanza ENCODING_UNSUPPORTED_CHARencodeLatin1 lanza ENCODING_UNSUPPORTED_CHAR con details { index, codePoint } para un carácter sobre U+00FF. Nunca lo trunca a su byte bajo, que es como llegaban caracteres de control corruptos al SII. Todavía puede pasar si pones texto en un campo que createDocument no sanitiza (un correo, una opción, un código de ítem): pásale sanitizeText, o corrige el texto, antes de firmar, porque la firma cubre el texto tal como está.
El XML del sobre firmado ya es seguro para Latin-1 cuando los documentos vienen de createDocument: encodeLatin1(envelope.xml).length es igual a envelope.xml.length. Guarda o envía los bytes, no el string de JavaScript. Escribir el archivo con writeFileSync(path, envelope.xml) lo codifica como UTF-8 y rompe todas las tildes.
import { writeFileSync } from 'node:fs'
writeFileSync('envio.xml', encodeLatin1(envelope.xml))Helpers XML#
El SDK construye y lee su XML con unos pocos helpers pequeños, exportados porque las respuestas del SII (SOAP, XML en forma de texto) necesitan el mismo trabajo:
| Función | Qué hace |
|---|---|
buildXml(object) | Serializa un objeto plano a XML compacto. Las claves son elementos en orden de inserción, las claves @_name son atributos, los arreglos repiten el elemento y los valores se escapan. |
parseXml(xml) | Parsea XML a un objeto plano: los elementos son claves, los repetidos son arreglos, los atributos son claves @_name y todo valor queda como string (007 queda '007'). Lanza XML_PARSE_FAILED cuando el XML no está bien formado. |
parseXmlWithoutNamespaces(xml) | Como parseXml, sin los prefijos de namespace (SII:RESPUESTA pasa a RESPUESTA). Útil para respuestas SOAP. |
extractTagContent(xml, tag) | El texto de la primera ocurrencia de <tag>…</tag>, o null. Por expresión regular, para respuestas planas del SII: no maneja atributos ni anidación. |
decodeXmlEntities(xml) | Decodifica las cinco entidades XML. Las respuestas SOAP suelen traer XML escapado. |
escapeXmlText(value) | Escapa un valor de texto con las cinco entidades XML (&, <, >, ", '). |
escapeXmlAttribute(value) | Escapa un valor para un atributo entre comillas dobles (&, <, "). |
wrapBase64(value, width?) | Quita los espacios de un base64 y lo corta en columnas de width (64 por defecto). |
formatBase64Columns(xml) | Recorta SignatureValue, Modulus y X509Certificate a 64 columnas, la disposición que espera el SII. |
import { buildXml, extractTagContent, parseXml } from 'dte-sdk'
buildXml({ Envio: { '@_version': '1.0', Linea: ['a', 'b&c'], Total: 10 } })
// '<Envio version="1.0"><Linea>a</Linea><Linea>b&c</Linea><Total>10</Total></Envio>'
parseXml('<A v="1"><B>x</B><B>y</B></A>') // { A: { B: ['x', 'y'], '@_v': '1' } }
extractTagContent('<RESP><TRACKID>123456</TRACKID></RESP>', 'TRACKID') // '123456'Helpers de texto y de hora#
| Función | Qué hace |
|---|---|
sanitizeText(text) | Normaliza texto libre para un campo del DTE: quita apóstrofes y comillas dobles, pasa guiones y puntos suspensivos tipográficos a ASCII, elimina lo que está fuera de ISO-8859-1 y recorta espacios. |
foldToAscii(text) | Pliega el texto a ASCII imprimible (á pasa a a, ß a ss), como necesita el TED. |
truncateText(text, maxLength, preserveWords?) | Sanitiza y corta el texto a maxLength; con preserveWords retrocede al último espacio si queda después del 70 % del límite. |
assertMaxLength(value, maxLength, field) | Lanza FIELD_TOO_LONG cuando un texto supera el límite. |
chileDate(now?) | La fecha de hoy en Chile, AAAA-MM-DD. |
chileTimestamp(now?) | AAAA-MM-DDTHH:MM:SS en hora de Chile, sin zona ni milisegundos (TmstFirma, TmstFirmaEnv, TSTED). |
chileDateTimeParts(now?) | El año, mes, día, hora, minuto y segundo con ceros a la izquierda de un instante en Chile. |
toIsoDate(value, field?) | Normaliza DD-MM-AAAA, DD/MM/AAAA o AAAA-MM-DD a AAAA-MM-DD. Lanza INVALID_DATE. |
isIsoDate(value) | true para una fecha de calendario real en AAAA-MM-DD. |
toSiiRut(value, field?), isSiiRut(value), splitSiiRut(rut) | Helpers de RUT sobre rut.ts: validan y formatean como 12345678-5, comprueban la forma, separan cuerpo y dígito verificador. |
Los helpers de fecha y hora usan la zona horaria de Chile mediante Intl, nunca la zona horaria del proceso.
Siguientes pasos#
- Servicios web del SII: envía el sobre y sigue su estado.
- Firma: el certificado y las firmas detrás de
signEnvelope. - Libros: los libros del SII van en su propio XML firmado.
- Certificación: los sets de pruebas también se envían como sobres.