Skip to content

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:

FieldValue
Type and folio33, folio 1024
Issued2026-10-28
Receiver77.654.321-7, Comercial Los Andes Ltda.
LinesLicencia anual 1 × 120000; Curso de capacitación 2 × 45000 (exempt)
Totalsnet 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. Add DTE_CAF_61_PATH to 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:

TypeScript
// 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:

TypeScript
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:

TypeScript
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.

TypeScript
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#

CodeTypical cause in this flow
MISSING_FIELDThe reference to the invoice is missing (when calling createDte() directly)
CAF_MISMATCHStamping with the type 33 CAF instead of the type 61 one
FOLIO_OUT_OF_RANGEThe type 61 folio sequence went past the CAF range
INVALID_AMOUNTA negative amount on a line