EvoX Genesis
EvoX Genesis는 고수준 목표를 재귀적 소프트웨어 세계로 전환해, 유한 에이전트가 개발하고 승인된 결과만 유지됩니다
TypeScript SDK로 칠레 전자세금문서(DTE)를 생성·서명·전송 및 SII 연동
A TypeScript SDK to build, stamp, sign and send Chilean electronic tax documents (DTE) to the SII, with the SII books, the SII portal and its folios, the certification and the printed samples.
개발 도구TypeScript · JavaScriptGitHub @arrowsw온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기TypeScript SDK for 🇨🇱 Electronic Tax Documents (DTE).
README 전체를 불러오는 중입니다.
README 전문 보기 ↗⚠️ Not ready for production.
dte-sdkis 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.
MntBruto), global discounts, purchase-invoice withholding.EnvioDTE and EnvioBOLETA with cover and signed SetDTE..pfx/.p12, legacy and modern) or PEM; the
holder RUT is read from the certificate.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.totalAmount, issuer.businessName)
that maps to the official SII tags (MntTotal, RznSoc) through a codec.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.0.1.0 is the first release with a public API, and every area this README describes ships in it. 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:
| Area | Entry | Against the real SII |
|---|---|---|
| Documents, stamps, signatures and envelopes | dte-sdk | Not sent yet |
| SII web services client | dte-sdk | Not run yet (issue 8) |
| Public registry of authorized issuers | dte-sdk | Not run yet |
| SII books | dte-sdk | Not sent yet; they validate against the official schemas in the tests |
| Portal session and company data | dte-sdk/portal | Login, logout, taxpayer data and the refusal pages, for a company not enrolled as an issuer |
| Folios and CAF requests | dte-sdk/portal | Not run yet: they need an enrolled company; the tests replay the pages the certification environment served to one |
| Purchase and sales register, MIPYME backup | dte-sdk/portal | Read-only requests answered for a company with no documents; rows, downloads and writes not seen yet |
| 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 |
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.
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
| Import from | What it holds | Runs on |
|---|---|---|
dte-sdk | Documents, stamps, signatures, envelopes, books 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 | Node |
dte-sdk/portal | SII portal, folios and CAF requests, the folio registry, purchase and sales register, MIPYME backup, 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.
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,
})
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.
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.
receiverAnswer(events, receivedAt, today) reads what the receiver answered, with no request:
the events 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). 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.{ signal, timeoutMs, retry } as a last argument.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.
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.
xml holds the private key of the CAF. Where you keep it is up to you.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.MAX_AUTOR and FOLIOS_DISP appear only while the SII rations a type. Unused folios lower what it
authorizes and eventually block stamping.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.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.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.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 ).
It does not sign in on demand: call it with a session that is alive.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.
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). A folder or file that cannot be read is
skipped. 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.
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.
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 |
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.OK confirms it.
Anything else throws purchaseChangeNotConfirmed, and the change may have applied, so read the document again before retrying.backupDayOverLimit or backupDayUnknownTypes before any download. Use
onChunk for long histories, so the XML does not stay in memory.total counts both.backupNoData is definitive. backupCaptcha and backupIndeterminate mean try later, never in a loop.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.
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.
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.CertificationStore over its own storage.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.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.
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.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.exchange and asks for the set by hand.intercambio/ and muestras/ as fixtures.INGRESO (a run cut off while uploading) is resumed, every PDF uploaded again.import { createSiiClient } from 'dte-sdk'
import {
createPortalClient,
createFolioAllocator,
downloadReceiptsTestSet,
parseReceiptsTestSet,
issueReceiptsSet,
sendReceiptsSet,
declareReceiptsTrackId,
readReceiptsStatus,
declareReceiptsCompliance,
verifyReceiptsAuthorization,
} from 'dte-sdk/portal'
const portal = createPortalClient({ environment: 'certification', certificate, companyRut })
const sii = createSiiClient({ environment: 'certification', certificate, companyRut })
// The receipts application is its own: its portal, its test set, its declaration. The pieces, in the order to chain them
const { text } = await downloadReceiptsTestSet(portal, { providerMail })
const issued = await issueReceiptsSet(parseReceiptsTestSet(text), {
issuer, // rut, businessName and address; the rest has the defaults of the document runner
certificate,
folios: createFolioAllocator([caf39]),
resolution, // number 0 and today's UTC date when left out
})
const sent = await sendReceiptsSet(sii, issued) // the envelope, 20 s, its status, then the folio consumption report
// envelopeTrackId is null when the SII said it already had the envelope and named no track id
if (sent.envelopeTrackId !== null) await declareReceiptsTrackId(portal, { trackId: sent.envelopeTrackId })
// The SII validates the set on its own and answers by mail. Later:
const status = await readReceiptsStatus(portal) // readyToDeclare once the set is approved (P90); declared after (P91)
if (status.readyToDeclare) await declareReceiptsCompliance(portal, { provider: { rut, name, mail }, businessName })
const check = await verifyReceiptsAuthorization({ production: productionPortal, certification: portal })
// check.production?.authorized: type 39 can be stamped in production
The receipts test set is a plain text of cases; each case is one receipt of type 39 to the final consumer, with prices
that include VAT and a reference SET / CASO-<n>. sendReceiptsSet uploads the EnvioBOLETA, waits 20 s and reads
its status, then sends the folio consumption report (RCOF) built from the envelope, starting at sequence number 100 and
trying the next one after a rejection, up to ten times; three duplicates in a row mean today's report was already
accepted (the certification environment takes one per company per day). Both uploads are read as the uploads of the
document runner are: only STATUS 0 with a TRACKID element is a sent file, STATUS 7 is a duplicate, STATUS 99
is a failure, and only a network failure is sent again. Each status is asked once: a query that never got through
counts as accepted, an answer without a state or with an unknown one as a rejection. The SII's objections to the
report are expected in certification and block nothing: ten attempts without an accepted report still end in sent. The other
outcomes are envelopeRejected, and consumptionFailed when the report could not be uploaded or was refused for lack
of authorisation (notAuthorized); the envelope's track id, when the SII named one, is in the result either way. The XMLs come back in the
result. There is no runner here: the SII's validation of the set takes its own time, and the caller reads the status
again later.
declareReceiptsCompliance authorises the company to issue receipts in
production. It reads the status first: alreadyDeclared and notReady are outcomes, and nothing is sent. Nor is it
sent without the application's own Long: a record that lacks it is SII_PORTAL_UNEXPECTED_PAGE. A record without an
authorisation date is declared with the date empty.provider): the SII answered some companies with a bare HTTP 500 when they
were declared as their own provider by default. A company that is its own provider passes itself.8. With two people enrolled for a
company, any other choice is refused.OBSERVACION in exact case); the receipts carry the UTC day and each is stamped with the UTC instant to the second,
while the envelope keeps the time of Chile; every request to the receipts portal carries only a fixed set of
headers, is sent once, waits 30 s and is read as UTF-8 whatever its status, the firewall's page included;
P90 and P91 are searched in the raw answer; the posts that declare are judged by //OK alone.
verifyReceiptsAuthorization reads a row as authorised when its state contains conform, aprobad or autoriz:
read the state it returns yourself.Referencia no date and a free-text code, so a
receipt takes receiptReferences ({ code, reason, documentType?, folio? }) and refuses references.verifyReceiptsAuthorization never throws. A stamping form the portal does not serve is queryFailed, not "not
authorised".import {
parsePrintableDocuments,
renderPrintedSample,
renderPrintedSamples,
writePrintedSamples,
findEnvelopeFiles,
readPrintedSamples,
} from 'dte-sdk/pdf'
// The signed XML is the input: one document the SDK issued, or any EnvioDTE/EnvioBOLETA
const [doc] = parsePrintableDocuments(signed.xml)
const pdf = await renderPrintedSample(doc, {
siiOffice: 'S.I.I. - SANTIAGO ORIENTE', // default: the office of the issuer's commune
resolution: 'Res. Ex. SII N° 80 del 2014',
})
const transferableCopy = await renderPrintedSample(doc, { transferable: true }) // adds the acknowledgement box and CEDIBLE
// Every document of every envelope, in memory, classified into the test and simulation sets
const result = await renderPrintedSamples({
envelopes: [{ name: 'envio-set-basico.xml', xml: envelope.xml }],
transferable: true,
})
// From the files of a certification run into SET-PRUEBAS and SET-SIMULACION
const files = await findEnvelopeFiles('./certification/envelopes')
const written = await writePrintedSamples({
xmlFiles: files,
outDir: './certification/samples',
logoPath: './logo.png',
transferable: true,
})
// Back from disk, for the certification runner: every .pdf under the directory, the test set folder first
const samples = await readPrintedSamples('./certification/samples')
The printed sample follows the SII printed-samples manual: one page 21.5 cm wide and as tall as its content between 11
and 33 cm, the red type box with the RUT, the type name and the folio, issuer, date, receiver, references, detail,
totals, the acknowledgement box and CEDIBLE mark on the transferable copy (33, 34, 43, 46 and sale guides), and the
PDF417 stamp of the TED with its legends. The legend under the type box is siiOffice when given; otherwise the office
of the issuer's commune (CmnaOrigen), looked up with siiOfficeForCommune in SII_OFFICES (355 commune names in 40
offices, every official commune included, ignoring case, surrounding spaces and accents), and S.I.I. - SANTIAGO CENTRO for a commune the table lacks.
The stamp encodes the signed bytes exactly (renderTedBarcode gives the PNG
alone). It needs Node and the optional peers pdf-lib and bwip-js, which are loaded on first use: without them
the render functions throw DEPENDENCY_UNAVAILABLE. Failures of a batch come back as data (failures[].code), never as thrown errors; a folio that is not a run
of digits is an INVALID_FOLIO failure, since it would name the file. Retentions, the commission block of settlement
invoices and a stamp raster above 300 DPI are not printed yet: see the
printed samples design.
import { DteError, isDteError } from 'dte-sdk'
try {
createDocument({ ...input, items: [{ name: 'x'.repeat(81), unitPrice: 1 }] })
} catch (err) {
if (isDteError(err)) err.code // 'FIELD_TOO_LONG'; err.details → { field: 'NmbItem', maxLength: 80, length: 81, value }
}
Messages never include RUTs, names or item text; the offending value lives in details.
The SII web services use the same DteError (TRANSPORT_FAILED, TRANSPORT_TLS, REQUEST_TIMEOUT,
SII_UPLOAD_REJECTED, SII_TOKEN_INVALID, WAIT_TIMEOUT, …); isRetryableError(err) tells which ones are
worth another try. Their messages carry only codes and numbers; a response excerpt (tokens and seeds
redacted) lives in details, never in the message. Transport errors carry details.operation (and
timeoutMs on REQUEST_TIMEOUT), SII_HTTP_ERROR carries httpStatus, url and body, and the
details.status of SII_UPLOAD_REJECTED is the DTEUpload status as a number.
classifyUploadStatus('EPR') → 'accepted', classifyUploadStatus('RCT') → 'rejected',
classifyUploadStatus('-11') → 'queryError' (retry). classifyDocumentStatus('FAU') → 'notReceived'.
Spanish glosses via uploadStatusMessage and documentStatusMessage. The same pattern covers the
DTEUpload reception statuses (classifyReceptionStatus, receptionStatusMessage) and the acceptance/claim
registry (claimResponseMessage, transferabilityMessage, CLAIM_ACTIONS).
npm install # also builds via `prepare`
npm test # unit, property, type-level and dist smoke tests
npm test -- --coverage
npm run build # esbuild (ES modules, one file per entry plus shared chunks) + tsc declarations
npm run lint # ESLint flat config + Prettier
npm run size # bundle size guard (.size-limit.json)
npm run scan # no real RUTs, local paths or secrets in tracked files
SII_LIVE=1 SII_CERT_PFX=… SII_CERT_PASSWORD=… npx jest tests/live # opt-in smoke against maullin (needs a real certificate)
Releases are tag-triggered and published from CI with npm provenance — see RELEASING.md. The design of each area and the decisions behind it are under docs/superpowers/specs, and docs/decisions/entry-points.md explains the four entries.
Contributions are welcome — feel free to open issues for bugs and feature requests, or submit a pull request.
MIT © Arrow Software
reasonREVISADO CONFORMErunBooks ends incomplete only when a mandatory book could not be sent.TpoDespacho. Run the process with TZ=America/Santiago.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.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.sleep and now to test without waiting.