Boletas
La certificación de boletas (boletas electrónicas, tipo 39) es una postulación aparte: su propio portal, su propio set de pruebas, su propio sobre (EnvioBOLETA), su propio reporte de consumo de folios (RCOF) y su propia declaración. No tiene ejecutor: después de que envías el set, el SII lo valida a su ritmo y responde por correo, así que encadenas las piezas tú mismo y vuelves a leer el estado más tarde.
Nada de esto se ha ejecutado todavía contra el portal de boletas. Requiere una empresa inscrita en la postulación de boletas. Cada petición sigue sin comprobarse, y las pruebas usan un texto de set sintético. El sobre de boletas y el reporte de consumo de folios sí validan contra los esquemas oficiales en las pruebas. Ver Estado.
declareReceiptsCompliance no se puede deshacer. Autoriza a la empresa a
emitir boletas en producción. Lee primero el estado y no envía nada cuando el
set aún no está aprobado o la declaración ya se hizo, pero una vez que envía,
es definitiva.
Las piezas, en orden#
Descargar el set de pruebas
downloadReceiptsTestSet(portal, { providerMail }) devuelve el texto del set.
parseReceiptsTestSet(text) lo lee en casos.
Emitir
issueReceiptsSet(cases, options) emite una boleta por caso y firma el
EnvioBOLETA.
Enviar
sendReceiptsSet(sii, issued) sube el sobre, lee su estado y envía el reporte de
consumo de folios. declareReceiptsTrackId(portal, { trackId }) entrega al portal
el track id del sobre.
Esperar y luego leer el estado
El SII valida el set y responde por correo. readReceiptsStatus(portal) te dice
cuándo está aprobado.
Declarar cumplimiento
declareReceiptsCompliance(portal, ...) hace la declaración.
verifyReceiptsAuthorization comprueba el resultado.
Un ejemplo completo#
import { readFileSync } from 'node:fs'
import { createSiiClient, loadCertificateFromPkcs12, parseCaf } from 'dte-sdk'
import {
createPortalClient,
createFolioAllocator,
downloadReceiptsTestSet,
parseReceiptsTestSet,
issueReceiptsSet,
sendReceiptsSet,
declareReceiptsTrackId,
readReceiptsStatus,
declareReceiptsCompliance,
verifyReceiptsAuthorization,
} from 'dte-sdk/portal'
const certificate = loadCertificateFromPkcs12(readFileSync('firma.pfx'), process.env.PFX_PASSWORD!)
const companyRut = '76543210-3'
const portal = createPortalClient({ environment: 'certification', certificate, companyRut })
const sii = createSiiClient({ environment: 'certification', certificate, companyRut })
// La postulación de boletas es aparte: su portal, su set de pruebas, su declaración
const { text } = await downloadReceiptsTestSet(portal, { providerMail: 'contacto@example.test' })
const issued = await issueReceiptsSet(parseReceiptsTestSet(text), {
issuer: { rut: companyRut, businessName: 'Comercial Andina SpA', address: 'Av. Los Aromos 1540' },
certificate,
folios: createFolioAllocator([parseCaf(readFileSync('caf-39.xml', 'utf8'))]),
resolution: { number: 0, date: '2026-01-15' }, // número 0 y la fecha UTC de hoy cuando se omite
})
// El sobre, 20 s, su estado, y luego el reporte de consumo de folios
const sent = await sendReceiptsSet(sii, issued)
sent.outcome // 'sent' | 'envelopeRejected' | 'consumptionFailed'
// envelopeTrackId es null cuando el SII dijo que ya tenía el sobre y no nombró track id
if (sent.envelopeTrackId !== null) await declareReceiptsTrackId(portal, { trackId: sent.envelopeTrackId })
// El SII valida el set por su cuenta y responde por correo. Más tarde:
const status = await readReceiptsStatus(portal) // readyToDeclare una vez aprobado (P90); declared después (P91)
if (status.readyToDeclare)
await declareReceiptsCompliance(portal, {
provider: { rut: companyRut, name: 'Comercial Andina SpA', mail: 'contacto@example.test' },
businessName: 'Comercial Andina SpA',
})
// ¿Se puede timbrar el tipo 39 en producción? ¿Y qué dice la página de avance de certificación?
const check = await verifyReceiptsAuthorization({ production: productionPortal, certification: portal })
check.production?.authorized
check.certification?.state // léelo tú mismo: ver abajoEl set de pruebas#
downloadReceiptsTestSet(portal, { providerMail }) lee primero la lista de representantes y el registro de la postulación (el portal quiere ambos antes de la descarga; sus respuestas no se juzgan), y luego descarga el set con la empresa, el representante que nombran las cookies de sesión rut y dv y el correo del proveedor. No se mira el estado HTTP: cualquier cuerpo de diez caracteres o más es el set. Devuelve { text, representativeRut }. Lanza SII_PORTAL_UNEXPECTED_PAGE cuando la descarga no es un set, y MISSING_FIELD (representative) cuando las cookies de sesión no nombran a ninguno.
parseReceiptsTestSet(text) lee el texto plano de casos, por posición:
- Una línea que empieza con
CASO-<n>abre un caso. - Una línea que nombra
Item,CantidadyPrecio(con esas mayúsculas exactas) activa el modo de ítems de ese caso; un set en mayúsculas no tiene esa cabecera y por lo tanto no tiene ítems. - En modo de ítems, cualquier otra línea no vacía que no sea una regla de
=es un ítem cuando se parte, por tabuladores o grupos de dos o más espacios (sin las partes vacías), en un nombre, una cantidad y un precio. En la cantidad se acepta una coma decimal; el precio descarta todo punto de miles y su primera coma. Un ítem cuyo nombre diceexentoes exento. - Una línea que empieza con
OBSERVACIONentrega su texto, hasta la primera comilla, al caso. - No se guarda nada anterior al primer caso, y un texto sin casos da una lista vacía.
Devuelve ReceiptsTestCase[]: number, items (name, quantity, unitPrice con IVA incluido, exempt) y observations.
issueReceiptsSet#
issueReceiptsSet(cases, { issuer, certificate, folios, resolution?, now? }) emite una boleta tipo 39 por caso, en orden, cada una con el siguiente folio de la fuente (FolioSource), y luego firma el EnvioBOLETA que las lleva.
- El receptor es el consumidor final (
66666666-6). Los precios del set se toman con IVA incluido. Un ítem exento se escribe como exento. Si una observación del caso diceunidad de medida en Kg., esa unidad va en su primer ítem. - Cada boleta lleva una referencia con el código
SETy la razónCASO-<n>, como referencia de boleta. - El
issuertoma los mismos valores por defecto que el ejecutor (DEFAULT_CERTIFICATION_ISSUER); el RUT, la razón social y la dirección no tienen.resolutiones número 0 y la fecha UTC de hoy cuando se omite, campo por campo. - Fechas y marcas de tiempo. Las boletas se fechan con el día UTC y cada una se timbra y se firma con el instante UTC al segundo, sin zona. Solo el sobre lleva la hora de Chile. El reloj se lee una vez para la fecha de emisión, una vez por boleta para sus marcas y una vez para el sobre.
- Quien envía el sobre es el titular del certificado (el emisor cuando el certificado no nombra ninguno); el receptor es el SII.
Devuelve { receipts, envelope, firstFolio, lastFolio }; cada boleta trae caseNumber, folio y el document firmado. Lanza MISSING_FIELD (cases) sin casos y FOLIO_OUT_OF_RANGE desde la fuente de folios. Los XML firmados vienen en el resultado: guárdalos.
sendReceiptsSet#
sendReceiptsSet(sii, issued, { sequence?, sleep?, onProgress? }) recibe el resultado de issueReceiptsSet, o cualquier cosa con su envelope firmado, y hace cuatro cosas:
- Sube el sobre. La respuesta se lee como las subidas de la corrida de certificación: solo
STATUS0 con un elementoTRACKIDes un archivo enviado;STATUS7 con track id también se toma como enviado, y sin él es un duplicado que salta al reporte;STATUS99 y cualquier otra respuesta es una falla y lanzaSII_UPLOAD_REJECTED. Cada subida se repite solo ante una falla de red, con los reintentos de la corrida (8 intentos, 2 s creciendo ×1,8 hasta 15 s). - Espera 20 s y lee su estado una vez (cuando tiene track id). Un rechazo termina aquí como
envelopeRejected. Una consulta que nunca llegó se registra y no bloquea nada; tampoco una respuesta sin estado, o con uno desconocido. - Envía el reporte de consumo de folios (RCOF) armado desde el sobre, con la secuencia
sequencemás el intento.sequencees 100 por defecto (también cuando es 0). Si el SII rechaza un reporte, el intento siguiente toma el siguiente número de secuencia, hasta diez intentos. UnSTATUS7 sin track id es un duplicado: tres duplicados seguidos significan que el reporte de hoy ya se aceptó (el ambiente de certificación recibe uno por empresa por día), y el resultado diceconsumptionAlreadySent. UnSTATUS99 u otro rechazo lo termina comoconsumptionFailed. - Lee el estado del reporte una vez, tras 30 s. Un éxito o un estado intermedio lo acepta, y también una consulta que nunca llegó. Cualquier otra cosa (un rechazo, ningún estado, un estado desconocido) prueba el siguiente número de secuencia, salvo que su glosa hable de autorización o firma, lo que lo termina como
consumptionFailed.
Los reparos del SII al reporte son esperables en certificación y no bloquean nada. Diez intentos sin reporte aceptado terminan igual en sent.
| Campo del resultado | Significado |
|---|---|
outcome | sent: el sobre se subió y no fue rechazado, y el reporte fue aceptado, ya enviado hoy, o con reparos. envelopeRejected. consumptionFailed: un reporte que no se pudo subir, o uno rechazado por falta de autorización |
notAuthorized | La glosa del SII o el error habla de autorización o firma: la empresa aún no puede firmar boletas |
envelopeTrackId, envelopeDuplicate | El track id del sobre, para declarar en el portal (null cuando el SII ya tenía el sobre y no nombró ninguno), y si el SII ya lo tenía |
consumptionTrackId, consumptionSequence | El track id y la secuencia del reporte aceptado, o null |
consumptionAlreadySent | Tres reportes seguidos fueron duplicados |
consumptions | Cada reporte intentado, en orden: sequence, xml, trackId, outcome (accepted, duplicate, rejected, failed) y detail |
detail | La glosa o el error que lo terminó, cuando no terminó en sent |
El track id del sobre viene en el resultado cualquiera sea el desenlace. onProgress recibe BOLETA_SOAP_CHECK y BOLETA_RCOF.
resendReceiptsConsumption(sii, { envelopeXml, sequence }) arma de nuevo el reporte desde un sobre ya enviado y lo sube una vez, con el número de secuencia que le des. STATUS 0 o 7 con un elemento TRACKID es un reporte enviado; un STATUS 7 que no nombra track id, un STATUS 99 y cualquier otra respuesta son rechazos (SII_UPLOAD_REJECTED). Devuelve { sequence, xml, trackId, duplicate }.
declareReceiptsTrackId#
declareReceiptsTrackId(portal, { trackId }) entrega al portal de boletas el track id del sobre, tal como se da, para que el SII valide el set. Se envía una vez. Lanza MISSING_FIELD (trackId) si está vacío y SII_PORTAL_REJECTED (receiptsTrackIdRefused) cuando la respuesta no es //OK. Devuelve la respuesta cruda como { body }.
readReceiptsStatus#
readReceiptsStatus(portal) devuelve { enrolled, readyToDeclare, declared, state }:
enrolled: la empresa tiene representantes vigentes en el portal de boletas.readyToDeclare: el SII aprobó el set (P90): se puede hacer la declaración.declared: la declaración ya se hizo (P91).
P90 y P91 se buscan en la respuesta cruda. El portal responde una lista vacía para el estado 90 tanto antes de la aprobación como después de la declaración, así que la lectura también consulta el estado 91, para distinguir ambos casos.
declareReceiptsCompliance#
declareReceiptsCompliance(portal, { provider, businessName, link? }) hace la declaración de cumplimiento de la postulación de boletas. Lee primero el estado:
outcome | Significado |
|---|---|
declared | La declaración se hizo ahora |
alreadyDeclared | Ya se había hecho antes. No se envía nada |
notReady | El SII aún no aprueba el set. No se envía nada |
- El proveedor de software es obligatorio (
provider:rut,nameymail, cada uno requerido): el SII respondió a algunas empresas con un HTTP 500 pelado cuando se declararon por defecto como su propio proveedor. Una empresa que es su propio proveedor se pasa a sí misma.businessNametambién es obligatorio;linkeswww.sii.clpor defecto. - El representante es el dueño de la sesión, leído de las cookies de la sesión; la lista del propio portal es solo el respaldo. Cuando ninguna cookie trae el dígito verificador, la declaración envía
8. Con dos personas inscritas para una empresa, cualquier otra elección se rechaza. Si no se encuentra ningún representante lanzaMISSING_FIELD(representative). - No se envía nada sin el Long propio de la postulación. Un registro que no lo trae lanza
SII_PORTAL_UNEXPECTED_PAGE. Un registro sin fecha de autorización se declara con la fecha vacía. - El envío que declara se juzga solo por
//OK; si no,SII_PORTAL_REJECTED(receiptsComplianceRefused). Una empresa sin representantes esreceiptsNotEnrolled. - La declaración se fecha con la fecha local del reloj del proceso, así que corre el proceso con
TZ=America/Santiago.
verifyReceiptsAuthorization#
verifyReceiptsAuthorization({ production?, certification?, companyRut? }) lee si la empresa puede emitir boletas, en uno de los lados o en ambos. Un lado es null cuando no entregas su cliente.
- Producción (
production, unPortalClienten producción): un GET de la página de entrada del timbraje y un POST de su formulario con la empresa.authorizedes true cuando el tipo 39 está entre los tipos de documento que ofrece el formulario (types). - Certificación (
certification): un GET de la página de avance.authorizedes true cuando la fila de boletas dice conforme, aprobado o autorizado, o una línea de la página dice que las boletas están autorizadas. La verificación lee una fila como autorizada cuando su estado contieneconform,aprobadoautoriz: lee tú mismo elstateque devuelve.
Nunca lanza. Una consulta que el portal no responde es queryFailed: true (con errorCode y error), nunca "no autorizada": un formulario de timbraje que el portal no sirve no dice nada sobre la autorización.
Cómo se llama al portal de boletas#
Cada petición al portal de boletas se escribe solo para él: únicamente las cabeceras que nombra la llamada y las cookies de la sesión, enviada una sola vez (sin reintento), con un timeout de 30 s (20 s para verifyReceiptsAuthorization), y leída como UTF-8 sea cual sea su estado, incluida la página del firewall. No hay calentamiento, ni búsqueda de hashes, ni redirección. Los envíos que declaran se juzgan por //OK. Los hashes del portal son el par que se sabe que funciona (RECEIPTS_PORTAL.known); pasa gwt: { permutation, policy } en la entrada de una operación si quedan obsoletos. Pasa sessionChecked: true cuando una operación anterior de la misma corrida ya comprobó la sesión guardada. Cada operación recibe ajustes opcionales de la llamada como último argumento.
RECEIPTS_PORTAL es su entrada GwtPortal. Sus hashes no se descubren, y su moduleName es lo que sugiere su interfaz, no un dato registrado.
La referencia propia de una boleta#
El esquema de boletas no le da fecha a Referencia y su código es texto libre, así que una boleta recibe receiptReferences ({ code, reason, documentType?, folio? }) y rechaza references. Eso es lo que escribe issueReceiptsSet, con el código SET y la razón CASO-<n>. Ver Documentos.
Próximos pasos#
- La certificación de las facturas: Sets de pruebas y El ejecutor.
- Emitir boletas fuera de la certificación: Ejemplo de boleta.
- El reporte de consumo de folios: Libros.