Skip to content

Receipts

The certification of receipts (boletas electrónicas, type 39) is an application of its own: its own portal, its own test set, its own envelope (EnvioBOLETA), its own folio consumption report (RCOF) and its own declaration. It has no runner: after you send the set, the SII validates it on its own time and answers by e-mail, so you chain the pieces yourself and read the status again later.

Nothing here has been run against the receipts portal yet. It needs a company enrolled in the receipts application. Every wire is still unproven, and the tests use a synthetic set text. The receipts envelope and the folio consumption report do validate against the official schemas in the tests. See Status.

declareReceiptsCompliance cannot be undone. It authorises the company to issue receipts in production. It reads the status first and sends nothing when the set is not approved yet or the declaration was already made, but once it sends, it is final.

The pieces, in order#

1

Download the test set

downloadReceiptsTestSet(portal, { providerMail }) returns the text of the set. parseReceiptsTestSet(text) reads it into cases.

2

Issue

issueReceiptsSet(cases, options) issues one receipt per case and signs the EnvioBOLETA.

3

Send

sendReceiptsSet(sii, issued) uploads the envelope, reads its status and sends the folio consumption report. declareReceiptsTrackId(portal, { trackId }) gives the envelope's track id to the portal.

4

Wait, then read the status

The SII validates the set and answers by e-mail. readReceiptsStatus(portal) tells you when it is approved.

5

Declare compliance

declareReceiptsCompliance(portal, ...) makes the declaration. verifyReceiptsAuthorization checks the result.

A complete example#

TypeScript
import { readFileSync } from 'node:fs'
import { createSiiClient, loadCertificateFromPkcs12, parseCaf } from 'dte-sdk'
import {
  createPortalClient,
  createFolioAllocator,
  downloadReceiptsTestSet,
  parseReceiptsTestSet,
  issueReceiptsSet,
  sendReceiptsSet,
  declareReceiptsTrackId,
  readReceiptsStatus,
  declareReceiptsCompliance,
  verifyReceiptsAuthorization,
} from 'dte-sdk/portal'
 
const certificate = loadCertificateFromPkcs12(readFileSync('firma.pfx'), process.env.PFX_PASSWORD!)
const companyRut = '76543210-3'
const portal = createPortalClient({ environment: 'certification', certificate, companyRut })
const sii = createSiiClient({ environment: 'certification', certificate, companyRut })
 
// The receipts application is its own: its portal, its test set, its declaration
const { text } = await downloadReceiptsTestSet(portal, { providerMail: 'contacto@example.test' })
const issued = await issueReceiptsSet(parseReceiptsTestSet(text), {
  issuer: { rut: companyRut, businessName: 'Comercial Andina SpA', address: 'Av. Los Aromos 1540' },
  certificate,
  folios: createFolioAllocator([parseCaf(readFileSync('caf-39.xml', 'utf8'))]),
  resolution: { number: 0, date: '2026-01-15' }, // number 0 and today's UTC date when left out
})
 
// The envelope, 20 s, its status, then the folio consumption report
const sent = await sendReceiptsSet(sii, issued)
sent.outcome // 'sent' | 'envelopeRejected' | 'consumptionFailed'
// 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 e-mail. Later:
const status = await readReceiptsStatus(portal) // readyToDeclare once approved (P90); declared after (P91)
if (status.readyToDeclare)
  await declareReceiptsCompliance(portal, {
    provider: { rut: companyRut, name: 'Comercial Andina SpA', mail: 'contacto@example.test' },
    businessName: 'Comercial Andina SpA',
  })
 
// Is type 39 stampable in production? And what does the certification progress page say?
const check = await verifyReceiptsAuthorization({ production: productionPortal, certification: portal })
check.production?.authorized
check.certification?.state // read it yourself: see below

The test set#

downloadReceiptsTestSet(portal, { providerMail }) reads the list of representatives and the application record first (the portal wants both before the download; their answers are not judged), then downloads the set with the company, the representative named in the session's rut and dv cookies and the provider's mail. The HTTP status is not looked at: any body of ten characters or more is the set. It returns { text, representativeRut }. It throws SII_PORTAL_UNEXPECTED_PAGE when the download is no set, and MISSING_FIELD (representative) when the session cookies name none.

parseReceiptsTestSet(text) reads the plain text of cases, by position:

  • A line starting with CASO-<n> opens a case.
  • A line that names Item, Cantidad and Precio (in exact case) turns the item mode on for that case; a set in capitals has no such header and so no items.
  • In item mode, every other non-empty line that is not a rule of = is an item when it splits, by tabs or runs of two or more spaces (empty parts dropped), into a name, a quantity and a price. A decimal comma is accepted in the quantity; the price drops every thousands dot and its first comma. An item whose name says exento is exempt.
  • A line starting with OBSERVACION gives its text, up to the first quote, to the case.
  • Nothing before the first case is kept, and a text without cases gives an empty list.

It returns ReceiptsTestCase[]: number, items (name, quantity, unitPrice with VAT included, exempt) and observations.

issueReceiptsSet#

issueReceiptsSet(cases, { issuer, certificate, folios, resolution?, now? }) issues one type 39 receipt per case, in order, each with the next folio of the source (FolioSource), then signs the EnvioBOLETA that carries them.

  • The receiver is the final consumer (66666666-6). The set's prices are taken as including VAT. An exempt item is written as exempt. If an observation of the case says unidad de medida en Kg., that unit goes on its first item.
  • Every receipt carries one reference with the code SET and the reason CASO-<n>, as a receipt reference.
  • The issuer takes the same defaults as the runner (DEFAULT_CERTIFICATION_ISSUER); the RUT, business name and address have none. resolution is number 0 and today's UTC date when left out, field by field.
  • Dates and stamps. The receipts are dated with the UTC day and each is stamped and signed with the UTC instant to the second, with no zone. Only the envelope carries the time of Chile. The clock is read once for the issue date, once per receipt for its stamps and once for the envelope.
  • The sender of the envelope is the certificate holder (the issuer when the certificate names none); the receiver is the SII.

It returns { receipts, envelope, firstFolio, lastFolio }; each receipt has caseNumber, folio and the signed document. It throws MISSING_FIELD (cases) without cases and FOLIO_OUT_OF_RANGE from the folio source. The signed XMLs are in the result: keep them.

sendReceiptsSet#

sendReceiptsSet(sii, issued, { sequence?, sleep?, onProgress? }) takes the result of issueReceiptsSet, or anything with its signed envelope, and does four things:

  1. Uploads the envelope. The reply is read as the uploads of the certification run are: only STATUS 0 with a TRACKID element is a sent file; STATUS 7 with a track id is also taken as sent, and without one it is a duplicate that skips to the report; STATUS 99 and any other reply is a failure and throws SII_UPLOAD_REJECTED. Each upload is sent again only for a network failure, with the retries of the run (8 attempts, 2 s growing by 1.8 up to 15 s).
  2. Waits 20 s and reads its status once (when it has a track id). A rejection ends here as envelopeRejected. A query that never got through is logged and blocks nothing; so does an answer without a state, or with an unknown one.
  3. Sends the folio consumption report (RCOF) built from the envelope, with the sequence number sequence plus the attempt. sequence is 100 by default (also when it is 0). If the SII rejects a report, the next attempt takes the next sequence number, up to ten attempts. A STATUS 7 with no track id is a duplicate: three duplicates in a row mean today's report was already accepted (the certification environment takes one per company per day), and the result says consumptionAlreadySent. A STATUS 99 or another refusal ends it as consumptionFailed.
  4. Reads the report's status once, after 30 s. A success or an intermediate state accepts it, and so does a query that never got through. Anything else (a rejection, no state, an unknown state) tries the next sequence number, unless its gloss speaks of authorisation or signing, which ends it as consumptionFailed.

The SII's objections to the report are expected in certification and block nothing. Ten attempts without an accepted report still end in sent.

Result fieldMeaning
outcomesent: the envelope was uploaded and not rejected, and the report accepted, already sent today, or objected to. envelopeRejected. consumptionFailed: a report that could not be uploaded, or one refused for lack of authorisation
notAuthorizedThe SII's gloss or the error speaks of authorisation or signing: the company cannot sign receipts yet
envelopeTrackId, envelopeDuplicateThe envelope's track id, to declare on the portal (null when the SII already had the envelope and named none), and whether the SII already had it
consumptionTrackId, consumptionSequenceThe accepted report's track id and sequence number, or null
consumptionAlreadySentThree reports in a row were duplicates
consumptionsEvery report tried, in order: sequence, xml, trackId, outcome (accepted, duplicate, rejected, failed) and detail
detailThe gloss or error that ended it, when it did not end in sent

The envelope's track id is in the result whichever way it ended. onProgress receives BOLETA_SOAP_CHECK and BOLETA_RCOF.

resendReceiptsConsumption(sii, { envelopeXml, sequence }) builds the report again from an envelope already sent and uploads it once, with the sequence number you give. STATUS 0 or 7 with a TRACKID element is a sent report; a STATUS 7 that names no track id, a STATUS 99 and any other reply are refusals (SII_UPLOAD_REJECTED). It returns { sequence, xml, trackId, duplicate }.

declareReceiptsTrackId#

declareReceiptsTrackId(portal, { trackId }) gives the envelope's track id to the receipts portal, exactly as given, so the SII validates the set. It is sent once. It throws MISSING_FIELD (trackId) for an empty one and SII_PORTAL_REJECTED (receiptsTrackIdRefused) when the answer is not //OK. It returns the raw answer as { body }.

readReceiptsStatus#

readReceiptsStatus(portal) returns { enrolled, readyToDeclare, declared, state }:

  • enrolled: the company has valid representatives on the receipts portal.
  • readyToDeclare: the SII approved the set (P90): the declaration can be made.
  • declared: the declaration was already made (P91).

P90 and P91 are searched in the raw answer. The portal answers an empty list for state 90 both before the approval and after the declaration, so the status asks for state 91 too, to tell the two apart.

declareReceiptsCompliance#

declareReceiptsCompliance(portal, { provider, businessName, link? }) makes the declaration of compliance of the receipts application. It reads the status first:

outcomeMeaning
declaredThe declaration was made now
alreadyDeclaredIt had been made before. Nothing is sent
notReadyThe SII has not approved the set yet. Nothing is sent
  • The software provider is mandatory (provider: rut, name and mail, each required): 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. businessName is mandatory too; link is www.sii.cl by default.
  • The representative is the owner of the session, read from the session's cookies; the portal's own list is only the fallback. When no cookie gives the check digit, the declaration sends 8. With two people enrolled for a company, any other choice is refused. If no representative can be found it throws MISSING_FIELD (representative).
  • Nothing is sent without the application's own Long. A record that lacks it throws SII_PORTAL_UNEXPECTED_PAGE. A record without an authorisation date is declared with the date empty.
  • The post that declares is judged by //OK alone; otherwise SII_PORTAL_REJECTED (receiptsComplianceRefused). A company with no representatives is receiptsNotEnrolled.
  • The declaration is dated with the local date of the process clock, so run the process with TZ=America/Santiago.

verifyReceiptsAuthorization#

verifyReceiptsAuthorization({ production?, certification?, companyRut? }) reads whether the company may issue receipts, on one side or both. A side is null when you do not give its client.

  • Production (production, a PortalClient on production): a GET of the stamping entry page and a POST of its form with the company. authorized is true when type 39 is among the document types the form offers (types).
  • Certification (certification): one GET of the progress page. authorized is true when the receipts row says conform, approved or authorised, or a line of the page says the receipts are authorised. The check reads a row as authorised when its state contains conform, aprobad or autoriz: read the state it returns yourself.

It never throws. A query the portal does not answer is queryFailed: true (with errorCode and error), never "not authorised": a stamping form the portal does not serve says nothing about authorisation.

How the receipts portal is called#

Every request to the receipts portal is written for it alone: only the headers the call names and the session's cookies, sent once (no retry), with a 30 s time-out (20 s for verifyReceiptsAuthorization), and read as UTF-8 whatever its status, the firewall's page included. There is no warm-up, no hash lookup and no redirect. The posts that declare are judged by //OK. The portal's hashes are the pair known to work (RECEIPTS_PORTAL.known); pass gwt: { permutation, policy } in the input of an operation if they go stale. Pass sessionChecked: true when an earlier operation of the same run already checked the saved session. Every operation takes optional call overrides as its last argument.

RECEIPTS_PORTAL is its GwtPortal entry. Its hashes are not discovered, and its moduleName is what its interface suggests, not a recorded fact.

A receipt's own reference#

The receipts schema gives Referencia no date and a free-text code, so a receipt takes receiptReferences ({ code, reason, documentType?, folio? }) and refuses references. That is what issueReceiptsSet writes, with the code SET and the reason CASO-<n>. See Documents.

Next steps#