Emitir una nota de crédito
Emite una nota de crédito electrónica tipo 61 que referencia la factura folio 1024, ya sea para anularla por completo (código 1) o para corregir sus montos (código 3).
Esta página se basa en Emitir una factura y reutiliza su módulo lib/dte.ts. La factura que se corrige es la que se emitió ahí:
| Campo | Valor |
|---|---|
| Tipo y folio | 33, folio 1024 |
| Emitida | 2026-10-28 |
| Receptor | 77.654.321-7, Comercial Los Andes Ltda. |
| Líneas | Licencia anual 1 × 120000; Curso de capacitación 2 × 45000 (exento) |
| Totales | neto 120000, exento 90000, IVA 22800, total 232800 |
Qué necesitas#
- Un CAF para el tipo 61. Cada tipo de documento tiene su propio CAF: timbrar una nota de crédito con el CAF de facturas lanza
CAF_MISMATCH. AgregaDTE_CAF_61_PATHa tus variables de entorno. - La factura original, tal como la guardó el flujo de facturas: folio, fecha de emisión, receptor y líneas.
- El mismo certificado que usas para las facturas.
1. Construye la nota de crédito#
Una sola función cubre las dos variantes. La referencia a la factura es lo que la convierte en una corrección, y createDte() la exige en el tipo 61:
// lib/credit-note.ts
import { readFile } from 'node:fs/promises'
import { createDte, parseCaf, signDte, stampDte, type Caf, type Dte, type LineItem } from 'dte-sdk'
import { ISSUER, getCredentials } from './dte'
let creditNoteCaf: Promise<Caf> | undefined
function getCreditNoteCaf(): Promise<Caf> {
creditNoteCaf ??= readFile(process.env.DTE_CAF_61_PATH!, 'latin1').then(parseCaf)
return creditNoteCaf
}
export type StoredInvoice = {
folio: number
issuedAt: string
receiver: Dte['receiver']
items: LineItem[]
}
export async function buildCreditNote(input: {
folio: number
invoice: StoredInvoice
code: 1 | 3
reason: string
items: LineItem[]
}) {
const [caf, { certificate }] = await Promise.all([getCreditNoteCaf(), getCredentials()])
const dte = createDte({
type: 61,
folio: input.folio,
issuer: ISSUER,
receiver: input.invoice.receiver, // same receiver as the invoice
items: input.items,
references: [
{
type: 33,
folio: input.invoice.folio,
date: input.invoice.issuedAt,
code: input.code,
reason: input.reason,
},
],
})
return signDte(stampDte(dte, caf), certificate)
}2a. Anula la factura (código 1)#
Usa el código 1 cuando la factura completa no debería existir: un pedido cancelado, un duplicado, un documento emitido al cliente equivocado. Copia exactamente las líneas de la factura, para que la nota de crédito cancele hasta el último peso:
import { buildCreditNote } from '@/lib/credit-note'
import { db } from '@/lib/db'
const invoice = await db.invoices.get(1024)
const folio = await db.folios.next(61) // e.g. 501
const creditNote = await buildCreditNote({
folio,
invoice,
code: 1,
reason: 'Anula factura',
items: invoice.items,
})
creditNote.totals
// { net: 120000, exempt: 90000, vatRate: 19, vat: 22800, total: 232800 }
if (creditNote.totals.total !== invoice.totals.total) {
throw new Error('A voiding credit note must match the invoice total')
}2b. Corrige los montos (código 3)#
Usa el código 3 cuando la factura sigue siendo válida pero algunos montos estaban mal. La nota de crédito lista solo la diferencia, nunca el monto completo corregido.
Supongamos que el cliente obtuvo un descuento de 20000 en la licencia después de emitida la factura, y dejó uno de los dos cupos de capacitación:
import { buildCreditNote } from '@/lib/credit-note'
import { db } from '@/lib/db'
const invoice = await db.invoices.get(1024)
const folio = await db.folios.next(61) // e.g. 501
const creditNote = await buildCreditNote({
folio,
invoice,
code: 3,
reason: 'Corrige montos: descuento en licencia y un cupo menos de capacitación',
items: [
{ name: 'Descuento Licencia anual', quantity: 1, unitPrice: 20000 },
{ name: 'Curso de capacitación', quantity: 1, unitPrice: 45000, exempt: true },
],
})
creditNote.totals
// net: 20000
// vat: round(20000 × 0.19) = 3800
// exempt: 45000
// { net: 20000, exempt: 45000, vatRate: 19, vat: 3800, total: 68800 }Después de esta nota, la factura queda en la práctica en 232800 − 68800 = 164000: neto 100000, IVA 19000 y exento 45000.
Los montos de una nota de crédito son positivos. Es el tipo de documento el que le indica al SII que debe restarlos; un unitPrice negativo lanza INVALID_AMOUNT.
El código 2 corrige solo texto, como una dirección mal escrita en la
factura, y no corrige montos. Esta página cubre los códigos 1 y 3.
3. Guárdala, envíala y haz su seguimiento#
El mismo patrón que con la factura: guarda primero el XML firmado, luego envía, y después guarda el track ID.
import { DteError, sendToSii } from 'dte-sdk'
import { SII, getCredentials } from '@/lib/dte'
import { db } from '@/lib/db'
await db.creditNotes.insert({
folio: creditNote.folio,
invoiceFolio: 1024,
code: 1, // or 3
totals: creditNote.totals,
xml: creditNote.xml,
state: 'signed',
})
try {
const { certificate } = await getCredentials()
const { trackId } = await sendToSii(creditNote, { certificate, ...SII })
await db.creditNotes.update(creditNote.folio, { trackId, state: 'sent' })
} catch (error) {
if (!(error instanceof DteError && error.code === 'SII_UNAVAILABLE')) throw error
// Stays 'signed': upload it from the job, as with invoices
}El job de estado del ejemplo de la factura funciona igual: llama a getStatus() con el track ID y busca el documento con type === 61 y el folio de la nota de crédito.
Errores esperables#
| Código | Causa típica en este flujo |
|---|---|
MISSING_FIELD | Falta la referencia a la factura (al llamar a createDte() directamente) |
CAF_MISMATCH | Timbrar con el CAF del tipo 33 en vez del CAF del tipo 61 |
FOLIO_OUT_OF_RANGE | La secuencia de folios del tipo 61 superó el rango del CAF |
INVALID_AMOUNT | Un monto negativo en una línea |
Relacionado#
- Emitir una factura - La factura que corrige esta nota, y los módulos compartidos
createDte()- El camporeferencesy sus códigosstampDte()- Un CAF por tipo de documentocalculateTotals()- Cómo se calculan los totales de la nota