Skip to content

Stamp

Every electronic tax document carries an electronic stamp, the TED (timbre electrónico). The SII authorizes a range of folios with a CAF (Código de Autorización de Folios), an XML file that holds the range, the authorized issuer and the private key that signs each stamp. parseCaf reads that file, stampDocument builds the TED of a document and signs it with the CAF key, and verifyTed checks a stamp against the CAF public key.

Everything on this page lives in the dte-sdk entry point.

The CAF XML holds a private key (RSASK). Treat the file as a secret, like a password or a certificate: do not commit it, do not log it, do not put it in a bucket anyone can read. Whoever has it can stamp documents with your folios. Caf.privateKeyPem and Caf.xml hold the secret after parsing.

The stamp is tested against synthetic data, but no stamped document has been sent to the real SII yet. See Status.

Usage#

TypeScript
import { readFileSync } from 'node:fs'
import { createDocument, parseCaf, stampDocument, verifyTed } from 'dte-sdk'
 
const caf = parseCaf(readFileSync('caf-33.xml', 'utf8'))
 
const document = createDocument(input) // a type 33 with a folio inside the CAF range
const stamped = stampDocument(document, caf)
 
stamped.ted.folio // 7
stamped.ted.totalAmount // 119000
stamped.ted.stampedAt // '2026-11-15T15:30:00', Chile time
stamped.ted.xml // '<TED version="1.0"><DD>…</DD><FRMT algoritmo="SHA1withRSA">…</FRMT></TED>'
 
verifyTed(stamped.ted.xml, caf) // true

stampDocument returns a StampedDocument: your Document plus a ted. It does not change the document: stamped.header equals document.header. To get the final XML you still sign the stamped document (see Sign), or let issueDocument run all the steps.

Parse a CAF#

parseCaf(xml) takes the text of the CAF file as the SII issued it and returns a Caf. It validates the structure, the document type, the range and the private key, and throws CAF_INVALID when anything is missing or malformed. Since 0.4.0 it also throws CAF_INVALID for a DOCTYPE, or when the text holds more than one <CAF or </CAF anywhere, in a comment too: the <CAF> block copied into every TED must be the CAF its fields are read from. A CAF as the SII issues it reads as before.

Since 0.7.0 it checks that the private key RSASK is the pair of the public key RSAPK the SII signed in the DA, as the SDK already checks a certificate's key. Before, a CAF with another key parsed, and every stamp signed with it failed the SII's check. Keys that are not a pair throw CAF_INVALID with the reason RSASK is not the pair of RSAPK. They are compared as numbers, so a modulus wrapped over lines or with a leading zero byte still pairs, and a CAF the SII issued always does. Its fields also read numeric character references (&#209;, &#xD1;) as the DOM of verifyCafSignature and the SII read them: a CAF whose writer turned an Ñ into &#209; gives issuerName with the Ñ, where it kept the reference, and one with a reference in RE or TD no longer fails to parse. A reference to a character XML does not allow stays as written, and no other entity is expanded. The <CAF> block kept for the TED is still the text as the SII issued it.

FieldSII tagMeaning
issuerRutDA/RERUT of the authorized issuer, validated with rut.ts (INVALID_RUT when it is not valid).
issuerNameDA/RSIssuer business name, as the SII wrote it (empty when absent).
documentTypeDA/TDThe document type the range is for. One of the nine supported types, or CAF_INVALID.
rangeDA/RNG{ from, to }, inclusive.
authorizationDateDA/FAYYYY-MM-DD, or null when the file has none.
publicKeyDA/RSAPK{ modulus, exponent }, the key that verifies the TED signatures.
keyIdDA/IDKKey identifier. 100 (CERTIFICATION_KEY_ID) marks a certification-environment CAF, and 300 a production one.
siiSignatureFRMAThe SII signature over the authorization data, without whitespace.
privateKeyPemRSASKThe private key that signs each TED. Secret.
xmlCAFThe <CAF>…</CAF> block exactly as the SII issued it, with the whitespace between tags removed. It is copied verbatim into every TED.
TypeScript
const caf = parseCaf(readFileSync('caf-33.xml', 'utf8'))
 
caf.issuerRut // '76543210-3'
caf.documentType // 33
caf.range // { from: 1, to: 100 }
caf.authorizationDate // '2026-10-31'
caf.keyId === CERTIFICATION_KEY_ID // true for a certification CAF

parseCaf needs the whole file text, including RSASK. It does not check the SII signature (FRMA) of the CAF, the proof that the SII issued it: verifyCafSignature(caf.xml) does, with the keys the SII signs every CAF with (see The SII's keys and the environment).

CAFs and expiry#

A CAF for invoices and notes (33, 46, 56 and 61) expires six months after its authorization date. The SII rejects every document stamped with an expired CAF (CAF-3-517), even if the envelope arrives later than it was stamped. The other types (34, 39, 41, 43, 52) do not expire.

TypeScript
import { cafExpiresAt, isCafExpired } from 'dte-sdk'
 
cafExpiresAt(caf) // Date, or null for types that never expire or a CAF without date
isCafExpired(caf) // true when it has expired or expires within a day
isCafExpired(caf, { now: new Date('2026-10-07T18:35:00Z'), marginMs: 0 })
FunctionResult
cafExpiresAt(caf)The expiry instant: six months after authorizationDate, at midnight of continental Chile (UTC-4). A day the sixth month does not have is its last day: a CAF authorized on 2026-08-31 expires on 2027-02-28, not in March. null for types that do not expire, or when the CAF has no authorization date. For a CAF authorized on 7 April 2026 it is 2026-10-07T04:00:00.000Z.
isCafExpired(caf, options?)true when the CAF has expired or expires within marginMs. options.now is the clock; options.marginMs defaults to one day (24 hours).

The one-day margin is on purpose: the envelope can reach the SII later than you stamped the document, and a CAF that expires tonight is not safe to use. stampDocument runs this check for you (see below).

Folio range#

TypeScript
import { assertFolioInRange, availableFolios, isFolioInRange } from 'dte-sdk'
 
availableFolios(caf) // 100, the size of the range
isFolioInRange(caf, 100) // true
isFolioInRange(caf, 101) // false
assertFolioInRange(caf, 101) // throws FOLIO_OUT_OF_RANGE

isFolioInRange(caf, folio) is true for an integer inside the inclusive range. assertFolioInRange(caf, folio) throws FOLIO_OUT_OF_RANGE otherwise, with details { folio, from, to, type }. availableFolios(caf) is the number of folios the CAF authorizes (to - from + 1): it counts the whole range, not the folios you have not used yet.

These functions do not remember anything. Which folios you already used is your bookkeeping: do not stamp the same folio twice and do not skip folios without a reason. The folio registry in dte-sdk/portal (createFolioRegistry) keeps that ledger; see Portal.

Stamp a document#

stampDocument(document, caf, options?) checks the document against the CAF, builds the exact DD string, signs it with the CAF private key and returns the document with its ted.

OptionMeaning
nowClock for TSTED and for the expiry check.
checkExpiryCheck the CAF expiry (default true). Set it to false only when you know why.
stampedAtTSTED exactly as given, instead of the Chile time of now.

The checks, and the error each one throws:

ErrorWhen
CAF_TYPE_MISMATCHThe CAF is for another document type than the document (details: type, cafType).
CAF_ISSUER_MISMATCHThe CAF issuer RUT is not the document issuer RUT.
FOLIO_OUT_OF_RANGEThe folio is outside the CAF range.
CAF_EXPIREDThe CAF has expired or expires within a day (isCafExpired), unless checkExpiry is false.

CAF_INVALID comes from parseCaf, not from stampDocument.

TypeScript
import { isDteError, stampDocument } from 'dte-sdk'
 
try {
  stampDocument(document, caf)
} catch (error) {
  if (isDteError(error) && error.code === 'FOLIO_OUT_OF_RANGE') {
    error.details // { folio: 101, from: 1, to: 100, type: 33 }
    // ask for another CAF
  }
}

The TED#

The Ted has the fields of the stamp, the CAF block, the stamp time and the signature:

FieldSII tagMeaning
issuerRutREIssuer RUT.
documentTypeTDDocument type.
folioFFolio.
issueDateFEIssue date.
receiverRutRRReceiver RUT (66666666-6 on a receipt without a receiver).
receiverNameRSRReceiver name, folded to ASCII, 40 characters at most.
totalAmountMNTThe document total (totalAmount).
firstItemNameIT1Name of the first item, folded to ASCII, 40 characters at most.
cafCAFThe exact <CAF>…</CAF> block.
stampedAtTSTEDStamp time in Chile, YYYY-MM-DDTHH:MM:SS.
signatureFRMTSHA1withRSA signature of <DD>, in base64.
xmlTEDThe full <TED version="1.0">…</TED> XML.

The two text fields are folded to plain ASCII with tedText before they are cut to 40 characters: accents are removed (á becomes a, ñ becomes n), ligatures and special letters are expanded (ß becomes ss) and anything still outside ASCII is dropped. The reason is that the SII's PDF417 reader loses the bytes at or above 128 and verifies the signature over what it read, so one accent inside the stamp ends in "TED - Firma invalida". Folding comes first and the cut second, because folding can make the text longer.

TypeScript
import { tedText } from 'dte-sdk'
 
tedText('Cajón de peras') // 'Cajon de peras'
tedText('x'.repeat(39) + 'ß') // 'x'.repeat(39) + 's' (ß folds to 'ss', then the cut to 40)
tedText('x'.repeat(50)) // 'x'.repeat(40)

A receipt without a receiver stamps the final consumer: ted.receiverRut is 66666666-6 and ted.receiverName is Consumidor Final.

What is signed#

The signature covers the exact <DD>…</DD> string: RE, TD, F, FE, RR, RSR, MNT, IT1, the CAF block and TSTED, in that order, with RSR and IT1 escaped as XML text. The bytes are the ISO-8859-1 bytes of that string, signed with RSA and SHA-1 (SHA1withRSA, PKCS#1 v1.5). That is the same string the PDF417 barcode of the printed document carries. The signature is deterministic: the same document, CAF and TSTED give the same signature.

Low-level functions#

You rarely need these, but they are exported:

FunctionWhat it does
buildDdString(fields)The exact <DD>…</DD> string, from the fields of a Ted without signature and xml. A full Ted works too.
buildTedXml(dd, signature)Wraps a DD and its base64 signature in <TED version="1.0">…<FRMT algoritmo="SHA1withRSA">…</FRMT></TED>.
tedText(text)Folds text to ASCII and cuts it to 40 characters, as RSR and IT1 need.
signWithCaf(caf, data)SHA1withRSA signature (base64) of data with the CAF private key, over its ISO-8859-1 bytes. Throws ENCODING_UNSUPPORTED_CHAR if data has a character above U+00FF.
verifyWithCaf(caf, data, signature)true when signature (base64) matches data under the CAF public key (RSAPK). Never throws: malformed input is false.

signWithCaf needs only caf.privateKeyPem and verifyWithCaf only caf.publicKey, so you can verify with a CAF that has no secret in it.

Verify a stamp#

verifyTed(tedXml, caf) returns true only when the <DD> bytes in the TED match its FRMT signature under the CAF public key. It is false for a tampered DD, a tampered signature, a TED without DD or FRMT, or a TED stamped with another CAF:

TypeScript
verifyTed(stamped.ted.xml, caf) // true
verifyTed(stamped.ted.xml.replace('<MNT>119000</MNT>', '<MNT>1</MNT>'), caf) // false

It does not throw and it does not check that the amounts in the TED match the document around it. Use it on your own documents (signed.ted.xml) or on received ones, with the CAF that matches. For a received document, verifyReceivedDocuments does more: it takes the CAF from the stamp itself, compares the DD with the header and checks the SII's signature over that CAF (see Verify received documents).

Next steps#

  • Sign: load the certificate and sign the stamped document.
  • Documents: issueDocument runs create, stamp and sign in one call.
  • Portal: request a CAF and keep the folio registry.
  • Security: where to keep the CAF and the certificate.