Saltar al contenido
dte-sdk

Inicio rápido

Esta guía emite una factura electrónica tipo 33 de punta a punta: carga el certificado y el CAF, emite el documento, lo envuelve en un sobre firmado, lo sube al ambiente de certificación del SII y lee el estado. Después muestra la variante para boletas (tipo 39) y el mismo flujo paso a paso.

Esta guía corre sobre la 0.8.0 de dte-sdk desde npm (npm install dte-sdk, revisa Instalación). Todavía no se recomienda para producción, y hasta la 1.0.0 la API puede cambiar en cualquier versión. De los servicios web del SII, solo la semilla y el token por SOAP y el registro de aceptación o reclamo se han probado contra el SII real, y solo en producción; los envíos y las consultas de estado todavía no se ejecutan en ninguno de los dos ambientes (se sigue en el issue 8), así que considera los pasos de envío y estado como no probados contra el servicio real. Revisa el Estado del proyecto.

Necesitas un CAF de tipo 33 y un certificado digital. ¿No conoces esos términos? Lee primero Conceptos básicos. ¿Aún no lo instalas? Revisa Instalación.

Emite una factura#

1

Carga el certificado

loadCertificateFromPkcs12() recibe los bytes de un archivo .pfx o .p12 (sirven los cifrados antiguos y los modernos) y su clave. Si en cambio tienes una llave y un certificado en PEM, usa loadCertificateFromPem(). La clave sale de tu entorno, nunca de tu código.

TypeScript
import { readFileSync } from 'node:fs'
import { loadCertificateFromPkcs12, loadCertificateFromPem } from 'dte-sdk'
 
const certificate = loadCertificateFromPkcs12(readFileSync('firma.pfx'), process.env.PFX_PASSWORD!)
 
// O desde PEM (una llave PKCS#1 o PKCS#8 sin cifrar):
// const certificate = loadCertificateFromPem({
//   privateKeyPem: readFileSync('firma.key.pem', 'utf8'),
//   certificatePem: readFileSync('firma.cert.pem', 'utf8'),
// })
 
certificate.rut // el RUT del titular, leído del certificado (o null)
certificate.notAfter // cuándo vence

Ambos cargadores comprueban que la llave corresponda al certificado y que este no haya vencido. Lanzan CERT_PASSWORD_WRONG, CERT_INVALID o CERT_EXPIRED; pasa { allowExpired: true } como último argumento para cargar igual un certificado vencido (en pruebas).

2

Lee el CAF

parseCaf() lee el XML que descargaste del SII y valida su estructura, tipo, rango y llave. El CAF contiene una llave privada: mantenlo fuera del control de versiones y de los logs.

TypeScript
import { parseCaf } from 'dte-sdk'
 
const caf = parseCaf(readFileSync('caf-33.xml', 'utf8'))
 
caf.documentType // 33
caf.range // { from: 1, to: 100 }
caf.authorizationDate // '2026-10-31'

Tú eliges el folio, dentro del rango del CAF. El SDK no recuerda qué folios ya usaste; la entrada dte-sdk/portal trae un registro local de folios para eso (Folios y CAF).

3

Emite el documento

issueDocument() ejecuta todo el flujo: valida la entrada y calcula los totales (createDocument), timbra el documento con el CAF (stampDocument) y lo firma con tu certificado (signDocument).

TypeScript
import { issueDocument } from 'dte-sdk'
 
const issuer = {
  rut: '76543210-3',
  businessName: 'Comercial Andina SpA',
  businessActivity: 'Software',
  address: 'Av. Los Aromos 1540',
  commune: 'Santiago',
  activityCode: 620200,
}
 
const invoice = issueDocument({
  document: {
    type: 33,
    folio: 1,
    issuer,
    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,
})

Sin issueDate, el documento toma la fecha de hoy en Chile. Se comprueba el vencimiento del CAF (pasa checkCafExpiry: false para omitirlo), y un folio fuera del rango, un CAF de otro tipo o de otro emisor y cualquier campo inválido lanzan un DteError antes de firmar nada.

4

Revisa los totales y el XML

TypeScript
invoice.header.totals
// { netAmount: 100000, vatRate: 19, vatAmount: 19000, totalAmount: 119000 }
 
invoice.ted.xml // el timbre electrónico <TED>
invoice.xml // el <DTE> firmado, sin prólogo XML

Los montos son pesos enteros, redondeados línea por línea, como los revisa el SII. El resultado de issueDocument() es un SignedDocument: el propio documento más ted, xml, signedAt y signature.

5

Crea y firma el sobre

El SII recibe los documentos dentro de un sobre. createEnvelope() agrupa documentos firmados bajo una carátula (Carátula) y signEnvelope() firma el SetDTE con el certificado de la persona que envía.

TypeScript
import { createEnvelope, signEnvelope, encodeLatin1 } from 'dte-sdk'
 
const envelope = signEnvelope(
  createEnvelope({
    kind: 'dte',
    documents: [invoice],
    cover: {
      issuerRut: '76543210-3',
      senderRut: certificate.rut!,
      resolutionDate: '2024-01-15',
      resolutionNumber: 123,
    },
  }),
  certificate,
)
 
envelope.xml // <?xml version="1.0" encoding="ISO-8859-1"?> <EnvioDTE> …
encodeLatin1(envelope.xml) // los bytes, si quieres guardar el archivo

Los campos de la carátula son issuerRut, senderRut (el titular del certificado que firma), resolutionDate (AAAA-MM-DD o DD-MM-AAAA), resolutionNumber (0 en certificación) y, opcionalmente, receiverRut (por defecto el SII), signedAt y setId (ambos se generan si se omiten). kind: 'dte' produce un EnvioDTE. Un sobre sin documentos lanza ENVELOPE_EMPTY y uno que mezcla boletas con otros documentos lanza ENVELOPE_KIND_MISMATCH.

6

Crea el cliente del SII

createSiiClient() devuelve datos planos: cada operación es una función libre que recibe el cliente como primer argumento. Los tokens se obtienen y se renuevan solos.

TypeScript
import { createSiiClient } from 'dte-sdk'
 
const sii = createSiiClient({
  environment: 'certification',
  certificate,
  companyRut: '76543210-3',
})

environment es 'certification' (el ambiente de pruebas del SII, maullin) o 'production' (palena). El certificado firma la semilla que entrega el SII y su titular es el remitente por defecto. El cliente también recibe transport (fetch por defecto), retry, timeouts, logger, capture y archive, entre otros: revisa Servicios web del SII.

7

Sube el sobre

TypeScript
import { uploadEnvelope } from 'dte-sdk'
 
const upload = await uploadEnvelope(sii, { envelope })
 
upload.trackId // el identificador de seguimiento del SII para este envío (solo dígitos)
upload.receivedAt // cuándo lo recibió el SII, hora de Chile
upload.duplicate // true si el SII ya tenía este archivo y devolvió el track id original

El XML sube como bytes ISO-8859-1, así que debe abrir con una declaración XML que nombre ISO-8859-1; los sobres que arma este SDK lo hacen. Un rechazo lanza SII_UPLOAD_REJECTED, con el STATUS del SII como número y sus líneas de error en details.

Un envío que el SII ya respondió nunca se vuelve a enviar, sea cual sea la respuesta, porque una segunda copia es justamente el duplicado que el SII rechaza. El cliente reintenta solo mientras no ha llegado ninguna respuesta (una falla de conexión, un timeout antes de los encabezados de respuesta).

8

Espera el estado

El SII valida el sobre de forma asíncrona. waitForUploadStatus() consulta cada intervalMs (5000 por defecto) hasta que el estado deja de ser pending, o se rinde tras timeoutMs (600000 por defecto).

TypeScript
import { waitForUploadStatus } from 'dte-sdk'
 
const status = await waitForUploadStatus(sii, { trackId: upload.trackId, channel: upload.channel })
 
status.status // 'accepted' | 'acceptedWithObjections' | 'rejected' | …
status.message // la glosa del SII, en español
status.code // el código propio del SII, por ejemplo 'EPR'
status.counts // { reported, accepted, rejected, objections } cuando el SII los entrega, si no null

status.status es la clase del código del SII: accepted (por ejemplo EPR, "Envío procesado"), acceptedWithObjections (RPR), rejected (por ejemplo RCH o RSC) o unknown para un código que el SDK no conoce. status.raw conserva la respuesta del SII tal como llegó. Si el SII no se pronuncia a tiempo, la espera lanza WAIT_TIMEOUT con el último código que vio; puedes volver a consultar más tarde con el mismo track id.

queryUploadStatus() consulta una sola vez, sin esperar. Pasa channel: upload.channel para consultar el estado por el canal por el que subió el sobre (soap en un EnvioDTE).

Emite una boleta#

Las boletas (tipo 39) no necesitan receptor y usan precios con IVA incluido. Sin receiver, el documento va al consumidor final (66666666-6, "Consumidor Final"). Con pricesIncludeVat: true cada precio unitario es el bruto, y el neto y el IVA se derivan del total. Los totales de una boleta no llevan vatRate.

TypeScript
const caf39 = parseCaf(readFileSync('caf-39.xml', 'utf8'))
 
const receipt = issueDocument({
  document: {
    type: 39,
    folio: 10,
    issuer,
    items: [{ name: 'Café', unitPrice: 1500 }],
    pricesIncludeVat: true,
  },
  caf: caf39,
  certificate,
})
 
receipt.header.totals // { netAmount: 1261, vatAmount: 239, totalAmount: 1500 }

Las boletas viajan en otro sobre: kind: 'receipt' produce un EnvioBOLETA, y solo acepta boletas (tipos 39 y 41). Su receptor es siempre el SII, pases lo que pases.

TypeScript
const receiptEnvelope = signEnvelope(
  createEnvelope({
    kind: 'receipt',
    documents: [receipt],
    cover: {
      issuerRut: '76543210-3',
      senderRut: certificate.rut!,
      resolutionDate: '2024-01-15',
      resolutionNumber: 123,
    },
  }),
  certificate,
)
 
const receiptUpload = await uploadEnvelope(sii, { envelope: receiptEnvelope })
const receiptStatus = await waitForUploadStatus(sii, {
  trackId: receiptUpload.trackId,
  channel: receiptUpload.channel,
})

El canal depende del ambiente:

SobreCertificaciónProducción
EnvioDTE (kind: 'dte')DTEUpload (SOAP / CGI)DTEUpload (SOAP / CGI)
EnvioBOLETA (kind: 'receipt')DTEUpload (SOAP / CGI)API REST de boletas

La opción receiptsChannel del cliente fija el canal de las boletas ('rest' en producción, 'soap' en certificación), y el campo channel de uploadEnvelope() lo reemplaza en una llamada. Un EnvioDTE siempre va por soap: pedir rest lanza INVALID_OPTION { field: 'channel' }. Un envío REST acepta como máximo 500 boletas; más que eso se rechaza antes de enviar nada. El estado de un sobre de boletas hay que consultarlo por el mismo canal por el que subió, por eso los ejemplos pasan channel: upload.channel.

El mismo flujo, paso a paso#

issueDocument() es un atajo de tres llamadas. Úsalas por separado cuando necesites mirar, guardar o probar lo que hay entre ellas.

TypeScript
import {
  createDocument,
  toSiiDocument,
  stampDocument,
  signDocument,
  verifyTed,
  verifyDocumentSignature,
} from 'dte-sdk'
 
const doc = createDocument(input) // valida, sanea, numera líneas, calcula totales
toSiiDocument(doc) // los mismos datos con las etiquetas oficiales: Encabezado, Detalle, MntTotal, RznSoc, NmbItem…
const stamped = stampDocument(doc, caf) // agrega el TED, firmado con la llave del CAF
const signed = signDocument(stamped, certificate) // XMLDSig envuelta → signed.xml
 
verifyTed(signed.ted.xml, caf) // true
verifyDocumentSignature(signed.xml) // { referenceId, digestValid: true, signatureValid: true, valid: true }

input es el mismo objeto que pasas como document a issueDocument(). createDocument() devuelve un Document con header (tipo, folio, fecha de emisión, emisor, receptor y totales), items y los bloques opcionales. toSiiDocument() es un mapeo puro a la estructura oficial del SII; fromSiiDocument() hace el camino inverso, para documentos recibidos. stampDocument() comprueba el tipo, el emisor, el rango de folios y el vencimiento del CAF antes de firmar. verifyTed() verifica un TED contra la llave pública del CAF y verifyDocumentSignature() verifica la firma del <DTE>, propio o recibido. verifyEnvelopeSignature() hace lo mismo con un sobre.

Toda función que depende de la hora actual acepta { now: Date } (createDocument(input, { now }), issueDocument({ …, now }), createEnvelope({ …, now })), de modo que las pruebas pueden ser deterministas.

Errores#

Todo lo que lanza el SDK es un DteError con un code estable y sin datos personales en su mensaje. El valor problemático vive en details.

TypeScript
import { isDteError } from 'dte-sdk'
 
try {
  issueDocument({ document, caf, certificate })
} catch (error) {
  if (isDteError(error)) {
    error.code // por ejemplo 'FOLIO_OUT_OF_RANGE', 'CAF_EXPIRED', 'FIELD_TOO_LONG'
    error.details // { folio, from, to, type } en FOLIO_OUT_OF_RANGE
  }
  throw error
}

Errores lista los códigos.

Siguientes pasos#