npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dte-sdk

v0.8.0

Published

TypeScript SDK for Chilean electronic tax documents (DTE): build, stamp (TED), sign and envelope DTEs for the SII, and talk to the SII web services (upload, status, acceptance/claims).

Readme

npm version downloads bundle size types included dependencies license

⚠️ Not ready for production. dte-sdk is under active development, and its use is not recommended until it reaches version 1.0.0, the release that will mark the library as mature enough to run in production. Until then the API can change in any release, and much of what this README describes has not met the real SII yet: see Status.

dte-sdk builds, stamps and signs Chilean DTE (Documento Tributario Electrónico) the way the SII expects them, sends them through the SII web services and tracks them. It also covers what surrounds issuing: the SII books, the certificate-authenticated SII portal (folios, the purchase and sales register, the MIPYME backup), the certification process and the printed samples. The API is readable English, with bilingual (EN/ES) documentation on every field.

  • 🧾 Document types 33, 34, 39, 41, 43, 46, 52, 56 and 61.
  • 🧮 Totals done right — per-line rounding, exempt and non-billable lines, VAT-included prices (MntBruto), global discounts, purchase-invoice withholding.
  • 🔏 TED + XMLDSig — the electronic stamp signed with the CAF key and the enveloped signature the SII validates, byte for byte.
  • ✉️ Envelopes — EnvioDTE and EnvioBOLETA with cover and signed SetDTE.
  • 📥 Received documents — the signature, the stamp and the CAF of every document a supplier sends, and the SII's signature over that CAF.
  • 🪪 Certificates — PKCS#12 (.pfx/.p12, legacy and modern) or PEM; the holder RUT is read from the certificate.
  • 📡 SII web services — seed/token, DTEUpload and the receipts REST API, envelope and document status, polling, acceptance/claims; injectable transport, retries, HTTP capture with redaction and an upload archive. TLS verification stays on.
  • 📚 SII books — the purchase and sales book, the dispatch guide book and the folio consumption report, with their summaries computed from the lines.
  • 🏛️ SII portal and folios — certificate login with reused sessions, company data, folio quota, CAF requests, annulment and re-obtaining, and a local folio registry.
  • 🗂️ Purchase and sales register and MIPYME backup — the documents and official summaries of a period, purchase reclassification, and the XML of issued and received documents.
  • 🔎 Activity verification — the status of the verification the SII requires before issuing documents with VAT, unstable for now, and the documents it takes to accredit an activity.
  • 🎓 Certification — the SII test sets parsed and issued, their books, the simulation, the exchange answers, and a resumable runner over the certification portals; and the certification of receipts (boletas), an application of its own, as pieces to chain.
  • 🖨️ Printed samples — one PDF per document following the SII manual, with its PDF417 stamp.
  • 🧠 Fully typed — a domain model in English (totalAmount, issuer.businessName) that maps to the official SII tags (MntTotal, RznSoc) through a codec.
  • 🇨🇱 RUTs by rut.ts — dte-sdk handles every RUT value with rut.ts, the most complete RUT library there is, with a world-class implementation. Every RUT is validated and normalized by it; nothing is reimplemented here.
  • 🛡️ Typed errors with stable codes and no personal data in messages.

Contents

Status

0.1.0 was the first release with a public API. 0.2.0 added the catalog of the register's document codes, 0.3.0 the activity verification, still unstable, and 0.4.0 the verification of received documents. 0.5.0 keeps the CAFs a batch request already holds when a later trip fails, and writes the folio registry for its owner only. 0.6.0 does the same for annulling, getting ranges back and the exact request, and logs what stopped a flow with every text redacted. 0.7.0 settles the gaps an application found while it was built on dte-sdk, in the folio flows, CAF files and CAF parsing, and the receiver's answer, which reads a failed query as unknown. Every area this README describes ships in 0.7.0. Releases before 1.0.0 come out as the work lands, without waiting for the runs against the real SII; those runs come before 1.0.0, the first version recommended for production.

Every area is tested against synthetic SII answers, with a 100 % coverage gate in CI. Those tests cannot show how the real SII answers, so this table says what has met it so far, in each of the SII's environments: production and certification (maullin). "Not recorded" means no run has been written down, not that the SII refused one; a dash means the SII offers it in the other environment only.

| Area | Entry | Production | Certification (maullin) | | -------------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Documents, stamps, signatures and envelopes | dte-sdk | Not sent yet | Not sent yet | | Verification of received documents | dte-sdk | A local check with no SII call: not run on a supplier's document yet. The tests sign test CAFs with a test SII key, and the SII's keys met real CAFs only in a check by hand | Same as production | | Web services: seed and token, SOAP | dte-sdk | Yes, 2026-11-09 and 10: a token for a seed the certificate signed, reused between calls | Not run yet (issue 8) | | Web services: seed and token, receipts REST | dte-sdk | Not run yet | Not run yet (issue 8) | | Web services: uploads and their status | dte-sdk | Not run yet | Not run yet (issue 8) | | Web services: acceptance and claim registry | dte-sdk | Yes, 2026-11-09 and 10, on one received invoice (type 33): registerClaim answered 0 for ERM and then for ACD; listClaimEvents answered 16 (no events) before and 15 (both events) after; queryReceptionDate gave the reception date and receiverAnswer the deadline. The claims (RCD, RFP, RFT) and queryTransferable not run yet | Not run yet (issue 8) | | Public registry of authorized issuers | dte-sdk | Not recorded | Not recorded | | SII books | dte-sdk | Not sent yet; they validate against the official schemas in the tests | Not sent yet | | Portal session | dte-sdk/portal | Yes, 2026-11-09 and 10: certificate login and the session reused between reads, for a company not enrolled as an issuer | Not recorded | | Company data and the refusal pages | dte-sdk/portal | Not recorded | Not recorded | | Folios and CAF requests | dte-sdk/portal | Not run yet: they need an enrolled company | Not run yet: they need an enrolled company; the tests replay the pages this environment served to one | | Purchase and sales register: reads | dte-sdk/portal | Yes, 2026-11-09 and 10, for a company with one received invoice: fetchRegisterSummary, listPeriodDocuments, fetchPurchaseDocument and listPurchaseTypes answered with its rows. createdAt (det_fec_creacion) comes as milliseconds since the epoch. Right after a claim, the listing's summary call answered HTTP 503 twice and answered on the next read | Not recorded | | Purchase and sales register: writes, MIPYME backup | dte-sdk/portal | Not run yet | Not run yet | | Activity verification | dte-sdk/portal | Read on 2026-11-09 for a conditioned verification pending accreditation, as before for a pending one; an approved one not seen yet, so the read stays unstable | — | | Certification sets, runner, exchange and upload of the samples | dte-sdk/portal | — | Not run yet; the exchange answers validate against the official schemas in the tests | | Certification of receipts (boletas) | dte-sdk/portal | — | Not run yet: it needs a company enrolled in the receipts application; the receipts envelope and the folio consumption report validate against the official schemas in the tests | | Printed samples | dte-sdk/pdf | — | Not reviewed by the SII yet |

The production runs of 2026-11-09 and 10 came from an application built on dte-sdk, for one company, on 0.3.0 and then 0.4.0. Earlier runs, whose environment wasn't written down, answered the portal's login and logout, the taxpayer data and the refusal pages for a company not enrolled as an issuer, and the register's read-only requests for a company with no documents.

Still to come before 1.0.0: running every area against the real SII, starting with the live runs tracked in issue 8. Also open: the detail calls of the purchase and sales register, to be compared against real answers (issue 14), and the improvements of the certification runner set aside to keep it to field behaviour, to be weighed after the first real certification (issue 29).

Out of scope: export documents (110, 111 and 112), fee receipts (boletas de honorarios), the assignment of invoices (AEC), forms F29 and F22, the exchange e-mail and full XSD validation.

Installation

npm install dte-sdk

Node ≥ 22.12. The package ships ES modules only, and a CommonJS project loads it with require() just the same. Printed samples need two more packages, which stay out of the install otherwise:

npm install pdf-lib bwip-js

Entry points

| Import from | What it holds | Runs on | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | dte-sdk | Documents, stamps, signatures, envelopes, books, the verification of received documents and the SII web services client over fetch | Node, Bun, Deno; bundles for the browser with no configuration | | dte-sdk/node | createNodeHttpsTransport, createDirectoryCapture, createDirectoryUploadArchive, createDirectoryTokenCache, createDirectoryFolioRegistry, findCafFiles, findLatestCafFile, resolveCafFile, cafFileScope | Node | | dte-sdk/portal | SII portal, folios and CAF requests, the folio registry, purchase and sales register, MIPYME backup, activity verification, and certification: sets, runner, exchange, upload of the samples and the receipts application | Node | | dte-sdk/pdf | Printed samples and the SII office of each commune (SII_OFFICES, siiOfficeForCommune) | Node, with pdf-lib and bwip-js installed |

DteError and isDteError come from dte-sdk and recognize what every entry throws.

Jest in its default CommonJS mode cannot require() an ES module. Run it in ESM mode, or let it transform this package with transformIgnorePatterns.

Quick start

import { readFileSync } from 'node:fs'
import { loadCertificateFromPkcs12, parseCaf, issueDocument, createEnvelope, signEnvelope, encodeLatin1 } 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: {
    type: 33,
    folio: 1,
    issuer: {
      rut: '76543210-3',
      businessName: 'Comercial Andina SpA',
      businessActivity: 'Software',
      address: 'Av. Los Aromos 1540',
      commune: 'Santiago',
      activityCode: 620200,
    },
    receiver: {
      rut: '12345678-5',
      businessName: 'Comercializadora Rapel Ltda',
      businessActivity: 'Comercio',
      address: 'Pasaje Los Olmos 88',
      commune: 'Providencia',
    },
    items: [{ name: 'Plan anual', quantity: 1, unitPrice: 100000 }],
  },
  caf,
  certificate,
})

invoice.header.totals // { netAmount: 100000, vatRate: 19, vatAmount: 19000, totalAmount: 119000 }
invoice.xml // signed <DTE> with its TED

const envelope = signEnvelope(
  createEnvelope({
    kind: 'dte',
    documents: [invoice],
    cover: {
      issuerRut: '76543210-3',
      senderRut: certificate.rut!,
      resolutionDate: '2024-01-15',
      resolutionNumber: 123,
    },
  }),
  certificate,
)

encodeLatin1(envelope.xml) // ISO-8859-1 bytes, as the SII receives them

To send the envelope to the SII, see SII web services.

Receipts (boletas) need no receiver and take VAT-included prices:

const receipt = issueDocument({
  document: { type: 39, folio: 10, issuer, items: [{ name: 'Café', unitPrice: 1500 }], pricesIncludeVat: true },
  caf: parseCaf(readFileSync('caf-39.xml', 'utf8')),
  certificate,
})

Step by step

import { createDocument, stampDocument, signDocument, verifyTed, verifyDocumentSignature, toSiiDocument } from 'dte-sdk'

const doc = createDocument(input) // validates, sanitizes, numbers lines, computes totals
toSiiDocument(doc) // the same data with the official tags: MntTotal, RznSoc, NmbItem…
const stamped = stampDocument(doc, caf) // adds the TED (signed with the CAF key)
const signed = signDocument(stamped, certificate) // enveloped XMLDSig → signed.xml

verifyTed(signed.ted.xml, caf) // true
verifyDocumentSignature(signed.xml) // { digestValid: true, signatureValid: true, valid: true, … }

Every function that depends on "now" accepts { now: Date }, so tests are deterministic.

Received documents

import { readFileSync } from 'node:fs'
import { decodeLatin1, isClaimDocumentType, verifyReceivedDocuments } from 'dte-sdk'

// Decode the bytes with the encoding the XML declaration names: ISO-8859-1 for the SII's documents
// In production by default: a document stamped with a certification CAF comes back invalid
const { envelope, documents, status } = verifyReceivedDocuments(decodeLatin1(readFileSync('EnvioDTE.xml')))

status // 'valid' | 'invalid' | 'unchecked': the worst of the envelope and its documents
envelope?.valid // the envelope's own signature; envelope is null for a single DTE
documents[0] // { type: 33, folio: 1234, issuerRut: '10000004-0', totalAmount: 119000, signature, ted, caf, status, xml, … }
const toAnswer = documents.filter((doc) => doc.status === 'valid' && isClaimDocumentType(doc.type))

verifyReceivedDocuments takes a DTE, an EnvioDTE or an EnvioBOLETA as a supplier sent it. Each document is checked three ways, and its status is invalid when a check fails, unchecked when none fails but the CAF's IDK has no known key, and valid otherwise:

  • signature: its XMLDSig signature over Documento (or Liquidacion), with found, digestValid and signatureValid. It proves the document is intact with respect to the certificate it carries, not whose that certificate is.
  • ted: the stamp. Its FRMT must verify with the key of the CAF the TED carries, over the DD as ISO-8859-1 bytes, and its DD must agree with the header: mismatches names what differs among the issuer, the type, the folio, the date, the receiver and the total. RSR and IT1 are free text each issuer folds its own way, and are not compared.
  • caf: the CAF in the stamp, against the document (mismatches: another issuer, another type, a folio outside its range) and against the SII: siiSignature checks its FRMA, the SII's signature over DA, with the SII key its IDK names.

SII_CAF_KEYS holds the keys 100 (certification) and 300 (production) that sign every CAF, taken from the SII's certificates as CryptoSys and LibreDTE publish them, the same bytes in both. Only the key of environment is accepted, production by default: a CAF signed with the other one is invalid, with the mismatch environment and caf.environment saying which it is, so a certification document never passes for a production one. Pass environment: 'certification' to check documents of the certification environment. siiKeys replaces the key material; spread SII_CAF_KEYS to add a key. verifyCafSignature(cafXml, { environment }) checks the FRMA of a single CAF the same way; to check the CAF your own TEDs will carry, pass it parseCaf(file).xml, the block parseCaf keeps.

  • A document that fails a check is a result, not an error. Malformed XML, another root, an envelope without exactly one SetDTE and a DTE without its Documento throw XML_PARSE_FAILED; an envelope without documents throws ENVELOPE_EMPTY.
  • What is checked is what is reported. Each document is read from its element in the parsed input, never from a cut of the text: its signature is checked over forms written from that element and its fields and stamp are read from it. xml is that element written out: the same elements, attributes, text and comments, which read back and verify the same, but not the input byte for byte (a CDATA section becomes escaped text, a character reference the character, an empty element <X/>). To archive a received document, keep the bytes of the input itself, not xml. A document is often signed before it goes into an envelope, with other namespaces in scope: its signature is checked where it sits and, failing that, on its own, with the SII namespace and as written; the forms differ only in namespace declarations, never in content. A signature passes only with one Reference, SHA-1, RSA-SHA1, C14N 1.0 and no transform but enveloped-signature or C14N 1.0.
  • The stamp is read from its elements alone. The FRMT and the FRMA are checked over the DD and the DA written from the very elements whose fields are compared: flattened, the form the SII signs, with their texts escaped the SII's way or with only &, < and > escaped (so &#xC1; reads as Á). A stamp is read only when its structure leaves no doubt: one TED, plain (no prefix, no namespace declaration, no comment, CDATA or instruction), holding a DD and then an FRMT and nothing else, and one CAF with one DA and one FRMA in the DD; anything else is a failed check. A DOCTYPE, which the parsers read differently, is refused with XML_PARSE_FAILED, as are a prefixed DTE or Documento.
  • Not checked: whose certificate signed, and whether the CAF had expired when the document was stamped.
  • Not run on a supplier's document yet. The tests sign test CAFs with a test SII key pair. The two SII keys were checked once by hand, outside the tests: key 100 verifies the CAF of the example the SII publishes, and key 300 verified production CAFs carried by real documents.

SII web services

import { createSiiClient, uploadEnvelope, waitForUploadStatus, registerClaim, claimResponseMessage } from 'dte-sdk'

const sii = createSiiClient({ environment: 'certification', certificate, companyRut: '76543210-3' })

const receipt = await uploadEnvelope(sii, { envelope }) // EnvioDTE → DTEUpload; EnvioBOLETA → REST in production
const status = await waitForUploadStatus(sii, { trackId: receipt.trackId })
status.status // 'accepted' | 'acceptedWithObjections' | 'rejected' | …
status.message // Spanish gloss from the SII

const claim = await registerClaim(sii, { issuerRut: '10000004-0', type: 33, folio: 1234, action: 'acceptContent' })
if (!claim.ok) console.log(claimResponseMessage(claim.code)) // business answers are codes, not errors

Uploads send the XML as ISO-8859-1 bytes, so it must open with an XML declaration naming ISO-8859-1 (the envelopes and books this SDK builds do); anything else is refused with INVALID_OPTION { field: 'xml' } before anything is sent, and whitespace before the declaration is dropped.

The client is plain data: every operation is a free function that takes it first. Tokens are obtained and renewed for you (one cache per environment, channel and certificate). Also available: getSeed/getToken, uploadBook, uploadFolioConsumption, queryUploadStatus, queryDocumentStatus, folioReceived, listClaimEvents, queryTransferable, queryReceptionDate, receiverStatus. CLAIM_DOCUMENT_TYPES (33, 34 and 43) and isClaimDocumentType say which documents the registry takes.

receiverAnswer(listed, receivedAt, today) reads what the receiver answered, with no request: the result of listClaimEvents, the receivedAt of queryReceptionDate and today in Chile (chileDate()). A credit note that annuls comes first, then a claim, then an acceptance; with none of them, eight calendar days after the SII received the document it is accepted by law (Ley 19.983, tacitlyAccepted). Pass the result of listClaimEvents whole: when the query failed and listed no events, the answer is unknown, never pending or accepted by law. Its events alone still work, but cannot tell a failed query. It also gives claimDeadline, the last day to claim.

queryAuthorizedIssuer(sii, { rut }) asks the public registry of authorized issuers of the SII (ee_empresa_rut) for a RUT: its business name, the resolution of the environment, which is the NroResol and FchResol of every cover (number 0 in certification), and the document types it may issue. The registry asks for no certificate and no session, so any object with environment, transport, capture, logger, now, retry and sleep will do as well as a client. A RUT that is not an authorized issuer throws SII_PORTAL_REJECTED with rejection: 'companyNotAuthorized'.

Options worth knowing:

  • transport: fetch by default; createNodeHttpsTransport({ tls }) from dte-sdk/node for a custom CA (tls.ca, added to the public roots bundled with Node, not in place of them) or, only if you must, rejectUnauthorized: false / legacyRenegotiation: true. TLS verification is on by default and the SII hosts verify with the system trust store, so the defaults just work.
  • retry: 6 attempts with exponential backoff by default; false disables it. An upload (uploadEnvelope, uploadBook, uploadFolioConsumption) and registerClaim change state at the SII, so they are sent again only while no SII response has arrived (connection failure, timeout before the response headers). Once the SII answered, whatever it said, and even if the reply was cut off halfway, they are never re-sent, except once with a fresh token when the SII refused the token. Queries are read-only and follow the full policy. retry.retryOn can only narrow what is retried (() => false never re-sends), never widen it. Getting a token is one retry loop around seed, signature and token, so every attempt signs a fresh seed. waitForUploadStatus polls every intervalMs instead of following the retry policy, and its timeoutMs is a budget: each poll and each sleep is cut to the time that remains.
  • capture: createDirectoryCapture({ dir }) from dte-sdk/node writes every HTTP exchange, redacted (tokens, seeds, cookies, private keys), for SII support tickets. archive: createDirectoryUploadArchive({ dir }) keeps the exact bytes of every upload the SII answered, with its TrackID and the SII's reply (tokens redacted). A duplicate answer is kept as <trackId>-<kind>-duplicate-<yyyymmddHHMMSS> and never replaces the record of the original.
  • logger: silent by default; pass console or your own. Messages never carry tokens, RUTs or XML.
  • tokenCache: in memory by default. createDirectoryTokenCache({ dir, logger }) from dte-sdk/node keeps the tokens on disk, one file per key that only its owner can read (on Windows the permissions of dir decide), so a restart and the other processes of the same user reuse them; it serves as the portal's sessionCache too. Give it a directory of its own. It never throws: an entry it cannot read or write is a miss, reported only to logger.
  • Every call accepts { signal, timeoutMs, retry } as a last argument.

SII books

import { bookEntryFromDocument, createPurchaseSalesBook, signPurchaseSalesBook, uploadBook } from 'dte-sdk'

const book = createPurchaseSalesBook({
  cover: {
    issuerRut: '76543210-3',
    senderRut: '12345678-5',
    period: '2026-10',
    resolutionDate: '2026-01-15',
    resolutionNumber: 0,
    operation: 'sale',
  },
  entries: invoices.map((doc) => bookEntryFromDocument(doc, { bookRut: '76543210-3', operation: 'sale' })),
  summaryOnlyTotals: [{ type: 39, documentCount: 120, netAmount: 840000, vatAmount: 159600, totalAmount: 999600 }],
})
book.totalMismatches // explicit line totals that differ from the SII formula; `strictTotals: true` throws instead

const receipt = await uploadBook(sii, { xml: signPurchaseSalesBook(book, certificate).xml })

createGuideBook/signGuideBook build the dispatch guide book and createFolioConsumption/signFolioConsumption the folio consumption report, which goes up with uploadFolioConsumption. The SDK computes the summaries of all three from the lines. The two books cover every submission kind of their schemas (total, partial, final, adjustment), and previousTotals carries the totals of earlier submissions: book.periodTotals is a plain JSON value you can store and pass back. The report has no submission kinds: to correct one, send it whole again with a higher sequence. bookEntryFromDocument, guideEntryFromDocument and folioConsumptionEntryFromDocument derive the lines from documents issued with this SDK.

Their use today is narrow. The RCV 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 Resumen de Ventas Diarias) is used in the receipts certification, and the SII probably no longer requires it in production.

SII portal and folios

import { createPortalClient, fetchIssuerProfile, queryFolioQuota, requestCaf, closePortalSession } from 'dte-sdk/portal'

const portal = createPortalClient({ environment: 'certification', certificate, companyRut: '76543210-3' })

const issuer = await fetchIssuerProfile(portal) // resolution of this environment, name, address, activity
const quota = await queryFolioQuota(portal, { type: 33 }) // reads MAX_AUTOR and FOLIOS_DISP; stamps nothing
const { xml, caf } = await requestCaf(portal, { type: 33, quantity: 50 })
await closePortalSession(portal)

The portal is the certificate-authenticated site of the SII, read as HTML. The certificate must be enabled as a login method first, a manual step at sii.cl (Cambiar Clave y datos de autenticación, option "Utilizando Clave Tributaria o Certificado Digital"); otherwise the login throws SII_PORTAL_CERT_NOT_ENABLED. It needs Node: fetch cannot present a client certificate.

  • CAF XML is a secret. xml holds the private key of the CAF. Where you keep it is up to you.
  • One session per certificate. The SII rations sessions per RUT, so sessionCache and lock are ports: several replicas need shared ones. Sessions are reused for 90 minutes and logged in on demand. In memory by default, so a restart logs in again; createDirectoryTokenCache({ dir }) from dte-sdk/node keeps the session on disk, one owner-only file per certificate, and a restart reuses it. The default lock holds within one process only: two processes that share the directory and run at once can send requests of one session at the same time or replace each other's session, so give them a shared lock too, or keep one process per certificate.
  • Rationing. MAX_AUTOR and FOLIOS_DISP appear only while the SII rations a type. Unused folios lower what it authorizes and eventually block stamping.
  • To lift a block, issue with the folios you have. reobtainFolios gets lost CAFs back without spending quota. requestCafInBatches is for a small MAX_AUTOR with nothing unused, and requestCafExact wants one CAF for the whole quantity.
  • A flow cut short keeps what it got. Once a folio flow holds something, a later step that fails with anything but a refusal it can report (a transport failure, a timeout, an expired session, a block, CAF_INVALID, your abort) ends it with what it holds and failure: { code, operation, rejection? }, and the original error goes to the client's logger with every text redacted. requestCafInBatches returns the CAFs it got, with complete: false and rejection: null; a failed requestCaf may have been granted all the same, so check listReobtainableRanges before asking again. annulFolios returns what the SII already annulled, reobtainFolios the CAFs it already downloaded, and requestCafExact the knownAnnulled of its annulment. Read failure as well as rejection, and persist knownAnnulled whether failure is set or not.
  • A grant can be smaller than asked. requestCaf caps at MAX_AUTOR and, past the effective maximum with no minQuantity, asks for one folio, so read granted. minQuantity and requestCafExact are the strict forms.
  • Annulling cannot be undone and counts against the quota for six months. annulFolios needs range, withinDays or all: true, and requestCafExact annuls only with allowAnnul: true. maxRanges limits a pass, not the call: each pass tries at most maxRanges ranges (50, or 20 in requestCafExact) and there are up to four passes, so one call can annul up to four times that many (200, or 80). Every step is tried up to three times, whatever the error, on top of the retries of the client. exclude protects a CAF still in use. Pass knownAnnulled back between runs: the portal keeps listing annulled ranges forever, and a range its keys cover, in one key or several, is not posted again. Folios the SII answered one by one come back in runs, and a walk cut short keeps them.
  • Getting a range back asks the SII which folios are free. reobtainFolios sends plain requests (referers without a host, the forms picked by their exact opening tag, no session handling on the way in) and reads the listing for ranges and nothing else: with a session that expired, or on a page that states a refusal, the list is empty, and a listing that fails for any reason reads as no ranges to get back (the failure is in reason). It does not sign in on demand: call it with a session that is alive. reobtainCaf on its own enters the procedure first and signs in on demand. Inside reobtainFolios, a login page in answer to a download is SII_PORTAL_SESSION_EXPIRED, which ends the walk with the CAFs in hand and failure, or throws with none.

createFolioRegistry, createMemoryFolioRegistry and, from dte-sdk/node, createDirectoryFolioRegistry keep local track of the folios of each CAF: reserve, mark as sent or annulled, release. reserve hands out the next free folio: after the last one reserved, or from the lowest one reserved and not sent, never one that is used or annulled and never a free one below that point. With { lowestFree: true } it hands out the lowest free folio of the whole range, which is what the certification run asks for; use it only when the registry records every folio the range has spent. A service with its own database implements FolioRegistry with its own transactions, the option included if it is to back a certification run. createDirectoryFolioRegistry keeps folios.json for its owner only, as the token cache does: the ledger holds the companies' RUTs, their track ids and what you keep in extra.

To pick the CAF file to issue with, dte-sdk/node searches a directory, and its folders down to five levels below it, for CAF files. findCafFiles({ dir, type, issuerRut }) lists the usable ones newest first (by modification time): .xml files that parse as a CAF of that type whose RE is that company, read from the file and never from the folder it lies in, and that have not expired (isCafExpired, with its one-day margin). Each file is read in the encoding its XML declaration names, ISO-8859-1 as the SII serves it or UTF-8, and with no declaration as UTF-8; a file whose bytes are not that encoding is skipped, as is a folder or file that cannot be read. With includeExpired: true the expired files are listed too, with expired set. findLatestCafFile gives the first of them, or null. resolveCafFile({ dir, type, issuerRut, required, registry, environment }) gives the newest one that still has required free folios, remaining in the registry for the scope of the file (or its whole range without a registry), with that scope to reserve from, and null when none has: then it is time to ask the SII for a new CAF. Neither findLatestCafFile nor resolveCafFile takes an expired file. cafFileScope(file, environment) builds the same scope from a CAF's text and the CAF parsed from it, such as the portal's ObtainedCaf: give it the text the file holds, since the scope's cafFingerprint is of that text.

import { createDirectoryFolioRegistry, resolveCafFile } from 'dte-sdk/node'

const registry = createDirectoryFolioRegistry({ dir: './folios' })
const file = await resolveCafFile({
  dir: './cafs',
  type: 33,
  issuerRut: '76543210-3',
  required: 1,
  registry,
  environment: 'production',
})
if (file === null) throw new Error('No CAF with free folios: request one')
const folio = await registry.reserve(file.scope!) // file.caf goes to issueDocument

A refusal of the SII is SII_PORTAL_REJECTED, with details.rejection and portalRejectionMessage saying what it means and what to do; the composite flows return it as data, so the CAFs already obtained are not lost. Login, logout and the refusal pages were measured against the real SII, but the pages of an enrolled company (quota, CAF, folio ranges) have not been seen yet: see the portal and folios design.

Purchase and sales register and MIPYME backup

These run on the same session as the rest of the portal.

import {
  listPeriodDocuments,
  fetchRegisterSummary,
  listPurchaseTypes,
  changePurchaseType,
  fetchPurchaseDocument,
  moveVatToCommonUse,
  downloadMipymeBackup,
} from 'dte-sdk/portal'

const { summary, documents } = await listPeriodDocuments(portal, { period: '2026-10', direction: 'received' })
const { rows, header } = await fetchRegisterSummary(portal, { period: '2026-10', operation: 'purchase' })

const types = await listPurchaseTypes(portal)
const key = { type: 33, folio: 1234, period: '2026-10', counterpartyRut: '12345678-5' }
await changePurchaseType(portal, { ...key, purchaseType: '4' })
const purchase = await fetchPurchaseDocument(portal, key)
await moveVatToCommonUse(portal, { ...key, commonUseVat: 19000 })

await downloadMipymeBackup(portal, {
  from: '2026-10-01',
  to: '2026-10-31',
  direction: 'received',
  onChunk: async ({ from, to, xml }) => saveBackup(`${from}_${to}.xml`, xml),
})

| I need | Function | SII service | | --------------------------------------------------------------------------------- | -------------------------------------- | ------------------------ | | Each document of a period (folio, date, amounts), with no receipts and no XML | listPeriodDocuments | consemitidosinternetui | | Official totals per document type and accounting status, with receipts aggregated | fetchRegisterSummary | consdcvinternetui | | How a purchase is classified, and changing it | fetchPurchaseDocument and the writes | complementoscvui | | The full XML of a document | downloadMipymeBackup | MIPYME backup |

  • The register's codes and statuses are data. REGISTER_DOCUMENT_CODES and registerDocumentCode(code) say, for each of the 49 document codes, which sides of the register show it, whether it is a DTE, its sign in the totals, whether it is listed per document and whether the buyer withholds the VAT. REGISTER_ACCOUNTING_STATUSES gives the accounting statuses of each side: four for purchases, only registered for sales.
  • A refused month reads as empty. Response codes are not looked at, so listPeriodDocuments and fetchRegisterSummary cannot tell a month the SII refuses to show from one with no movement. raw holds each row as the SII sent it. The row names come from the SII's client code and have not been seen with real documents: an absent field is null.
  • A write changes the real register on a production client. Each one is a single attempt and only the SII's OK confirms it. Anything else throws purchaseChangeNotConfirmed, and the change may have applied, so read the document again before retrying.
  • VAT amounts are final amounts, the value of each bucket after the change and not a delta.
  • The backup reads production from any client, because the service exists only there. It reads real documents even from a certification client, and a development environment that stores them needs its own gate. It needs only the certificate, not being enrolled as an issuer.
  • The backup downloads at most 20 documents at a time, so it splits the range, and by document type within a day. The whole plan comes first: a day that cannot be downloaded throws backupDayOverLimit or backupDayUnknownTypes before any download. Use onChunk for long histories, so the XML does not stay in memory.
  • Known defect: a two-day range with more than 20 documents downloads only its first day, while total counts both.
  • backupNoData is definitive. backupCaptcha and backupIndeterminate mean try later, never in a loop.

Activity verification

The SII verifies a company's activities before it lets the company issue documents with VAT. These read that verification on the same session as the rest of the portal.

import { unstable_fetchActivityVerification, listAccreditationDocuments } from 'dte-sdk/portal'

const { requests, messages } = await unstable_fetchActivityVerification(portal)
// requests[0]: { status: 'PAC', statusDescription: 'PENDIENTE DE ACREDITACIÓN', preliminaryResult: 'CONDICIONADA',
//                finalResult: null, requestedOn: '2026-11-05', accreditationDeadline: null, … }

const documents = await listAccreditationDocuments(portal) // [{ category: 'general', code: 1, description: '…' }, …]
  • unstable_fetchActivityVerification is unstable. Its shape may change in any release, a patch included. It has been read once against the real SII, for a company whose verification was pending, and the codes of an approved one are not known yet: nothing here says by itself whether the company may issue documents with VAT. Once an approved verification has been read, it becomes fetchActivityVerification.
  • It asks for the company, not for the session. The SII's own page shows the verification of whoever logged in. This sends the company's RUT, and the SII answered a session of its legal representative. A request of another RUT throws SII_PORTAL_UNEXPECTED_PAGE with step: 'otherTaxpayer' instead of passing as the company's.
  • Officials, comments and the address never come out. The SII sends them; dte-sdk drops them, and there is no raw.
  • No requests means the company never asked for the verification. An error code from the SII throws activityVerificationRefused, with its words in details.portalMessage.
  • Both read production from any client, because the app exists only there.

Certification: test sets, simulation and books

import {
  parseTestSetText,
  planCertificationSets,
  createFolioAllocator,
  issueTestSet,
  issueSimulation,
  createCertificationSalesBook,
} from 'dte-sdk/portal'

const plan = planCertificationSets(parseTestSetText(setText)) // the text the SII generated for the applicant
const options = {
  issuer,
  receiver,
  certificate,
  folios: createFolioAllocator(cafs),
  resolution: { number: 0, date: '2026-01-01' },
}
const basic = await issueTestSet(plan.basic!, options) // documents and the signed envelope, with the SET references
const sales = createCertificationSalesBook(basic, {
  period: '2026-11',
  issuerRut,
  senderRut,
  resolution,
  certificate,
  receiver: { rut, name },
})
const simulation = await issueSimulation(plan, { ...options, folios: createFolioAllocator(freshCafs) })

The SII test set is parsed line by line into cases; planCertificationSets turns them into what each set issues and the folios each type needs; issueTestSet issues the basic, exempt, guide and purchase sets through issueDocument with the SET reference (CASO <id>) and the rules the SII review expects (returned goods at the original price, cancellations taking the cancelled document's items, the issuer as receiver of purchase invoices and internal transfers); issueSimulation re-issues every case in one envelope; the three createCertification*Book functions build and sign the sales, purchase and guide books, as total submissions or as adjustments of a period the SII already holds. Everything is data: sending, waiting for the status and declaring belong to the certification runner. No real test-set text was available while writing this: the grammar follows the sets the SII delivers in the field, and the first download should be kept as a fixture.

Certification: the runner

import { createSiiClient } from 'dte-sdk'
import { createDirectoryFolioRegistry } from 'dte-sdk/node'
import {
  createPortalClient,
  createDirectoryCertificationStore,
  runCertification,
  sendTestSet,
  declareTestSets,
} from 'dte-sdk/portal'

const deps = {
  portal: createPortalClient({ environment: 'certification', certificate, companyRut }),
  sii: createSiiClient({ environment: 'certification', certificate, companyRut }),
  store: createDirectoryCertificationStore({ dir: './certification' }), // state.json and the artefacts; holds CAF keys
  registry: createDirectoryFolioRegistry({ dir: './folios' }),
  issuer, // rut, businessName and address; the activity, its code, the commune and the city have defaults
  certificate,
  // receiver: the test receiver 66666666-6 by default; resolution: number 0 with today's UTC date by default
  onProgress: (event) => console.log(event.step),
}

// The whole HTML side, resumable: run it again after a fix
const first = await runCertification(deps) // answers the exchange set, then stops with `printedSamples` until the samples are given
// The samples uploaded and submitted, it stops at `compliance`: the sworn statement needs an explicit go
const second = await runCertification(deps, {
  samples: () => readPrintedSamples('./certification/samples'),
  declareCompliance: true,
})

// One step alone, after the SII rejected a set with observations
let state = (await deps.store.load())!
state = (await sendTestSet(deps, state, { kind: 'basic' })).state
state = (await declareTestSets(deps, state)).state

The runner does what the field does, in its order: it downloads the test set once (downloading again regenerates it at the SII, so a run with a plan starts over), gets the folios through the registry (CAFs in hand first, then the quota, re-obtaining and annulling only as a last resort), sends each set and keeps its envelope and documents as artefacts for the printed samples, declares the track ids on pe_avance2 with every field in form order and reads the page again to verify (a row left empty is asked again; "ENVIO CON ERRORES O REPAROS" is a state of an envelope, not a failure of the declaration), waits for the approval (a page that shows none of the sets sent counts as approved; a read that fails only uses up its attempt), runs the books (the history of books the SII holds marks the occupied periods; a book the SII closes or never reviews is resent as an adjustment or in the next free month, at most 24 times), asks for the next stage once the three mandatory books are sent, sends and declares the simulation and confirms it, answers the exchange set and confirms the exchange, uploads the printed samples when the samples option gives them (without them it stops at printedSamples), and stops at compliance: the declaration of compliance is the sworn statement of the legal representative, it cannot be undone, and the runner makes it only with declareCompliance: true.

  • Everything is a step. obtainTestSet, prepareFolios, sendTestSet, declareTestSets, waitForTestSetsApproval, runBooks, advanceToNextStage, sendSimulation, declareSimulation, waitForSimulationApproval, runExchange, uploadPrintedSamples and declareComplianceFinal take the dependencies and the state and return the saved state with an outcome; the portal operations beneath them (downloadTestSet, declareProgress, readProgress, advanceStage, listBookHistory, declareCompliance, and those of the next section) are exported too.
  • The state is a secret. It holds the CAF files, each with a private key; the directory store writes them readable by the user only. A service may implement CertificationStore over its own storage.
  • The run keeps to field behaviour. In what the SII receives and in how the run flows, the runner does what is known to work with the SII, including where a stricter reading looked safer. The alignment design says what changed and what is not reproduced, and the improvements set aside are listed in issue 29 for after the first real certification.
  • The test sets are written in a fixed form. Their documents carry the UTC day as issue date and UTC stamps with milliseconds (a run after 21:00 in Chile, 20:00 in winter, dates them the next day), unit prices with six decimals, notes that correct a text with a zero total and no VAT rate, and every case of the guide set as a guide. The declaration carries today's Chile date for every track id, whatever day the envelope was sent on.
  • The uploads of the run are sent again only for a network failure that names a reset socket, a timed-out connection or an unresolved host: eight attempts, 2 s growing by 1.8 up to 15 s. Only STATUS 0 with a TRACKID element is a sent file; STATUS 7 is a duplicate recovered by that element; STATUS 99 is a failed send.
  • The books start over on every run. The period is the UTC month the test set was read in (the current UTC month when the set brought no purchase book), or the first free month below it; which books stand approved is what the portal shows, not what the state remembers, so a run resumed at the books sends again every book the portal does not show as REVISADO CONFORME. When a period is taken every book goes again as an adjustment, the guide book and the exempt purchase book too; the books the declaration names with "ENVIO CON ERRORES O REPAROS" or the SOAP status rejects are not awaited but sent again; no portal read of the phase stops the run. A row the page leaves rejected, closed or declared keeps that search going even when it is of a book this run does not send (an exempt purchase book the test set did not bring): nothing is sent for it, but the books are declared again and that row is awaited, up to 24 times.
  • The portal decides the stage. With the three mandatory books sent the runner asks for the next stage whether or not the books were approved, and stops with the portal's sentence when the portal does not move. runBooks ends incomplete only when a mandatory book could not be sent.
  • The simulation is not written as the test sets are: the local date of the process, stamps in UTC to the second, the envelope stamped in Chile time, plain unit prices, and a guide without TpoDespacho. Run the process with TZ=America/Santiago.
  • The declaration of compliance counts as made when the last page no longer shows its form. That is a reading of the page, not a confirmation from the SII; keep the page the store saves and check it.
  • The folios of a run are checked against the SII. Before a set is issued the SII is asked whether it already holds the folios the run is about to use (as the company, up to 30 queries per CAF); one it holds is marked used. A range obtained again is used only through the folios the SII showed free, and takes the place of the CAFs in hand of its type. A maximum that cannot be read counts as no cap, a blocked type is not asked for but obtained again, a maximum below the need is asked in as many batches as folios are missing, and after annulling the maximum is read again. Annulling (the last resort: unused ranges of the last 180 days, 10 in each of up to four passes) never touches a range the run downloaded, and the ranges the SII reported annulled are kept in the state (annulledRanges) and not tried again. Every CAF obtained is saved in the state at once, so a step that ends short leaves it in hand for the next attempt.
  • The operator gives less. Without receiver the documents go to the test receiver (DEFAULT_CERTIFICATION_RECEIVER); without resolution every envelope of the run carries number 0 and the UTC date of the day the run started, which is only right on the day the SII resolved the application, so give resolution.date on any other day; the issuer's line of business, activity code, commune and city fall back to DEFAULT_CERTIFICATION_ISSUER. A receiver's line of business is cut at its 40 characters instead of being refused, the issuer's too where the issuer is the receiver of its own documents (the purchase set, internal transfers, the simulation); as issuer it keeps its 80.
  • The waits are the field's: 5 polls of 10 s for the sets, 40 of 15 s for the books, 30 of 10 s for the simulation, 10 declaration attempts 5 s apart after 10 s. Inject sleep and now to test without waiting.
  • Nothing here has been run against the portal yet. Every page shape comes from field behaviour; the first enrolled run should keep the artefacts the store writes (every portal answer is there) as fixtures.

Certification: the exchange and the printed samples

import { runCertification, createExchangeAnswers } from 'dte-sdk/portal'
import { readPrintedSamples, writePrintedSamples, findEnvelopeFiles } from 'dte-sdk/pdf'

// After the simulation: the exchange set is downloaded from the exchange portal, answered and confirmed; then the
// samples are rendered from the envelopes the store kept and uploaded to the samples portal
const samplesDir = './certification/samples'
await writePrintedSamples({
  xmlFiles: await findEnvelopeFiles('./certification'),
  outDir: samplesDir,
  transferable: true,
})
const run = await runCertification(deps, { samples: () => readPrintedSamples(samplesDir) })
// run.stopped: `compliance` once the samples are submitted; `rejected` with the SII's reasons in `run.detail`

// The set could not be downloaded (run.stopped `exchange`, run.detail names `intercambio/set-intercambio.xml`):
// save it by hand from the exchange portal into that artefact of the store, or pass it
await runCertification(deps, { exchangeSetXml: await readFile('set-intercambio.xml', 'utf8') })

// The answers alone, for a set in hand
const answers = createExchangeAnswers(setXml, {
  companyRut,
  certificate,
  contact: { name: 'Contacto', mail: '[email protected]' },
})

The exchange is the SII's set of documents addressed to the company; the answers are the reception response (respuesta-recepcion-envio.xml), the receipts of Ley 19.983 (envio-recibos.xml) and the commercial approval (respuesta-aprobacion-comercial.xml), each signed, and valid against the official schemas in the tests. The runner downloads the set once (it stays as an artefact), validates the user on the exchange portal, uploads the three answers and confirms the exchange, which moves the application to the printed samples. The printed samples are the PDFs of every document the sets and the simulation issued, as writePrintedSamples lays them out: the runner creates a review on the samples portal (or resumes one left in INGRESO), uploads every PDF, submits the review and reads its state again; a review the SII rejects at once comes back as rejected with a summary of how the SII filed each PDF. The portal operations beneath (downloadExchangeSet, validateExchangeUser, uploadExchangeAnswers, confirmExchange; readSamplesReview, createSamplesReview, uploadSample, submitSamplesReview, readSamplesDetail) and the GWT-RPC codec (gwtBody, gwtAnswer) are exported too. The exchange portal is called with the pair of hashes known to work; the samples portal's are discovered once on the way in, with the known pair as the fallback; gwt: { permutation, policy } overrides either.

  • One stage, one warm-up. The run enters each portal once per stage of work: once for the download of the exchange set, once for the validation, the three uploads and the confirmation, and once for the whole of the printed samples. The cookies the warm-up sets live only for that stage. withExchangeStage and withSamplesStage open a stage for a caller that chains the operations by hand (stage in their input); alone, each operation opens its own.
  • These calls are sent once and wait as long as it takes: no retry (but for a PDF upload the portal answers with a 5xx) and no time-out of their own, with a fixed set of headers for each portal and the answers read as UTF-8. Pass timeoutMs to bound them. Inside a stage each call reads its own answer whatever its status: the client neither throws on a 502, 503 or 504 nor looks for the firewall's page there, so an error on the user validation or on the set validation does not stop the exchange.
  • A set that cannot be downloaded, for any reason, stops the run with exchange and asks for the set by hand.
  • The exchange set is read loosely: it is not checked for well-formedness, a number that does not parse is 0, a missing date or RUT is empty, and only a set without documents (or one the parser cannot read at all) fails. The names of the PDFs travel as given.
  • Nothing here has been run against the two portals yet. Every wire and wording comes from field behaviour; the first enrolled run should keep every artefact the store writes under intercambio/ and muestras/ as fixtures.
  • The exchange answers are regenerated and uploaded on every run that reaches the portal; only the set is kept.
  • A review left in INGRESO (a run cut off while uploading) is