Issue a Credit Note
Issue a type 61 electronic credit note that references invoice folio 1024, either to void it completely (code 1) or to correct its amounts (code 3).
This page builds on Issue an Invoice and reuses its lib/dte.ts module. The invoice being corrected is the one issued there:
| Field | Value |
|---|---|
| Type and folio | 33, folio 1024 |
| Issued | 2026-10-28 |
| Receiver | 77.654.321-7, Comercial Los Andes Ltda. |
| Lines | Licencia anual 1 × 120000; Curso de capacitación 2 × 45000 (exempt) |
| Totals | net 120000, exempt 90000, IVA 22800, total 232800 |
What You Need#
- A CAF for type 61. Each document type has its own CAF: stamping a credit note with the invoice CAF throws
CAF_MISMATCH. AddDTE_CAF_61_PATHto your environment. - The original invoice, as the invoice flow stored it: folio, issue date, receiver and lines.
- The same certificate you use for invoices.
1. Build the Credit Note#
One function handles both variants. The reference to the invoice is what makes it a correction, and createDte() requires it for type 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. Void the Invoice (Code 1)#
Use code 1 when the whole invoice should not exist: a cancelled order, a duplicate, a document issued to the wrong customer. Copy the invoice's lines exactly, so the credit note cancels every 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. Correct the Amounts (Code 3)#
Use code 3 when the invoice stays valid but some amounts were wrong. The credit note lists only the difference, never the corrected full amount.
Say the customer got a 20000 discount on the license after the invoice was issued, and dropped one of the two training seats:
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 }After this note, the invoice is effectively worth 232800 − 68800 = 164000: net 100000, IVA 19000 and exempt 45000.
Amounts on a credit note are positive. The document type is what tells the SII to subtract them; a negative unitPrice throws INVALID_AMOUNT.
Code 2 fixes text only, such as a misspelled address on the invoice, and
carries no amount corrections. This page covers codes 1 and 3.
3. Store, Send and Follow It#
Same pattern as the invoice: store the signed XML first, then send, then persist the 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
}The status job from the invoice example works the same way: call getStatus() with the track id and look for the document with type === 61 and the credit note's folio.
Errors to Expect#
| Code | Typical cause in this flow |
|---|---|
MISSING_FIELD | The reference to the invoice is missing (when calling createDte() directly) |
CAF_MISMATCH | Stamping with the type 33 CAF instead of the type 61 one |
FOLIO_OUT_OF_RANGE | The type 61 folio sequence went past the CAF range |
INVALID_AMOUNT | A negative amount on a line |
Related#
- Issue an Invoice - The invoice this note corrects, and the shared modules
createDte()- Thereferencesfield and its codesstampDte()- One CAF per document typecalculateTotals()- How the note's totals are calculated