Saltar al contenido
dte-sdk

Libros del SII

El SDK arma tres documentos XML que el SII llama libros: el libro de compras y ventas (LibroCompraVenta, el IECV), el libro de guías de despacho (LibroGuia) y el reporte de consumo de folios (ConsumoFolios, el RCOF). Tú entregas las líneas, el SDK calcula los resúmenes, renderiza el XML en el formato que valida el SII, lo firma con el certificado de quien envía y, con el cliente de servicios web, lo sube.

Hoy su uso es acotado. El registro de compras y ventas (RCV) reemplazó al libro de compras y ventas en producción desde el período 2017-08, y el libro de guías se envía solo cuando el SII lo pide; ambos siguen formando parte del set de pruebas de certificación. El reporte de consumo de folios, hoy llamado Resumen de Ventas Diarias, se usa en la certificación de boletas, y probablemente el SII ya no lo exige en producción. Ver Certificación.

Los libros aún no se han enviado al SII real. En las pruebas validan contra los esquemas XML oficiales, y su XML coincide byte a byte con archivos golden. La 0.8.0 de dte-sdk está en npm, la versión más reciente. Todavía no se recomienda para producción: la mayoría de las áreas no se ha probado contra el SII real, y hasta la 1.0.0 la API puede cambiar en cualquier versión. Revisa Estado.

Uso#

Un libro de ventas de un mes, armado desde los documentos que emitiste, firmado y subido:

TypeScript
import {
  bookEntryFromDocument,
  createPurchaseSalesBook,
  signPurchaseSalesBook,
  uploadBook,
} from 'dte-sdk'
 
const book = createPurchaseSalesBook({
  cover: {
    issuerRut: '76543210-3', // la empresa dueña del libro
    senderRut: certificate.rut!, // quien firma y envía
    period: '2026-10',
    resolutionDate: '2026-01-15',
    resolutionNumber: 0, // 0 en certificación
    operation: 'sale',
  },
  entries: invoices.map((document) =>
    bookEntryFromDocument(document, { bookRut: '76543210-3', operation: 'sale' }),
  ),
  // Las boletas no se listan línea por línea: van como totales.
  summaryOnlyTotals: [{ type: 39, documentCount: 120, netAmount: 840000, vatAmount: 159600, totalAmount: 999600 }],
})
 
book.periodTotals // una fila por tipo de documento, calculada desde las líneas y los totales de solo resumen
book.totalMismatches // totales explícitos de línea que difieren de la fórmula del SII (vacío aquí)
 
const signed = signPurchaseSalesBook(book, certificate)
const receipt = await uploadBook(sii, { xml: signed.xml })
receipt.trackId // síguelo con waitForUploadStatus

createPurchaseSalesBook valida y normaliza, signPurchaseSalesBook devuelve el libro más xml (con el prólogo ISO-8859-1, listo para subir) y los valores de la signature, y uploadBook lo envía. Toda función es pura, salvo el envío.

Los tres libros de un vistazo#

LibroArmarFirmarSubirEnvíos
Compras y ventas (LibroCompraVenta)createPurchaseSalesBooksignPurchaseSalesBookuploadBooktotal, partial, final, adjustment
Guías de despacho (LibroGuia)createGuideBooksignGuideBookuploadBooktotal, partial, final, adjustment
Consumo de folios (ConsumoFolios)createFolioConsumptionsignFolioConsumptionuploadFolioConsumptionninguno: se reenvía el reporte completo

Cada uno tiene una función build…Xml que renderiza el XML sin firma (buildPurchaseSalesBookXml, buildGuideBookXml, buildFolioConsumptionXml) y una función verify…Signature que verifica la firma de un XML (verifyPurchaseSalesBookSignature, verifyGuideBookSignature, verifyFolioConsumptionSignature). La firma es una XMLDSig envuelta sobre el elemento EnvioLibro (o DocumentoConsumoFolios), referenciado por su ID.

toSiiPurchaseSalesBook, toSiiGuideBook y toSiiFolioConsumption convierten un libro en un objeto con las etiquetas oficiales del SII (Caratula, Detalle, ResumenPeriodo…), por si quieres ver o procesar lo que se renderiza.

El XML renderizado parte con una declaración ISO-8859-1, que uploadBook exige. Además pone un salto de línea entre cada par de etiquetas adyacentes, porque el SII rechaza las líneas de más de unos 4 090 caracteres (CHR-00002).

El libro de compras y ventas#

La carátula#

CampoObligatorioSignificado
issuerRutsíRUT de la empresa dueña del libro (RutEmisorLibro).
senderRutsíRUT de quien envía, normalmente el titular del certificado (RutEnvia).
periodsíPeríodo tributario, AAAA-MM. En un libro especial, el primero que incluye.
resolutionDatesíFecha de la resolución del SII, AAAA-MM-DD o DD-MM-AAAA.
resolutionNumbersíNúmero de resolución, un entero de 0 a 999 999. 0 en certificación.
operationsí'sale' (VENTA) o 'purchase' (COMPRA).
kindno'monthly' (MENSUAL, el valor por defecto), 'special' (ESPECIAL) o 'rectification' (RECTIFICA).
submissionno'total' (por defecto), 'partial', 'final' o 'adjustment'.
segmentNumberen partialNúmero del segmento. Se emite solo en un envío parcial.
notificationFolioen specialFolio de la notificación del SII que pide el libro.
rectificationCodeen rectificationEl código de autorización del reemplazo, hasta 10 caracteres (CodAutRec).

Un campo obligatorio faltante es MISSING_FIELD { field: 'cover.…' }, un período o una fecha inválidos son INVALID_DATE, un RUT inválido es INVALID_RUT.

Tipos y envíos#

El tipo (kind) dice qué es el libro: el del mes, uno especial que pidió el SII, o el reemplazo de uno ya enviado. El envío (submission) dice cómo llega al SII. Un mes puede llegar de una vez o por partes, y un mes cerrado se puede corregir después:

EnvíoQué llevaResúmenes que obtiene
totalTodas las líneas del período, en un solo libro.Resumen del período.
partialUn segmento de las líneas, con su segmentNumber. Necesita líneas; no acepta totales previos ni totales de solo resumen.Solo resumen del segmento.
finalEl cierre de los segmentos: sin líneas; necesita previousTotals (la suma de los segmentos) o summaryOnlyTotals.Resumen del período.
adjustmentLíneas que agregan o eliminan sobre un libro ya enviado (adjustment: 'add' o 'remove' en cada línea; add es el valor por defecto).Resumen del segmento (el ajuste con signo) y resumen del período.

Las zonas se validan antes de renderizar nada: un parcial sin líneas es MISSING_FIELD { field: 'entries' }; un final con líneas es INVALID_OPTION { field: 'entries' }; previousTotals en algo que no sea un final o un ajuste es INVALID_OPTION { field: 'previousTotals' }; summaryOnlyTotals en un parcial es INVALID_OPTION { field: 'summaryOnlyTotals' }; un final sin totales previos ni de solo resumen es MISSING_FIELD { field: 'previousTotals' }. Un libro special sin notificationFolio o uno rectification sin rectificationCode es MISSING_FIELD.

Las líneas#

Cada elemento de entries es un documento, con los nombres en inglés del SDK (la etiqueta SII en español que produce cada uno está en el JSDoc). Los que más usarás:

CampoSignificado
type, folioTipo de documento (ver BOOK_DOCUMENT_TYPES) y folio.
dateFecha del documento, AAAA-MM-DD o DD-MM-AAAA, de 2000 a 2050.
counterpartyRut, counterpartyNameEl cliente en un libro de ventas, el proveedor en uno de compras. El nombre tiene como máximo 50 caracteres.
exemptAmount, netAmount, vatAmountMontos. En un libro de ventas parten en 0; en uno de compras vatAmount es la parte recuperable.
taxRateTasa de IVA en porcentaje. Se completa con la tasa de IVA cuando una línea tiene neto e IVA y no tiene tasa.
otherTaxesImpuestos adicionales y retenciones: { code, rate?, amount }, hasta 20.
nonRecoverableVatIVA no recuperable en compras: { code, amount }, hasta 5 (ver NON_RECOVERABLE_VAT_CODES).
commonUseVat, fixedAssetNetAmount, fixedAssetVatAmount, nonCreditableTax…Lo propio del lado de compras.
totalVatWithheld, partialVatWithheld, constructionCredit, containerDeposit, settlement…Retenciones, créditos y comisiones de las facturas de liquidación.
totalAmountEl total de la línea. Se calcula con la fórmula del SII si falta; se conserva y se compara si viene.
voidedtrue para un folio anulado antes de llegar al SII: solo viajan tipo y folio y no suma montos.
adjustment'add' o 'remove' en un envío de ajuste.

El total de la línea sigue la fórmula del formato del SII para cada lado del libro: neto más exento más IVA más impuestos adicionales, menos el IVA retenido y así, y en compras además el IVA de uso común, el IVA no recuperable y el impuesto sin derecho a crédito. Los impuestos de retención no suman al total de una compra, y el código 60 (retención de factura inicial) resta.

Una línea inválida es INVALID_DOCUMENT_TYPE, INVALID_FOLIO, INVALID_AMOUNT, INVALID_DATE, INVALID_RUT, INVALID_OPTION o FIELD_TOO_LONG, con details.index indicando cuál línea.

Totales explícitos: totalMismatches y strictTotals#

Si pasas un totalAmount distinto del que da la fórmula, el SDK conserva tu valor (el SII acepta algunos de esos) e informa la diferencia, para que la decisión siga siendo tuya:

TypeScript
const book = createPurchaseSalesBook({
  cover,
  entries: [{ type: 33, folio: 101, netAmount: 100000, vatAmount: 19000, totalAmount: 120000 }],
})
book.totalMismatches // [{ index: 0, type: 33, folio: 101, expected: 119000, actual: 120000 }]

Con strictTotals: true la misma entrada lanza en cambio BOOK_TOTAL_MISMATCH, con details: { index, expected, actual } de la primera diferencia.

Resúmenes y arrastre de totales entre envíos#

El SDK calcula los resúmenes desde las líneas: una fila por tipo de documento, en el orden de la primera aparición.

  • segmentTotals es el resumen de las líneas propias de este envío. Existe en los envíos partial y adjustment que tienen líneas; en los demás es null.
  • periodTotals es el resumen de todo el período: las líneas, más summaryOnlyTotals, más previousTotals. Es null en un partial y cuando no hay nada que sumar.
  • summaryOnlyTotals lleva lo que el formato excluye del detalle: las boletas y algunos otros tipos (SUMMARY_ONLY_DOCUMENT_TYPES: 35, 38, 39, 41, 48, 105, 500, 501, 919, 920, 922 y 924). Entrega los totales de ese tipo como { type, documentCount, netAmount, vatAmount, totalAmount… }; lo que omitas cuenta como 0.

periodTotals es un valor JSON simple. Guárdalo después de cada envío y devuélvelo como previousTotals en el siguiente; accumulateBookTotals(base, delta) suma dos listas de filas por tipo de documento (y, dentro de un tipo, el IVA no recuperable y otros impuestos por código):

TypeScript
import { accumulateBookTotals, createPurchaseSalesBook } from 'dte-sdk'
 
const cover = { issuerRut: '76543210-3', senderRut: '12345678-5', period: '2026-10', resolutionDate: '2026-01-15', resolutionNumber: 0, operation: 'sale' as const }
 
// Dos segmentos del mes…
const first = createPurchaseSalesBook({ cover: { ...cover, submission: 'partial', segmentNumber: 1 }, entries: firstHalf })
const second = createPurchaseSalesBook({ cover: { ...cover, submission: 'partial', segmentNumber: 2 }, entries: secondHalf })
 
// …y el cierre, sin líneas, que arrastra lo que sumaron los segmentos.
const final = createPurchaseSalesBook({
  cover: { ...cover, submission: 'final' },
  previousTotals: accumulateBookTotals(first.segmentTotals!, second.segmentTotals!),
  summaryOnlyTotals: [{ type: 39, documentCount: 120, netAmount: 840000, vatAmount: 159600, totalAmount: 999600 }],
})
final.periodTotals // el mes completo

Un ajuste firma sus líneas (remove cuenta en negativo) y las suma encima de los totales previos, así que su periodTotals es el mes corregido.

IVA de uso común y factores#

Cuando un resumen del período trae IVA de uso común, el libro necesita el factor de proporcionalidad: pasa commonUseVatFactor (0,001 a 99,999, con a lo más 3 decimales); sin él, la construcción falla con MISSING_FIELD { field: 'commonUseVatFactor' }. additionalTaxFactors asocia el código de un impuesto adicional con su factor (0,001 a 1, con a lo más 4 decimales). Un factor fuera de rango es INVALID_OPTION.

Ids y reloj#

El ID del EnvioLibro se genera (LCV_<dd>_<mm>_<aaaa>_<hh>_<mm>_<ss>_<ms> en hora de Chile, a partir de now) salvo que pases id. now también fija la marca de tiempo de la firma (signedAt). Pasa ambos para que una construcción sea reproducible en pruebas.

Derivar líneas desde documentos#

Rara vez escribirás las líneas a mano. Tres funciones las derivan desde documentos emitidos con este SDK:

TypeScript
import {
  bookEntryFromDocument,
  folioConsumptionEntryFromDocument,
  guideEntryFromDocument,
} from 'dte-sdk'
 
bookEntryFromDocument(document, { bookRut: '76543210-3', operation: 'sale' })
bookEntryFromDocument(received, { bookRut: '76543210-3', operation: 'purchase', creditNoteSign: 'negative' })
guideEntryFromDocument(guide) // una guía de despacho (52)
folioConsumptionEntryFromDocument(receipt) // una boleta (39 o 41)
  • bookEntryFromDocument recibe el documento y { bookRut, operation, creditNoteSign? }. La contraparte es el receptor cuando el dueño del libro emitió el documento y el emisor en caso contrario, lo que cubre ventas, compras recibidas y facturas de compra (46) en cualquier lado. El nombre se corta a 50 caracteres, porque el campo del libro es más corto que el del documento. creditNoteSign: 'negative' niega los montos de una nota de crédito (61); el valor por defecto, 'positive', es como lo dice el formato. No se infieren, y quedan para que los agregues al resultado cuando el caso los necesite: issuedByBookOwner, purchaseInvoice y la referencia al documento anulado.
  • guideEntryFromDocument recibe una guía de despacho; cualquier otro tipo es INVALID_DOCUMENT_TYPE. El traslado es venta por defecto, y el receptor y los montos viajan solo en ventas: una guía que no es venta informa únicamente su total.
  • folioConsumptionEntryFromDocument recibe una boleta; cualquier otro tipo es INVALID_DOCUMENT_TYPE.

El libro de guías de despacho#

TypeScript
import { createGuideBook, guideEntryFromDocument, signGuideBook, uploadBook } from 'dte-sdk'
 
const guideBook = createGuideBook({
  cover: {
    issuerRut: '76543210-3',
    senderRut: certificate.rut!,
    period: '2026-10',
    resolutionDate: '2026-01-15',
    resolutionNumber: 0,
    notificationFolio: 3, // siempre obligatorio: el folio de la notificación del SII
  },
  entries: guides.map((guide) => guideEntryFromDocument(guide)),
})
 
await uploadBook(sii, { xml: signGuideBook(guideBook, certificate).xml }) // se envía como LibroGuia.xml

La carátula es la misma del libro de compras y ventas, sin operation ni kind (el esquema solo admite el tipo especial), y con notificationFolio obligatorio en todo libro de guías. Los envíos funcionan igual, incluidos segmentNumber en un parcial, previousTotals en un final o un ajuste y adjustment: 'add' | 'remove' en las líneas.

Una línea es { folio, voided?, adjustment?, transferType?, date?, receiverRut?, receiverName?, netAmount?, taxRate?, vatAmount?, totalAmount?, modifiedAmount?, referenceType?, referenceFolio?, referenceDate? }:

  • transferType es la operación, de 1 a 7 (GUIDE_TRANSFER_TYPES: 1 venta, 2 ventas por efectuar, 3 consignaciones, 4 productos en demostración, 5 traslados internos, 6 otros traslados no venta, 7 guía de devolución). El esquema llega hasta 7, así que los valores 8 y 9 de IndTraslado de la guía no se pueden informar en una línea.
  • voided es 'folio' (anulado antes de llegar al SII), 'guide' (anulada después) o 'partialReceipt' (productos recibidos en parte, con el modifiedAmount corregido). Los dos primeros descartan los montos y el receptor; partialReceipt los conserva (GUIDE_VOID_CODES: 1, 2 y 3).
  • vatAmount se calcula desde la tasa y el neto cuando falta o es 0, y totalAmount es neto más IVA cuando falta. La tasa y el IVA se emiten siempre, en 0 si faltan.

El resumen (GuideTotals) es { voidedFolios, voidedGuides, saleGuides, saleGuidesAmount, modifiedAmount, transfers }, donde transfers trae un { type, count, amount } por cada operación que no es venta. Como el del libro de compras y ventas, es JSON simple: guarda periodTotals, devuélvelo como previousTotals y suma dos con accumulateGuideTotals(base, delta).

El reporte de consumo de folios (RCOF)#

El reporte dice qué folios de boleta usó una empresa en un rango de días. Se arma desde las boletas que emitiste (tipos 39 y 41):

TypeScript
import {
  createFolioConsumption,
  folioConsumptionEntryFromDocument,
  signFolioConsumption,
  uploadFolioConsumption,
} from 'dte-sdk'
 
const report = createFolioConsumption({
  cover: {
    issuerRut: '76543210-3',
    senderRut: certificate.rut!,
    resolutionDate: '2026-01-15',
    resolutionNumber: 0,
    // from, to: por defecto, la fecha de emisión más temprana y la más tardía de las líneas
    // sequence: 1 por defecto
  },
  entries: receipts.map((receipt) => folioConsumptionEntryFromDocument(receipt)),
  voidedFolios: [{ type: 39, folio: 52 }], // folios anulados en tu sistema antes de llegar al SII
})
 
report.summaries // uno por tipo de boleta presente, primero el 39
await uploadFolioConsumption(sii, { xml: signFolioConsumption(report, certificate).xml })
  • Fechas. from y to (FchInicio, FchFinal) son por defecto la fecha de emisión más temprana y la más tardía de las líneas. Un día sin ventas no tiene líneas de donde leerlas, así que debes pasarlas: si no, MISSING_FIELD { field: 'cover.from' }. Un día sin ventas produce un resumen de tipo 39 en ceros.
  • Resúmenes. Uno por cada tipo de boleta presente, con los montos neto, IVA, exento y total, las cantidades de folios emitidos, anulados y utilizados, y los rangos. El tipo 41 (boleta exenta) ignora neto e IVA; la tasa de IVA aparece solo en el tipo 39.
  • Rangos. compactFolioRanges(folios) ordena los folios y agrupa los consecutivos en rangos { from, to } (un folio solo tiene from igual a to): compactFolioRanges([5, 1, 2, 3]) da [{ from: 1, to: 3 }, { from: 5, to: 5 }]. El reporte lista por separado los folios emitidos y los anulados. Un folio repetido entre las líneas y los folios anulados es INVALID_FOLIO.
  • Sin tipos de envío. Para corregir un reporte, se envía completo otra vez con un sequence mayor (cover.sequence, 1 por defecto, hasta 999; correlative es el contador opcional dentro del día).
  • Los folios anulados son aquí los que se anularon en tu sistema antes de llegar al SII, no los anulados con una nota de crédito.

Subir#

uploadBook(sii, { xml }) envía un libro firmado y uploadFolioConsumption(sii, { xml }) un reporte firmado. Ambos van por DTEUpload en ISO-8859-1 y devuelven un UploadReceipt con el trackId. El nombre de archivo es por defecto LibroGuia.xml para un libro de guías, LibroCV.xml para cualquier otro libro y ConsumoFolios.xml para un reporte; fileName lo reemplaza. Los estados del envío de un libro tienen sus propios códigos: LOK (envío de libro aceptado), LSO, LNC (el tipo de envío de libro no corresponde), LRH (libro rechazado con historial)… Ver Servicios web del SII y Errores.

Catálogos#

ExportQué contiene
BOOK_OPERATIONS{ sale: 'VENTA', purchase: 'COMPRA' }
BOOK_KINDS{ monthly: 'MENSUAL', special: 'ESPECIAL', rectification: 'RECTIFICA' }
BOOK_SUBMISSIONS{ total: 'TOTAL', partial: 'PARCIAL', final: 'FINAL', adjustment: 'AJUSTE' }
BOOK_DOCUMENT_TYPESLos tipos de documento que admite el esquema del libro (DoctoType de LibroCV_v10.xsd), electrónicos o no; isBookDocumentType(value) es la guarda.
SUMMARY_ONLY_DOCUMENT_TYPESTipos que el formato excluye del detalle: 35, 38, 39, 41, 48, 105, 500, 501, 919, 920, 922, 924.
NON_RECOVERABLE_VAT_CODESPor qué no se puede recuperar el IVA de una compra: nonTaxableOperations 1, lateRegistration 2, rejectedExpenses 3, freeDeliveries 4, other 9.
GUIDE_TRANSFER_TYPESLas operaciones de guía 1 a 7.
GUIDE_VOID_CODES{ folio: 1, guide: 2, partialReceipt: 3 }

Próximos pasos#

  • Servicios web del SII sube el XML firmado y sigue su estado.
  • Certificación usa los tres libros en los sets de pruebas y en la certificación de boletas.
  • Errores lista BOOK_TOTAL_MISMATCH y los demás códigos que lanzan los libros.