Saltar al contenido
dte-sdk

Emitir una nota de crédito

Una nota de crédito (tipo 61) es la forma de deshacer o corregir una factura que ya está en el SII: no se edita la factura, se emite un documento nuevo que apunta a ella. Este ejemplo emite los tres tipos de nota de crédito contra la factura de Emitir una factura, envía la nota y le pregunta al SII qué dice ahora de la factura. Al final hay una variante con nota de débito (tipo 56).

Esta página corre sobre la 0.8.0 de dte-sdk desde npm (ver Instalación). Los documentos y los sobres están probados contra respuestas sintéticas del SII y todavía no se han enviado al SII real. De los servicios web del SII, solo la semilla y el token por SOAP y el registro de aceptación o reclamo se han enfrentado a él, y solo en producción; los envíos y las consultas de estado todavía no se ejecutan en ninguno de los dos ambientes (issue 8). Ejecútalo primero contra el ambiente de certificación.

Qué vas a construir#

Un programa, credit-note.ts, que emite una nota por corrida. Eliges el caso en la línea de comandos (cancel, correct-text o correct-amounts), y el programa reserva un folio, emite y firma la nota, la envuelve en un EnvioDTE, la envía, espera al SII y luego consulta el estado de la factura a la que la nota se refiere.

La nota dice qué le hace a la factura en su referencia (Referencia), con un código. Hay tres:

CasoCódigoLíneas de la notaTotales
Anular la factura1, REFERENCE_CODES.cancelsLas mismas líneas de la facturaLos mismos de la factura: neto 165000, IVA 31350, exento 90000, total 286350
Corregir un texto de la factura2, REFERENCE_CODES.correctsTextUna línea que nombra la corrección, sin precioTotal 0
Corregir montos, o una devolución parcial3, REFERENCE_CODES.correctsAmountsSolo lo que se acredita: aquí 1 hora de soporteNeto 25000, IVA 4750, total 29750

Los tres son alternativas para la misma factura: una factura real se anula o se corrige, no ambas.

Requisitos previos#

  • Todo lo de Emitir una factura: Node 22.12 o superior, firma.pfx con PFX_PASSWORD, y la resolución en SII_RESOLUTION_DATE y SII_RESOLUTION_NUMBER.
  • Un CAF del tipo 61 como caf-61.xml, para el mismo RUT que el emisor. Cada tipo de documento tiene su propio CAF: el CAF del tipo 33 no sirve para una nota. Las notas de crédito, igual que las facturas, tienen un CAF que vence seis meses después de su fecha de autorización.
  • Una factura ya emitida. El programa usa la del ejemplo de la factura: tipo 33, folio 1, emitida el 2026-11-05, total 286350. Cambia la constante INVOICE para que coincida con la tuya: la fecha y el total son lo que el SII usa para encontrarla.
  • El mismo receptor de la factura, con los mismos datos.

El programa completo#

TypeScript
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import {
  REFERENCE_CODES,
  createEnvelope,
  createSiiClient,
  encodeLatin1,
  isDteError,
  issueDocument,
  loadCertificateFromPkcs12,
  parseCaf,
  queryDocumentStatus,
  signEnvelope,
  uploadEnvelope,
  waitForUploadStatus,
  type DocumentInput,
  type ReferenceCode,
} from 'dte-sdk'
import { createDirectoryCapture, createDirectoryFolioRegistry, createDirectoryUploadArchive } from 'dte-sdk/node'
 
const COMPANY_RUT = '76543210-3'
const ENVIRONMENT = 'certification'
const resolution = {
  date: process.env.SII_RESOLUTION_DATE!, // AAAA-MM-DD
  number: Number(process.env.SII_RESOLUTION_NUMBER ?? 0),
}
 
// La factura a la que se refieren las notas: la del ejemplo de la factura
const INVOICE = { type: 33, folio: 1, date: '2026-11-05', totalAmount: 286350 } as const
 
const issuer = {
  rut: COMPANY_RUT,
  businessName: 'Comercial Andina SpA',
  businessActivity: 'Software',
  activityCode: 620200,
  address: 'Av. Los Aromos 1540',
  commune: 'Santiago',
}
 
// El receptor de la factura, tal como estaba en la factura
const receiver = {
  rut: '12345678-5',
  businessName: 'Comercializadora Rapel Ltda',
  businessActivity: 'Comercio',
  address: 'Pasaje Los Olmos 88',
  commune: 'Providencia',
}
 
const invoiceItems: DocumentInput['items'] = [
  { name: 'Plan anual', quantity: 1, unitPrice: 100000, discountPercent: 10 },
  { name: 'Horas de soporte', quantity: 3, unit: 'HRS', unitPrice: 25000 },
  { name: 'Curso de capacitación', quantity: 2, unitPrice: 45000, exempt: true },
]
 
const referenceToInvoice = (code: ReferenceCode, reason: string) => [
  { documentType: INVOICE.type, folio: INVOICE.folio, date: INVOICE.date, code, reason },
]
 
type NoteCase = Pick<DocumentInput, 'type' | 'items' | 'references'> & Partial<Pick<DocumentInput, 'receiver' | 'totals'>>
 
const CASES: Record<string, NoteCase> = {
  // Código 1: la nota deshace toda la factura, así que repite cada línea
  cancel: {
    type: 61,
    items: invoiceItems,
    references: referenceToInvoice(REFERENCE_CODES.cancels, 'Anula factura 1'),
  },
  // Código 2: la factura tiene un texto equivocado. Una línea que nombra la corrección, sin montos
  'correct-text': {
    type: 61,
    items: [{ name: 'Corrige giro del receptor' }],
    receiver: { ...receiver, businessActivity: 'Comercio al por menor' }, // el texto corregido
    totals: { totalAmount: 0 },
    references: referenceToInvoice(REFERENCE_CODES.correctsText, 'Corrige giro del receptor'),
  },
  // Código 3: cambian los montos. La nota lleva solo lo que se acredita: aquí, una de las tres horas de soporte
  'correct-amounts': {
    type: 61,
    items: [{ name: 'Horas de soporte', quantity: 1, unit: 'HRS', unitPrice: 25000 }],
    references: referenceToInvoice(REFERENCE_CODES.correctsAmounts, 'Devolución parcial: 1 hora de soporte'),
  },
}
 
const chosen = CASES[process.argv[2] ?? 'cancel']
if (chosen === undefined) throw new Error(`Unknown case. Use one of: ${Object.keys(CASES).join(', ')}`)
 
const certificate = loadCertificateFromPkcs12(await readFile('firma.pfx'), process.env.PFX_PASSWORD!)
// Cada tipo de documento tiene su propio CAF: caf-61.xml para notas de crédito, caf-56.xml para notas de débito
const caf = parseCaf(await readFile(`caf-${chosen.type}.xml`, 'latin1'))
 
const folios = createDirectoryFolioRegistry({ dir: './data/folios' })
const scope = { companyRut: COMPANY_RUT, type: chosen.type, range: caf.range, environment: ENVIRONMENT } as const
 
const sii = createSiiClient({
  environment: ENVIRONMENT,
  certificate,
  companyRut: COMPANY_RUT,
  capture: createDirectoryCapture({ dir: './data/capture' }),
  archive: createDirectoryUploadArchive({ dir: './data/uploads' }),
})
 
try {
  const folio = await folios.reserve(scope)
 
  const note = issueDocument({
    document: { folio, issuer, receiver, ...chosen }, // chosen trae el tipo, las líneas y la referencia
    caf,
    certificate,
  })
  console.log(`note ${chosen.type} folio ${folio}`, JSON.stringify(note.header.totals))
  console.log('reference', JSON.stringify(note.references))
 
  const envelope = signEnvelope(
    createEnvelope({
      kind: 'dte',
      documents: [note],
      cover: {
        issuerRut: COMPANY_RUT,
        senderRut: certificate.rut!,
        resolutionDate: resolution.date,
        resolutionNumber: resolution.number,
      },
    }),
    certificate,
  )
  await mkdir('./data/signed', { recursive: true })
  await writeFile(`./data/signed/${chosen.type}-${folio}.xml`, encodeLatin1(envelope.xml))
 
  const upload = await uploadEnvelope(sii, { envelope })
  await folios.markSent(scope, folio, { trackId: upload.trackId })
  const status = await waitForUploadStatus(sii, { trackId: upload.trackId })
  console.log(`envelope ${status.code} ${status.status}: ${status.message}`)
 
  if (status.status === 'accepted' || status.status === 'acceptedWithObjections') {
    // El SII ya conoce la nota: preguntar por la factura a la que se refiere
    const invoice = await queryDocumentStatus(sii, {
      receiverRut: receiver.rut,
      type: INVOICE.type,
      folio: INVOICE.folio,
      issueDate: INVOICE.date,
      totalAmount: INVOICE.totalAmount,
    })
    console.log(`invoice ${INVOICE.folio}: ${invoice.code} ${invoice.status}: ${invoice.message}`)
  } else {
    process.exitCode = 1
  }
} catch (error) {
  if (!isDteError(error)) throw error
  console.error(error.code, error.details)
  process.exitCode = 1
}

Ejecuta un caso a la vez (o el equivalente con tu ejecutor de TypeScript):

Shell
node credit-note.ts cancel
node credit-note.ts correct-text
node credit-note.ts correct-amounts

Paso a paso#

1

Describir la factura que corriges

La nota necesita tres datos de la factura: su tipo, su folio y su fecha de emisión. Van en la referencia como documentType, folio y date (TpoDocRef, FolioRef, FchRef). La fecha es la fecha de emisión de la factura, en AAAA-MM-DD. queryDocumentStatus necesita además el total de la factura, por eso INVOICE lo tiene.

Una referencia (ReferenceInput) tiene un code y un reason (CodRef, RazonRef). La razón es texto libre de hasta 90 caracteres, sanitizado como todos los demás textos del documento. Un documento puede llevar varias referencias: se numeran desde 1 en el orden del arreglo.

2

Caso 1: anular (código 1)

La nota repite cada línea de la factura, así que sus totales son los de la factura. El programa repite las líneas de la factura exactamente (invoiceItems), para que los montos no puedan diferir: el descuento, la línea exenta y el redondeo dan neto 165000, exento 90000, IVA 31350 y total 286350. Cuando el SII procese la nota, el estado de la factura pasa a ANC.

3

Caso 2: corregir un texto (código 2)

No se acredita nada, así que la nota no tiene montos. Tiene una línea que nombra la corrección, sin cantidad ni precio, y un total de 0. El texto corregido va en los datos de la nota: en este ejemplo, el giro del receptor.

Sin totals, una línea sin precio da { vatRate: 19, totalAmount: 0 }. El totals: { totalAmount: 0 } explícito escribe solo MntTotal, que es la forma que usa la corrida de certificación del propio SDK para estas notas. totals es una opción avanzada: tú te haces cargo del cálculo, así que úsala solo para este caso. El estado de la factura pasa a TMC.

4

Caso 3: corregir montos o devolución parcial (código 3)

La nota lleva solo lo que se acredita. Aquí al cliente se le acredita una de las tres horas de soporte: 1 × 25000, neto 25000, IVA 4750 (25000 × 19 / 100) y un total de 29750. Una cantidad devuelta de cualquier otra línea funciona igual: la nota lleva la línea con la cantidad que vuelve. El estado de la factura pasa a MMC.

5

Emitir, enviar y consultar

Todo lo que sigue a la elección del caso es el flujo del ejemplo de la factura: reservar un folio del registro del CAF del tipo 61, issueDocument, createEnvelope y signEnvelope, guardar el XML, uploadEnvelope, markSent, waitForUploadStatus. La única diferencia es la pregunta del final: el programa consulta al SII por la factura, no por la nota. Una nota aceptada cambia lo que el SII responde sobre la factura que referencia, y así se ve el efecto.

El type de la nota sale del caso, y el CAF se lee de caf-<tipo>.xml. Si el archivo es de otro tipo, issueDocument lanza CAF_TYPE_MISMATCH.

Resultado esperado#

Con un registro nuevo y un CAF del tipo 61, la primera nota de cada caso:

Text
note 61 folio 1 {"netAmount":165000,"exemptAmount":90000,"vatRate":19,"vatAmount":31350,"totalAmount":286350}
reference [{"lineNumber":1,"documentType":33,"folio":"1","date":"2026-11-05","code":1,"reason":"Anula factura 1"}]
envelope EPR accepted: Envío procesado
invoice 1: ANC cancelled: Existe nota de crédito que anula el documento

Para correct-text:

Text
note 61 folio 1 {"totalAmount":0}
reference [{"lineNumber":1,"documentType":33,"folio":"1","date":"2026-11-05","code":2,"reason":"Corrige giro del receptor"}]
envelope EPR accepted: Envío procesado
invoice 1: TMC modified: Existe nota de crédito que modifica textos

Para correct-amounts:

Text
note 61 folio 1 {"netAmount":25000,"vatRate":19,"vatAmount":4750,"totalAmount":29750}
reference [{"lineNumber":1,"documentType":33,"folio":"1","date":"2026-11-05","code":3,"reason":"Devolución parcial: 1 hora de soporte"}]
envelope EPR accepted: Envío procesado
invoice 1: MMC modified: Existe nota de crédito que modifica montos

Los totales y la referencia los calcula el SDK. El folio es el siguiente de tu CAF. Las dos últimas líneas vienen del SII, que manda su propio texto junto con el código: lo que importa es el código y su clase (cancelled, modified).

Variante con nota de débito (tipo 56)#

Una nota de débito (tipo 56) es el espejo: agrega a una factura lo que faltó, o deshace una nota de crédito. Se construye igual: una referencia, un código, líneas y el flujo de arriba, con un CAF del tipo 56 (caf-56.xml). El programa ya lee caf-${chosen.type}.xml y mantiene un alcance de registro por tipo, así que una nota de débito son dos entradas más en CASES:

TypeScript
  // una nota de débito (tipo 56) que sube el monto: dos horas de soporte más
  debit: {
    type: 56,
    items: [{ name: 'Horas de soporte adicionales', quantity: 2, unit: 'HRS', unitPrice: 25000 }],
    references: referenceToInvoice(REFERENCE_CODES.correctsAmounts, 'Horas de soporte no facturadas'),
  },
  // una nota de débito que anula una nota de crédito: apunta a la nota (61) y no tiene montos
  'cancel-credit-note': {
    type: 56,
    items: [{ name: 'Anula nota de crédito 1' }],
    totals: { totalAmount: 0 },
    references: [
      { documentType: 61, folio: 1, date: '2026-11-06', code: REFERENCE_CODES.cancels, reason: 'Anula nota de crédito 1' },
    ],
  },

La primera sube la factura con dos horas de soporte más: neto 50000, IVA 9500, total 59500, con código 3. La segunda anula la nota de crédito de folio 1: referencia a la nota (documentType: 61) con código 1 y tiene una sola línea sin montos, como hace la corrida de certificación del SDK. Ejecútalas con node credit-note.ts debit y node credit-note.ts cancel-credit-note. El programa siempre pregunta por la factura al final. Cuando una nota de débito referencia una factura, el estado de la factura pasa a MMD (montos), TMD (texto) o AND (anulada); para cancel-credit-note, cambia la pregunta para consultar por la propia nota de crédito (su tipo, folio, fecha y total).

Qué puede salir mal#

CódigoDóndeQué hacer
CAF_TYPE_MISMATCHissueDocumentEl CAF no es del tipo de la nota. Una nota necesita su propio CAF (61 o 56).
CAF_ISSUER_MISMATCH, CAF_EXPIRED, FOLIO_OUT_OF_RANGEissueDocument, reserveEl CAF es de otro RUT, tiene más de seis meses, o no tiene más folios.
MISSING_FIELDcreateDocumentUna nota necesita un receptor con su dirección y comuna, y al menos una línea. details.field dice cuál.
INVALID_DATEcreateDocumentLa date de la referencia no es una fecha válida. Usa AAAA-MM-DD (también se acepta DD-MM-AAAA).
FIELD_TOO_LONGcreateDocumentLa reason tiene más de 90 caracteres, o el nombre de una línea más de 80.
INVALID_OPTIONcreateDocumentreceiptReferences es solo para boletas (39 y 41). Una nota usa references.
SII_UPLOAD_REJECTED, WAIT_TIMEOUT, códigos de transporteEnvío y esperaLo mismo que en la factura: ver Qué puede salir mal.

Si el estado del sobre es rejected (RPT, RSC esquema; RFR firma; RCT carátula), el programa termina con un código de salida distinto de cero y no se pregunta nada sobre la factura. La respuesta del SII está en status.raw, y los archivos de data/capture y data/uploads guardan el intercambio.

Siguientes pasos#

  • Emitir una boleta para la boleta, tipos 39 y 41, que tiene su propio sobre y su propia forma de referencia.
  • Documentos para cada campo de un documento y sus límites, y Totales para cómo se calculan los montos.
  • Servicios web del SII para queryDocumentStatus y el registro de aceptación y reclamo.
  • Certificación: los sets de pruebas incluyen notas con estos tres códigos, y el corredor las emite por ti.