Saltar al contenido
dte-sdk

Registro de compras y ventas

El RCV (Registro de Compras y Ventas) es el registro propio del SII de lo que compraste y vendiste. dte-sdk/portal lo lee en la misma sesión con certificado que el resto del portal, cambia cómo se clasifica una compra y descarga el XML de tus documentos emitidos o recibidos desde el respaldo MIPYME. También trae, como datos, lo que el registro hace con cada uno de sus códigos de documento. Esta página cubre qué lee cada función, a qué servicio del SII llama y las trampas: un mes rechazado que se lee como vacío, escrituras que cambian el registro real y un defecto conocido en el respaldo.

Todo lo que ves aquí está probado contra respuestas sintéticas del SII, con una exigencia de 100 % de cobertura en el CI. Contra el SII real solo se han respondido lecturas: en producción, el 9 y el 10 de noviembre de 2026, fetchRegisterSummary, listPeriodDocuments, fetchPurchaseDocument y listPurchaseTypes respondieron con las filas de una empresa con una factura recibida, y antes, solicitudes de solo lectura para una empresa sin documentos. Las descargas del respaldo MIPYME y las escrituras aún no se han visto. Los nombres de los campos de las filas vienen del propio código cliente del SII, y hasta ahora solo se han enfrentado a esa factura. Revisa el Estado.

La 0.8.0 de dte-sdk está en npm, la versión más reciente. Todavía no se recomienda para producción: la mayoría de las áreas no se ha probado contra el SII real, y hasta la 1.0.0 la API puede cambiar en cualquier versión. Revisa Estado.

Uso básico#

Primero necesitas un cliente del portal: revisa Portal del SII y folios.

TypeScript
import {
  changePurchaseType,
  downloadMipymeBackup,
  fetchPurchaseDocument,
  fetchRegisterSummary,
  listPeriodDocuments,
  listPurchaseTypes,
  moveVatToCommonUse,
} from 'dte-sdk/portal'
 
// Todos los documentos de octubre de 2026 que recibió la empresa, una fila cada uno
const { summary, documents } = await listPeriodDocuments(portal, { period: '2026-10', direction: 'received' })
 
// Los totales oficiales del libro de compras, por tipo de documento
const { rows, header } = await fetchRegisterSummary(portal, { period: '2026-10', operation: 'purchase' })
 
// Cómo se clasifica una compra, y cambiarlo
const types = await listPurchaseTypes(portal)
const key = { type: 33, folio: 1234, period: '2026-10', counterpartyRut: '12345678-5' }
const purchase = await fetchPurchaseDocument(portal, key) // null si el SII no tiene datos para esa clave
await changePurchaseType(portal, { ...key, purchaseType: '4' }) // un id de listPurchaseTypes
 
// El XML completo de los documentos, en tramos de a lo más 20
await downloadMipymeBackup(portal, {
  from: '2026-10-01',
  to: '2026-10-31',
  direction: 'received',
  onChunk: async ({ from, to, xml }) => saveBackup(`${from}_${to}.xml`, xml),
})

saveBackup es tuyo. El respaldo contiene tus documentos reales: guárdalos con el cuidado que das a cualquier registro tributario.

¿Qué necesito?#

El registro tiene tres servicios detrás, más el respaldo. Cada función lee uno de ellos:

NecesitoFunciónServicio del SII
Cada documento de un período (folio, fecha, montos), sin boletas y sin XMLlistPeriodDocumentsconsemitidosinternetui
Los totales oficiales por tipo de documento y estado contable, con las boletas agregadasfetchRegisterSummaryconsdcvinternetui
Cómo se clasifica una compra, los tipos de compra, y cambiarlosfetchPurchaseDocument, listPurchaseTypes y las escriturascomplementoscvui
El XML completo de un documentodownloadMipymeBackupRespaldo MIPYME

El host del registro depende del ambiente del cliente (www4c.sii.cl para certificación, www4.sii.cl para producción). El respaldo no: revisa El respaldo MIPYME.

Todas estas funciones reciben el mismo último argumento opcional, overrides, que el resto del portal ({ signal, timeoutMs, retry }), y el companyRut opcional que reemplaza al del cliente.

Leer el registro#

Documentos de un período#

TypeScript
const { summary, documents } = await listPeriodDocuments(portal, {
  period: '2026-10', // AAAA-MM
  direction: 'issued', // el valor por defecto; o 'received'
})
 
summary[0].type // 33
summary[0].documentCount
documents[0].folio
documents[0].totalAmount

Pide el resumen por tipo de documento y luego hace una llamada de detalle por fila, al mismo tiempo. Devuelve:

  • summary: un PeriodSummaryRow por tipo de documento y sección: type, typeDescription, section, documentCount, exemptAmount, netAmount, vatAmount, totalAmount. La sección es S1 ventas, S2 facturas de compra, S3 liquidaciones, S4 exportaciones o S5 guías de despacho.
  • documents: todos los documentos de los tipos del resumen, como PeriodDocument: type, typeDescription, folio, issueDate, receivedDate, counterpartyRut, counterpartyName, los cuatro montos, eventOrder (el último evento: 2 acuse, 3 aceptado, 4 reclamado) y eventDescription.

Este no es el registro oficial. Lista lo que el SII tiene por documento, sin hora ni XML, y deja fuera lo que el SII agrega (las boletas, por ejemplo). counterpartyRut es la otra parte, sin validar: en los documentos recibidos trae al emisor.

Los totales oficiales#

TypeScript
const { rows, header } = await fetchRegisterSummary(portal, {
  period: '2026-10',
  operation: 'purchase', // o 'sale'
  accountingStatus: 'registered', // el valor por defecto; 'pending', 'notIncluded' o 'claimed'
})
 
rows[0].type
rows[0].documentCount
rows[0].commonUseVat
rows[0].nonRecoverableVat
header?.updatedAt
header?.proportionalityFactor

Son los totales oficiales del período por tipo de documento y estado contable, incluido lo que el SII agrega, como las boletas. Las rows son RegisterSummaryRow: type, typeName, documentCount, exemptAmount, netAmount, vatAmount, commonUseVat, nonRecoverableVat, totalAmount. header es null o trae updatedAt (cuándo cambió el registro por última vez), proportionalityFactor (el factor de proporcionalidad) y raw (la cabecera tal como la envió el SII). El factor es un número de 0 a 1. Según se ha visto, el SII lo envía como texto con espacios y coma decimal (" 1,0000", "0,8532"), y esa forma también se lee. Un valor fuera de 0 a 1 no es un factor: proportionalityFactor es null y el valor queda en raw.

Códigos de documento y estados contables#

Cada fila de los totales oficiales trae un type de documento. Lo que el registro hace con cada uno son datos del paquete, desde la 0.2.0: registerDocumentCode(code) entrega uno de los 49 códigos de documento del registro, o undefined, y REGISTER_DOCUMENT_CODES los lista todos. Nada de esto llama al SII.

TypeScript
import { REGISTER_ACCOUNTING_STATUSES, fetchRegisterSummary, registerDocumentCode } from 'dte-sdk/portal'
 
const creditNote = registerDocumentCode(61)
creditNote?.name // 'Nota de Crédito Electrónica'
creditNote?.operations // ['purchase', 'sale']
creditNote?.sign // -1: resta de los totales
registerDocumentCode(71) // undefined: no es un código del registro
 
const { rows } = await fetchRegisterSummary(portal, { period: '2026-10', operation: 'purchase' })
for (const row of rows) {
  const code = row.type === null ? undefined : registerDocumentCode(row.type)
  if (code?.sign === -1) {
    // Una nota de crédito o una disminución: resta de los totales del período
  }
}
 
// Todos los estados contables que puede tener el lado de compras; las ventas solo tienen 'registered'
for (const accountingStatus of REGISTER_ACCOUNTING_STATUSES.purchase) {
  await fetchRegisterSummary(portal, { period: '2026-10', operation: 'purchase', accountingStatus })
}

Cada entrada dice en qué lados del registro aparece el código (operations), si es un DTE (electronic), si llega al registro (inRegister, falso para las guías de despacho 50 y 52), su signo en los totales (sign, -1 para las notas de crédito y las disminuciones), si el registro lo lista documento por documento (perDocument, falso para las boletas y los demás códigos que guarda solo como una línea de resumen mensual) y si el comprador retiene el IVA (buyerWithholdsVat: 29, 45 y 46). REGISTER_ACCOUNTING_STATUSES entrega los cuatro estados para las compras y solo registered para las ventas, porque ninguna fuente del SII describe otros estados de una venta.

Donde el SII declara un valor, el catálogo lo sigue. Los pocos que no declara son inferidos, y el JSDoc de cada uno lo dice: que las guías de despacho quedan fuera del registro, el signo de cada código (el del F29: las notas de crédito restan) y que el 906 es solo resumen. Los montos de los códigos 40, 43 y 103 pueden venir negativos y traen su propio signo. El catálogo todavía no se ha contrastado con un registro real con documentos. La referencia de la API agrupa los 49 códigos según lo que el catálogo dice de ellos.

Compras: tipos y detalle#

TypeScript
const types = await listPurchaseTypes(portal)
// [{ id, description, interchangeable }, ...]: los tipos de compra que el SII permite darle a un documento
 
const purchase = await fetchPurchaseDocument(portal, {
  type: 33,
  folio: 1234,
  period: '2026-10',
  counterpartyRut: '12345678-5', // el RUT del emisor
})
purchase?.purchaseType // el tipo de compra vigente

listPurchaseTypes está medido contra el SII real. Su indicador interchangeable dice que el cliente del propio SII ofrece mover el IVA entre recuperable y uso común para ese tipo. No significa que el tipo se pueda cambiar.

fetchPurchaseDocument devuelve el documento tal como lo tiene el registro, con su purchaseType vigente, commonUseVat y nonRecoverableVat, o null si el SII no tiene datos para esa clave. La clave es type, folio, period (AAAA-MM) y counterpartyRut, el emisor de la compra.

Trampas al leer#

  • Un mes rechazado se lee como vacío. El SDK no mira los códigos de respuesta de estos servicios. Por eso listPeriodDocuments y fetchRegisterSummary no pueden distinguir un mes que el SII se niega a mostrar de un mes sin movimiento: ambos vuelven sin filas. No tomes una respuesta vacía como prueba de que no pasó nada.
  • raw contiene cada fila exactamente como la envió el SII. Los nombres de los campos tipados vienen del propio código cliente del SII y no se han visto con documentos reales. Si uno falta o no se puede leer, el campo tipado es null, y lo que envió el SII sigue en raw. Lee raw cuando un null te sorprenda.
  • Los números pueden ser null. Todo monto puede ser null. No hagas aritmética con un monto sin revisarlo.
  • Sin reintentos por defecto. Una lectura hace un intento por llamada. Pasa { retry: { ... } } como overrides para repetir cada llamada. Una lectura que encuentra una sesión vencida o una página que no puede leer parte de nuevo una vez con una sesión nueva.

listPeriodDocuments y fetchRegisterSummary lanzan INVALID_DATE si el period no es AAAA-MM, INVALID_OPTION ante un direction, operation o accountingStatus desconocido, MISSING_FIELD sin empresa, INVALID_RUT y SII_PORTAL_UNEXPECTED_PAGE ante una respuesta que no es JSON.

Cambiar el registro#

Tres funciones cambian cómo se registra una compra.

TypeScript
import { changePurchaseType, fetchPurchaseDocument, moveVatToCommonUse, moveVatToRecoverable } from 'dte-sdk/portal'
 
const key = { type: 33, folio: 1234, period: '2026-10', counterpartyRut: '12345678-5' }
 
// Cambiar el tipo de compra: un id de listPurchaseTypes
await changePurchaseType(portal, { ...key, purchaseType: '4' })
 
// Mover IVA de recuperable a uso común: pasa el monto FINAL de cada casilla
await moveVatToCommonUse(portal, { ...key, commonUseVat: 19000 })
 
// Lo inverso
await moveVatToRecoverable(portal, { ...key, commonUseVat: 0, recoverableVat: 19000 })

En un cliente de producción, estas funciones cambian el registro real. El SDK no ofrece vista previa ni deshacer.

  • Un solo intento, y solo el OK del SII confirma. Cada escritura es un único intento: el SDK no puede saber si una respuesta perdida significa que el cambio se aplicó, así que nunca lo repite, y overrides.retry no alcanza a una escritura. Cualquier cosa distinta del OK del SII lanza SII_PORTAL_REJECTED con details.rejection igual a purchaseChangeNotConfirmed, y details.portalMessage cuando el SII dijo algo.
  • Tras purchaseChangeNotConfirmed, lee antes de reintentar. El cambio pudo aplicarse. Llama a fetchPurchaseDocument y compara purchaseType o los montos de IVA con lo que querías. Reintenta solo si no cambió.
  • Los montos de IVA son montos finales, no diferencias. commonUseVat y recoverableVat son el valor de cada casilla después del cambio. Para mover todo el IVA de un documento a uso común, pasa el total como commonUseVat y omite recoverableVat. Para devolverlo todo, pasa commonUseVat: 0 y el total como recoverableVat. Un monto que omites va vacío.
  • Una escritura lanza INVALID_DATE, MISSING_FIELD o INVALID_RUT ante una clave mala, y SII_PORTAL_UNEXPECTED_PAGE ante una respuesta que no puede leer.

El respaldo MIPYME#

El SII guarda en su servicio MIPYME un respaldo del XML de los documentos que una empresa emitió y recibió. downloadMipymeBackup lista un rango de fechas, lo divide, descarga cada tramo y te entrega el XML. Úsalo cuando necesites el documento completo, que el registro no tiene.

TypeScript
import { downloadMipymeBackup } from 'dte-sdk/portal'
 
const backup = await downloadMipymeBackup(portal, {
  from: '2026-10-01', // AAAA-MM-DD
  to: '2026-10-31',
  direction: 'issued', // el valor por defecto; o 'received'
  type: 33, // opcional: solo este tipo de documento
  attempts: 3, // intentos por listado y por descarga; 3 por defecto
  onChunk: async ({ from, to, total, xml }) => {
    await saveBackup(`${from}_${to}.xml`, xml)
  },
})
 
backup.total // documentos contados
backup.chunks // [{ from, to, total, type? }], del más nuevo al más viejo
OpciónPor defectoQué hace
from, toobligatoriasPrimer y último día, AAAA-MM-DD. Si no, INVALID_DATE.
direction'issued'Documentos emitidos o recibidos.
typetodosSolo este tipo de documento.
attempts3Intentos por listado y por descarga, con esperas de 1 s que se duplican entre ellos. El respaldo tiene su propio ciclo de reintentos: overrides.retry solo alcanza al inicio de sesión.
onChunkningunaRecibe cada tramo (from, to, total, xml) a medida que llega. Entonces el resultado no trae xml.
companyRutel del clienteLa empresa, si no es la del cliente.

El resultado es un MipymeBackup: total es la suma de los totales de los tramos (los documentos que contaron los listados), y chunks son los tramos, del más nuevo al más viejo. Cada MipymeBackupChunk trae from, to, total, type (solo cuando hubo que separar un día por tipo de documento) y xml. Con onChunk, el xml no aparece en chunks, así un histórico largo no queda en memoria. Usa onChunk para históricos largos.

Cómo se divide el rango#

El SII permite a lo más 20 documentos por descarga: 20 responden XML, 21 responden una página de error. Por eso el SDK planifica primero y descarga después.

  1. Pregunta al SII cuántos documentos tiene todo el rango. Hasta 20, es un solo tramo.
  2. Sobre 20, divide el rango en mitades, por día, y planifica cada mitad de la misma forma.
  3. Cuando un tramo ya no se puede dividir por día y sigue teniendo más de 20, divide ese día por tipo de documento, consultando la cantidad de cada tipo de una lista fija de tipos.

El plan completo va primero. Si un día no se puede descargar, la llamada lanza antes de cualquier descarga, así que nunca recibes medio respaldo sin saberlo:

  • backupDayOverLimit: un día y un tipo de documento superan los 20 documentos. No se puede dividir más. details trae el day, el type y la cantidad listed.
  • backupDayUnknownTypes: un día supera los 20 documentos y tiene documentos de tipos que el SDK no sabe separar, así que el día no se puede cubrir. details trae el day, la cantidad listed y la cantidad covered.

Ambos son SII_PORTAL_REJECTED, con la clave en details.rejection.

Defecto conocido. Un rango de dos días con más de 20 documentos descarga solo su primer día, separado por tipo, mientras total sigue contando ambos días. Lo mismo puede ocurrirle a un tramo de dos días dentro de un rango más largo, porque el plan divide los rangos por la mitad hasta llegar a dos días. Mientras no se corrija, pide un día a la vez cuando un tramo pueda tener más de 20 documentos, y compara lo que descargaste con total.

Qué lee el respaldo#

  • Lee producción desde cualquier cliente. El servicio existe solo en producción, así que downloadMipymeBackup lee documentos reales incluso desde un cliente creado con environment: 'certification'. Si un entorno de desarrollo o de pruebas guarda lo que descarga, ese entorno necesita su propia puerta: el SDK no lo va a detener.
  • Solo necesita el certificado. No exige que la empresa esté inscrita como emisor.

Rechazos#

El respaldo responde con páginas, y el SDK las lee como rechazos SII_PORTAL_REJECTED:

details.rejectionSignificadoQué hacer
backupNoDataEl SII dice que no tiene información en MIPYME para este RUT.Es definitivo: no hay nada que descargar.
backupCaptchaEl SII encendió el captcha.No se puede automatizar mientras esté encendido. Intenta más tarde, nunca en un ciclo.
backupIndeterminateEl portal devolvió varios mensajes de su plantilla, sin un veredicto único.No se puede concluir nada, tampoco que no haya datos. Intenta más tarde.
backupRefusedEl portal rechazó.El motivo es el mensaje del SII, en details.portalMessage.
backupNoCompanyEl portal devolvió su página "Seleccionar empresa" sin ningún mensaje.No se pudo determinar la causa.
backupDayOverLimit, backupDayUnknownTypesUn día no se puede descargar dentro del límite de 20 documentos.Revisa arriba. No se descargó nada.

backupNoData es definitivo. backupCaptcha y backupIndeterminate significan "intenta más tarde": no los reintentes en un ciclo, porque eso empeora un captcha. Los errores de sesión del portal (SII_PORTAL_SESSION_LIMIT, SII_PORTAL_BLOCKED y el resto) están descritos en Portal del SII y folios, y portalRejectionMessage(rejection) entrega la explicación en español de cualquiera de estas claves.

Próximos pasos#