SII books
The SDK builds three XML documents that the SII calls books: the purchase and sales book (LibroCompraVenta, the IECV), the dispatch guide book (LibroGuia) and the folio consumption report (ConsumoFolios, the RCOF). You give it the lines, it computes the summaries, renders the XML in the format the SII validates, signs it with the sender's certificate and, with the web services client, uploads it.
Their use today is narrow. The RCV (registro de compras y ventas) replaced the purchase and sales book in production from period 2017-08, and the guide book is sent only when the SII asks for it; both are still part of the certification test set. The folio consumption report, now called Resumen de Ventas Diarias, is used in the receipts certification, and the SII probably no longer requires it in production. See Certification.
The books have not been sent to the real SII yet. In the tests they validate against the official XML schemas, and their XML matches golden files byte for byte. dte-sdk 0.8.0 is on npm, the latest release. It is not recommended for production yet: most areas have not met the real SII, and until 1.0.0 the API can change in any release. See Status.
Usage#
A sales book for one month, built from the documents you issued, signed and uploaded:
import {
bookEntryFromDocument,
createPurchaseSalesBook,
signPurchaseSalesBook,
uploadBook,
} from 'dte-sdk'
const book = createPurchaseSalesBook({
cover: {
issuerRut: '76543210-3', // the company that owns the book
senderRut: certificate.rut!, // who signs and sends
period: '2026-10',
resolutionDate: '2026-01-15',
resolutionNumber: 0, // 0 in certification
operation: 'sale',
},
entries: invoices.map((document) =>
bookEntryFromDocument(document, { bookRut: '76543210-3', operation: 'sale' }),
),
// Receipts are not listed line by line: they go in as totals.
summaryOnlyTotals: [{ type: 39, documentCount: 120, netAmount: 840000, vatAmount: 159600, totalAmount: 999600 }],
})
book.periodTotals // one row per document type, computed from the lines and the summary-only totals
book.totalMismatches // explicit line totals that differ from the SII formula (empty here)
const signed = signPurchaseSalesBook(book, certificate)
const receipt = await uploadBook(sii, { xml: signed.xml })
receipt.trackId // follow it with waitForUploadStatuscreatePurchaseSalesBook validates and normalizes, signPurchaseSalesBook returns the book plus xml (with the ISO-8859-1 prolog, ready to upload) and the signature values, and uploadBook sends it. Every function is pure except the upload.
The three books at a glance#
| Book | Build | Sign | Upload | Submissions |
|---|---|---|---|---|
Purchase and sales (LibroCompraVenta) | createPurchaseSalesBook | signPurchaseSalesBook | uploadBook | total, partial, final, adjustment |
Dispatch guides (LibroGuia) | createGuideBook | signGuideBook | uploadBook | total, partial, final, adjustment |
Folio consumption (ConsumoFolios) | createFolioConsumption | signFolioConsumption | uploadFolioConsumption | none: resend the whole report |
Each has a build…Xml function that renders the unsigned XML (buildPurchaseSalesBookXml, buildGuideBookXml, buildFolioConsumptionXml) and a verify…Signature function that checks the signature of an XML (verifyPurchaseSalesBookSignature, verifyGuideBookSignature, verifyFolioConsumptionSignature). The signature is an enveloped XMLDSig over the EnvioLibro (or DocumentoConsumoFolios) element, referenced by its ID.
toSiiPurchaseSalesBook, toSiiGuideBook and toSiiFolioConsumption convert a book to an object with the official SII tags (Caratula, Detalle, ResumenPeriodo…), if you want to see or post-process what is rendered.
The rendered XML starts with an ISO-8859-1 declaration, which uploadBook requires. It also puts a line break between every pair of adjacent tags, because the SII rejects lines longer than about 4 090 characters (CHR-00002).
The purchase and sales book#
The cover#
| Field | Required | Meaning |
|---|---|---|
issuerRut | yes | RUT of the company the book belongs to (RutEmisorLibro). |
senderRut | yes | RUT of the authorized sender, normally the holder of the certificate (RutEnvia). |
period | yes | Tax period, YYYY-MM. On a special book, the earliest period it covers. |
resolutionDate | yes | Date of the SII resolution, YYYY-MM-DD or DD-MM-YYYY. |
resolutionNumber | yes | Resolution number, an integer from 0 to 999 999. 0 in certification. |
operation | yes | 'sale' (VENTA) or 'purchase' (COMPRA). |
kind | no | 'monthly' (MENSUAL, the default), 'special' (ESPECIAL) or 'rectification' (RECTIFICA). |
submission | no | 'total' (the default), 'partial', 'final' or 'adjustment'. |
segmentNumber | on partial | Number of the segment. Emitted only on a partial submission. |
notificationFolio | on special | Folio of the SII notification that asks for the book. |
rectificationCode | on rectification | The authorization code of the replacement, up to 10 characters (CodAutRec). |
A missing required field is MISSING_FIELD { field: 'cover.…' }, a bad period or date is INVALID_DATE, a bad RUT is INVALID_RUT.
Kinds and submissions#
The kind says what the book is: the month's book, a special one the SII requested, or the replacement of one already sent. The submission says how it reaches the SII. A month can arrive in one go or in pieces, and a closed month can be corrected afterwards:
| Submission | What it carries | Summaries it gets |
|---|---|---|
total | All the lines of the period, in one book. | Period summary. |
partial | One segment of the lines, with its segmentNumber. Needs lines; takes no previous totals and no summary-only totals. | Segment summary only. |
final | The closing of the segments: no lines; it needs previousTotals (the sum of the segments) or summaryOnlyTotals. | Period summary. |
adjustment | Lines that add to or remove from a book already sent (adjustment: 'add' or 'remove' on each line; add is the default). | Segment summary (the signed adjustment) and period summary. |
The zones are validated before anything is rendered: a partial without lines is MISSING_FIELD { field: 'entries' }; a final with lines is INVALID_OPTION { field: 'entries' }; previousTotals on anything but a final or an adjustment is INVALID_OPTION { field: 'previousTotals' }; summaryOnlyTotals on a partial is INVALID_OPTION { field: 'summaryOnlyTotals' }; a final with neither previous nor summary-only totals is MISSING_FIELD { field: 'previousTotals' }. A special book without notificationFolio or a rectification without rectificationCode is MISSING_FIELD.
The lines#
Each entry of entries is a document, with the SDK's English names (the Spanish SII tag each one produces is in the JSDoc). The ones you will use most:
| Field | Meaning |
|---|---|
type, folio | Document type (see BOOK_DOCUMENT_TYPES) and folio. |
date | Date of the document, YYYY-MM-DD or DD-MM-YYYY, from 2000 to 2050. |
counterpartyRut, counterpartyName | The customer on a sales book, the supplier on a purchase book. The name is at most 50 characters. |
exemptAmount, netAmount, vatAmount | Amounts. On a sales book they default to 0; on a purchase book vatAmount is the recoverable part. |
taxRate | VAT rate in percent. Filled with the VAT rate when a line has net and VAT and no rate. |
otherTaxes | Additional taxes and retentions: { code, rate?, amount }, up to 20. |
nonRecoverableVat | Non-recoverable VAT on purchases: { code, amount }, up to 5 (see NON_RECOVERABLE_VAT_CODES). |
commonUseVat, fixedAssetNetAmount, fixedAssetVatAmount, nonCreditableTax… | The purchase-side specifics. |
totalVatWithheld, partialVatWithheld, constructionCredit, containerDeposit, settlement… | Retentions, credits and commissions of settlement invoices. |
totalAmount | The line total. Computed with the SII formula when absent; kept and compared when present. |
voided | true for a folio voided before it reached the SII: only type and folio travel and it adds no amounts. |
adjustment | 'add' or 'remove' on an adjustment submission. |
The line total follows the formula of the SII format for each side of the book: net plus exempt plus VAT plus additional taxes, less withheld VAT and so on, and on purchases plus common-use VAT, non-recoverable VAT and tax without credit. Retention taxes do not add to a purchase total, and code 60 (initial invoice retention) subtracts.
An invalid line is INVALID_DOCUMENT_TYPE, INVALID_FOLIO, INVALID_AMOUNT, INVALID_DATE, INVALID_RUT, INVALID_OPTION or FIELD_TOO_LONG, with details.index telling which line.
Explicit totals: totalMismatches and strictTotals#
If you pass a totalAmount that is not what the formula gives, the SDK keeps your value (the SII accepts some of those) and reports the difference, so a decision stays yours:
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 }]With strictTotals: true the same input throws BOOK_TOTAL_MISMATCH instead, with details: { index, expected, actual } for the first mismatch.
Summaries and carrying totals between submissions#
The SDK computes the summaries from the lines: one row per document type, in the order of first appearance.
segmentTotalsis the summary of this submission's own lines. It exists onpartialandadjustmentsubmissions that have lines; otherwise it isnull.periodTotalsis the summary of the whole period: the lines, plussummaryOnlyTotals, pluspreviousTotals. It isnullon apartialand when there is nothing to add up.summaryOnlyTotalscarries what the format excludes from the detail: receipts and a few other types (SUMMARY_ONLY_DOCUMENT_TYPES: 35, 38, 39, 41, 48, 105, 500, 501, 919, 920, 922 and 924). Give the totals of that type as{ type, documentCount, netAmount, vatAmount, totalAmount… }; whatever you leave out counts as 0.
periodTotals is a plain JSON value. Store it after each submission and pass it back as previousTotals in the next one; accumulateBookTotals(base, delta) adds two lists of rows by document type (and, inside a type, non-recoverable VAT and other taxes by code):
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 }
// Two segments of the month…
const first = createPurchaseSalesBook({ cover: { ...cover, submission: 'partial', segmentNumber: 1 }, entries: firstHalf })
const second = createPurchaseSalesBook({ cover: { ...cover, submission: 'partial', segmentNumber: 2 }, entries: secondHalf })
// …and the closing, with no lines, that carries what the segments added up to.
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 // the whole monthAn adjustment signs its lines (remove counts negative) and carries them on top of the previous totals, so its periodTotals is the corrected month.
Common-use VAT and factors#
When a period summary has common-use VAT, the book needs the proportionality factor: pass commonUseVatFactor (0.001 to 99.999, at most 3 decimals); without it, the build fails with MISSING_FIELD { field: 'commonUseVatFactor' }. additionalTaxFactors maps an additional tax code to its factor (0.001 to 1, at most 4 decimals). A factor out of range is INVALID_OPTION.
Ids and the clock#
The EnvioLibro ID is generated (LCV_<dd>_<mm>_<yyyy>_<hh>_<mm>_<ss>_<ms> in Chile time, from now) unless you pass id. now also sets the signing timestamp (signedAt). Pass both to make a build reproducible in tests.
Deriving lines from documents#
You rarely write lines by hand. Three functions derive them from documents issued with this SDK:
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) // a dispatch guide (52)
folioConsumptionEntryFromDocument(receipt) // a receipt (39 or 41)bookEntryFromDocumenttakes the document and{ bookRut, operation, creditNoteSign? }. The counterparty is the receiver when the owner of the book issued the document and the issuer otherwise, which covers sales, received purchases and purchase invoices (46) on either side. The name is cut to 50 characters, because the book field is shorter than the document's.creditNoteSign: 'negative'negates the amounts of a credit note (61); the default,'positive', is how the format has it. Not inferred, and left for you to spread into the result when the case needs them:issuedByBookOwner,purchaseInvoiceand the reference to the voided document.guideEntryFromDocumenttakes a dispatch guide; any other type isINVALID_DOCUMENT_TYPE. The transfer is a sale by default, and the receiver and the amounts travel only on sales: a non-sale guide reports its total alone.folioConsumptionEntryFromDocumenttakes a receipt; any other type isINVALID_DOCUMENT_TYPE.
The dispatch guide book#
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, // always required: the folio of the SII notification
},
entries: guides.map((guide) => guideEntryFromDocument(guide)),
})
await uploadBook(sii, { xml: signGuideBook(guideBook, certificate).xml }) // sent as LibroGuia.xmlThe cover is the same as the purchase and sales book's, without operation and kind (the schema admits only the special kind), and with notificationFolio mandatory on every guide book. The submissions work the same way, including segmentNumber on a partial, previousTotals on a final or an adjustment and adjustment: 'add' | 'remove' on the lines.
A line is { folio, voided?, adjustment?, transferType?, date?, receiverRut?, receiverName?, netAmount?, taxRate?, vatAmount?, totalAmount?, modifiedAmount?, referenceType?, referenceFolio?, referenceDate? }:
transferTypeis the operation, from 1 to 7 (GUIDE_TRANSFER_TYPES: 1 sale, 2 sale to be made, 3 consignment, 4 demonstration, 5 internal transfer, 6 other non-sale transfer, 7 return guide). The schema stops at 7, so the values 8 and 9 of the guide'sIndTrasladocannot be reported on a line.voidedis'folio'(voided before reaching the SII),'guide'(voided afterwards) or'partialReceipt'(goods partly received, with the correctedmodifiedAmount). The first two drop the amounts and the receiver;partialReceiptkeeps them (GUIDE_VOID_CODES: 1, 2 and 3).vatAmountis computed from the rate and the net when absent or 0, andtotalAmountis net plus VAT when absent. The rate and the VAT are always emitted, as 0 when absent.
The summary (GuideTotals) is { voidedFolios, voidedGuides, saleGuides, saleGuidesAmount, modifiedAmount, transfers }, where transfers has one { type, count, amount } per non-sale operation. Like the book's, it is plain JSON: store periodTotals, pass it back as previousTotals, and add two of them with accumulateGuideTotals(base, delta).
The folio consumption report (RCOF)#
The report says which receipt folios a company used on a range of days. It is built from the receipts you issued (types 39 and 41):
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: default to the earliest and latest issue dates of the lines
// sequence: 1 by default
},
entries: receipts.map((receipt) => folioConsumptionEntryFromDocument(receipt)),
voidedFolios: [{ type: 39, folio: 52 }], // folios voided in your system before reaching the SII
})
report.summaries // one per receipt type present, type 39 first
await uploadFolioConsumption(sii, { xml: signFolioConsumption(report, certificate).xml })- Dates.
fromandto(FchInicio,FchFinal) default to the earliest and latestissueDateof the lines. A day without sales has no lines to read them from, so you must pass them:MISSING_FIELD { field: 'cover.from' }otherwise. A day without sales produces one summary of type 39 in zeros. - Summaries. One per receipt type present, with the net, VAT, exempt and total amounts, the counts of issued, voided and used folios, and the ranges. Type 41 (exempt receipt) ignores net and VAT; the VAT rate appears only on type 39.
- Ranges.
compactFolioRanges(folios)sorts folios and groups the consecutive ones into{ from, to }ranges (a lone folio hasfromequal toto):compactFolioRanges([5, 1, 2, 3])gives[{ from: 1, to: 3 }, { from: 5, to: 5 }]. The report lists issued folios and voided folios in separate ranges. A folio repeated across lines and voided folios isINVALID_FOLIO. - No submission kinds. To correct a report, send it whole again with a higher
sequence(cover.sequence, 1 by default, up to 999;correlativeis the optional counter within the day). - Voided folios here are those voided in your system before they reached the SII, not those cancelled with a credit note.
Uploading#
uploadBook(sii, { xml }) sends a signed book and uploadFolioConsumption(sii, { xml }) a signed report. Both go through DTEUpload as ISO-8859-1 and return an UploadReceipt with the trackId. The file name defaults to LibroGuia.xml for a guide book, LibroCV.xml for any other book and ConsumoFolios.xml for a report; fileName overrides it. The statuses of a book upload use their own codes: LOK (book accepted), LSO, LNC (type of submission does not match), LRH (rejected with history)… See SII web services and Errors.
Catalogs#
| Export | What it holds |
|---|---|
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_TYPES | The document types the book schema admits (DoctoType of LibroCV_v10.xsd), electronic or not; isBookDocumentType(value) is the guard. |
SUMMARY_ONLY_DOCUMENT_TYPES | Types the format excludes from the detail: 35, 38, 39, 41, 48, 105, 500, 501, 919, 920, 922, 924. |
NON_RECOVERABLE_VAT_CODES | Why the VAT of a purchase cannot be recovered: nonTaxableOperations 1, lateRegistration 2, rejectedExpenses 3, freeDeliveries 4, other 9. |
GUIDE_TRANSFER_TYPES | The guide operations 1 to 7. |
GUIDE_VOID_CODES | { folio: 1, guide: 2, partialReceipt: 3 } |
Next steps#
- SII web services uploads the signed XML and follows its status.
- Certification uses the three books in the test sets and the receipts certification.
- Errors lists
BOOK_TOTAL_MISMATCHand the other codes the books raise.