Normativa DTE v1.2: Guía técnica para desarrolladores
De qué trata la normativa v1.2
El 17 de noviembre de 2025, el Ministerio de Hacienda publicó la versión 1.2 de la normativa que regula los Documentos Tributarios Electrónicos (DTE) en El Salvador. Este documento es la referencia técnica oficial para cualquier desarrollador que esté construyendo un sistema de facturación electrónica o integrando DTE en un ERP, sistema contable o plataforma de comercio.
La normativa define 11 tipos de documentos tributarios, sus estructuras JSON, las reglas de firma digital mediante JWS (JSON Web Signature), el flujo de transmisión hacia los servidores del Ministerio de Hacienda y las validaciones que tu sistema debe cumplir para que los documentos sean aceptados. Todo el detalle técnico está disponible en el portal oficial de facturación electrónica.
Si estás desarrollando una integración DTE, esta guía te da una visión general de los componentes técnicos clave y código de ejemplo en TypeScript para que puedas arrancar más rápido.
Los 11 tipos de DTE
La normativa define 11 tipos de documentos tributarios electrónicos, cada uno con su código, versión de esquema JSON y caso de uso específico.
| Código | Nombre | Versión JSON | Caso de uso |
|---|---|---|---|
| 01 | Factura Electrónica (FE) | v1 | Ventas a consumidor final |
| 03 | Comprobante de Crédito Fiscal (CCFE) | v3 | Operaciones entre contribuyentes de IVA |
| 04 | Nota de Remisión (NRE) | v3 | Traslado de mercadería |
| 05 | Nota de Crédito (NCE) | v3 | Anulaciones y descuentos |
| 06 | Nota de Débito (NDE) | v3 | Cargos adicionales |
| 07 | Comprobante de Retención (CRE) | v1 | Retención de IVA |
| 08 | Comprobante de Liquidación (CLE) | v1 | Operaciones en comisión |
| 09 | Doc. Contable de Liquidación (DCLE) | v1 | Contabilización de liquidaciones |
| 11 | Factura de Exportación (FEXE) | v2 | Ventas de exportación |
| 14 | Factura de Sujeto Excluido (FSEE) | v1 | Compras a personas no inscritas en IVA |
| 15 | Comprobante de Donación (CDE) | v1 | Donaciones |
El tipo de documento que más vas a trabajar depende del giro de tu cliente. Para la mayoría de negocios que operan entre contribuyentes, el CCFE (código 03) es el documento principal. Para ventas a consumidor final, es la FE (código 01). El resto cubre escenarios más específicos.
Estructura JSON de un DTE
Todos los tipos de DTE comparten una estructura base con cinco secciones principales. Aquí tienes una interfaz TypeScript que representa el esqueleto común:
interface DocumentoTributarioElectronico {
identificacion: {
version: number; // Versión del esquema JSON
ambiente: "00" | "01"; // "00" = pruebas, "01" = producción
tipoDte: string; // Código del tipo (01, 03, 04, etc.)
numeroControl: string; // Formato: DTE-XX-MXXXPXXX-XXXXXXXXXXXXXXX
codigoGeneracion: string; // UUID v4 único por documento
tipoModelo: 1 | 2; // 1 = normal, 2 = contingencia
tipoOperacion: 1 | 2; // 1 = transmisión normal, 2 = transmisión por contingencia
tipoContingencia: null | 1 | 2 | 3 | 4 | 5;
motivoContin: string | null;
fecEmi: string; // Fecha emisión: "YYYY-MM-DD"
horEmi: string; // Hora emisión: "HH:mm:ss"
tipoMoneda: "USD"; // Siempre USD para El Salvador
};
emisor: {
nit: string; // NIT del emisor (sin guiones)
nrc: string; // Número de Registro de Contribuyente
nombre: string; // Razón social
codActividad: string; // Código de actividad económica
descActividad: string; // Descripción de la actividad
nombreComercial: string;
tipoEstablecimiento: string;
direccion: {
departamento: string; // Código de departamento (01-14)
municipio: string; // Código de municipio
complemento: string; // Dirección completa
};
telefono: string;
correo: string;
codEstableMH: string | null;
codEstable: string | null;
codPuntoVentaMH: string | null;
codPuntoVenta: string | null;
};
receptor: {
// Varía según el tipo de DTE
// CCFE requiere NIT/NRC del receptor
// FE puede usar DUI o ser consumidor final
};
cuerpoDocumento: Array<{
numItem: number; // Número de línea (1, 2, 3...)
tipoItem: 1 | 2 | 3 | 4; // 1=bien, 2=servicio, 3=ambos, 4=otro
codigo: string | null;
descripcion: string;
cantidad: number;
uniMedida: number; // Código unidad de medida
precioUni: number; // Precio unitario (8 decimales)
montoDescu: number; // Descuento por item
ventaNoSuj: number; // Venta no sujeta
ventaExenta: number; // Venta exenta
ventaGravada: number; // Venta gravada
}>;
resumen: {
totalNoSuj: number; // Total no sujeto
totalExenta: number; // Total exento
totalGravada: number; // Total gravado
subTotalVentas: number; // Subtotal
descuNoSuj: number;
descuExenta: number;
descuGravada: number;
totalDescu: number; // Total descuentos
subTotal: number;
montoTotalOperacion: number; // Total de la operación
ivaRete1: number; // IVA retenido
reteRenta: number; // Retención de renta
totalPagar: number; // Total a pagar (2 decimales)
totalLetras: string; // Monto en letras
condicionOperacion: 1 | 2 | 3; // 1=contado, 2=crédito, 3=otro
};
}La sección identificacion es donde defines qué tipo de documento es, su código de generación único y el Número de Control asignado por tu sistema. La sección emisor contiene los datos fiscales de quien emite el documento. El receptor varía según el tipo de DTE. El cuerpoDocumento es el detalle de los items. Y el resumen consolida los totales.
Generación de UUID y Número de Control
Cada DTE necesita dos identificadores únicos: el codigoGeneracion (un UUID v4) y el numeroControl (un código con formato específico).
Código de generación
El código de generación es simplemente un UUID v4 en mayúsculas:
function generarCodigoGeneracion(): string {
return crypto.randomUUID().toUpperCase();
}
// Resultado: "A1B2C3D4-E5F6-7890-ABCD-EF1234567890"Número de Control
El Número de Control sigue el formato DTE-XX-MXXXPXXX-XXXXXXXXXXXXXXX donde:
DTEes el prefijo fijoXXes el código del tipo de DTE (01, 03, 05, etc.)MXXXPXXXidentifica el establecimiento y punto de ventaXXXXXXXXXXXXXXXes un correlativo de 15 dígitos
function generarNumeroControl(
tipoDte: string,
codigoEstablecimiento: string,
codigoPuntoVenta: string,
correlativo: number
): string {
const corr = correlativo.toString().padStart(15, "0");
return `DTE-${tipoDte}-${codigoEstablecimiento}${codigoPuntoVenta}-${corr}`;
}
// Ejemplo para un CCFE:
// generarNumeroControl("03", "M001", "P001", 1)
// Resultado: "DTE-03-M001P001-000000000000001"El correlativo es responsabilidad de tu sistema. Debe ser secuencial y no repetirse para la combinación de tipo + establecimiento + punto de venta. Además, la normativa establece que el número de control debe iniciar en 1 cada 1 de enero — no puede repetirse dentro de un mismo año calendario. Esto significa que tu sistema necesita un mecanismo de reinicio anual del correlativo por cada combinación de tipo + establecimiento + punto de venta.
Construcción de un CCFE (tipo 03)
El Comprobante de Crédito Fiscal es el documento más común en operaciones entre contribuyentes de IVA. Aquí tienes una función TypeScript que construye un esqueleto válido:
interface CcfeParams {
emisor: {
nit: string;
nrc: string;
nombre: string;
codActividad: string;
descActividad: string;
nombreComercial: string;
direccion: { departamento: string; municipio: string; complemento: string };
telefono: string;
correo: string;
codEstableMH: string;
codPuntoVentaMH: string;
};
receptor: {
nit: string;
nrc: string;
nombre: string;
codActividad: string;
descActividad: string;
nombreComercial: string;
direccion: { departamento: string; municipio: string; complemento: string };
telefono: string;
correo: string;
};
items: Array<{
descripcion: string;
cantidad: number;
precioUni: number; // Precio sin IVA
descuento?: number;
}>;
correlativo: number;
codEstablecimiento: string;
codPuntoVenta: string;
}
function construirCcfe(params: CcfeParams) {
const { emisor, receptor, items, correlativo } = params;
const ahora = new Date();
const fecEmi = ahora.toISOString().split("T")[0];
const horEmi = ahora.toTimeString().split(" ")[0];
// Construir cuerpo del documento con cálculos de IVA
const cuerpoDocumento = items.map((item, index) => {
const montoDescu = item.descuento ?? 0;
const ventaGravada = parseFloat(
(item.cantidad * item.precioUni - montoDescu).toFixed(8)
);
return {
numItem: index + 1,
tipoItem: 1 as const, // 1 = bien
codigo: null,
descripcion: item.descripcion,
cantidad: item.cantidad,
uniMedida: 59, // 59 = unidad
precioUni: item.precioUni,
montoDescu,
ventaNoSuj: 0,
ventaExenta: 0,
ventaGravada,
tributos: ["20"], // "20" = IVA
};
});
// Calcular resumen
const totalGravada = cuerpoDocumento.reduce(
(sum, item) => sum + item.ventaGravada, 0
);
const iva = parseFloat((totalGravada * 0.13).toFixed(2));
const totalPagar = parseFloat((totalGravada + iva).toFixed(2));
return {
identificacion: {
version: 3, // CCFE usa versión 3
ambiente: "00" as const, // "00" = pruebas
tipoDte: "03",
numeroControl: generarNumeroControl(
"03",
params.codEstablecimiento,
params.codPuntoVenta,
correlativo
),
codigoGeneracion: generarCodigoGeneracion(),
tipoModelo: 1 as const, // Modelo normal
tipoOperacion: 1 as const, // Transmisión normal
tipoContingencia: null,
motivoContin: null,
fecEmi,
horEmi,
tipoMoneda: "USD" as const,
},
emisor: {
...emisor,
tipoEstablecimiento: "01", // Casa matriz
codEstable: params.codEstablecimiento,
codPuntoVenta: params.codPuntoVenta,
},
receptor: {
...receptor,
tipoDocumento: "36", // NIT
numDocumento: receptor.nit,
},
cuerpoDocumento,
resumen: {
totalNoSuj: 0,
totalExenta: 0,
totalGravada: parseFloat(totalGravada.toFixed(2)),
subTotalVentas: parseFloat(totalGravada.toFixed(2)),
descuNoSuj: 0,
descuExenta: 0,
descuGravada: 0,
totalDescu: 0,
subTotal: parseFloat(totalGravada.toFixed(2)),
ivaRete1: 0,
reteRenta: 0,
montoTotalOperacion: totalPagar,
totalPagar,
totalLetras: "", // Debes generar esto aparte
condicionOperacion: 1,
tributos: [{
codigo: "20",
descripcion: "Impuesto al Valor Agregado 13%",
valor: iva,
}],
},
};
}Este esqueleto cubre el caso más simple: una venta al contado con items gravados al 13% de IVA. En producción necesitarás manejar ventas exentas, descuentos, retenciones y las distintas condiciones de operación.
Firma JWS: firmar antes de transmitir
Todos los DTE deben firmarse digitalmente antes de ser transmitidos al Ministerio de Hacienda. La normativa exige el estándar JWS (JSON Web Signature) con el algoritmo RSA-SHA512.
El flujo es:
- Construyes el JSON del DTE
- Lo firmas con tu llave privada RSA usando el algoritmo RS512
- El resultado es un token JWS (tres segmentos separados por puntos: header.payload.signature)
- Ese token JWS es lo que transmites al Ministerio
El Ministerio de Hacienda ofrece la herramienta open-source SVFE-API-Firmador que puedes usar como servicio de firma independiente. Es una API REST a la que le envías el JSON y tu certificado, y te devuelve el JWS firmado.
Si prefieres firmar directamente en tu aplicación, necesitas tu certificado digital (.crt) emitido por el Ministerio de Hacienda — un archivo XML con la estructura CertificadoMH que contiene tu llave privada codificada en base64 — y una librería JWS compatible con RS512.
Si extraes la llave privada del certificado XML a formato PEM, puedes firmar directamente con la librería jose:
import { SignJWT, importPKCS8 } from "jose";
async function firmarDte(
dte: Record<string, unknown>,
privateKeyPem: string
): Promise<string> {
const privateKey = await importPKCS8(privateKeyPem, "RS512");
const jws = await new SignJWT(dte as unknown as Record<string, unknown>)
.setProtectedHeader({ alg: "RS512" })
.sign(privateKey);
return jws;
}Si prefieres una solución que ya maneja el formato de certificado del Ministerio de Hacienda, el paquete dte-signer-sv abstrae la extracción de la llave privada del XML y la firma en un solo paso:
import { signDTE } from "dte-signer-sv";
const jws = await signDTE({
dte: miDocumentoDte,
privatePassword: "contraseña-del-certificado",
certificate: "./certificados/88888888888888.crt",
loadCertificateFromPath: true,
});Lo importante es que la firma debe hacerse sobre el JSON completo del DTE. Si modificas cualquier campo después de firmar, la validación va a fallar en el lado del Ministerio.
Transmisión: ambientes de prueba y producción
El Ministerio de Hacienda maneja dos ambientes: pruebas (dte-test) y producción. El flujo de transmisión es el mismo en ambos, solo cambian los endpoints.
Flujo de autenticación y envío
-
Obtener token de autenticación. Envías tus credenciales (NIT + contraseña) al endpoint de autenticación. Recibes un token JWT con vigencia limitada.
-
Transmitir el DTE firmado. Envías el JWS al endpoint de recepción junto con tu token de autenticación.
-
Recibir el sello de recepción. Si el documento pasa las validaciones, el Ministerio te devuelve un sello de recepción que confirma que el DTE fue aceptado. Este sello es tu comprobante de que el documento tiene validez fiscal.
Si el documento tiene errores de estructura, cálculo o datos, recibes un rechazo con el detalle de las observaciones. Tu sistema debe estar preparado para manejar tanto la aceptación como el rechazo, y para reintentar en caso de errores de conexión.
Consideración para desarrollo
Siempre desarrolla y prueba contra el ambiente de pruebas primero. Los documentos emitidos en ambiente: "00" no tienen validez fiscal. Solo cuando todo funcione correctamente, cambia a ambiente: "01" para producción.
Estados de un documento después de transmitir
Una vez que transmites un DTE, el Ministerio de Hacienda lo clasifica en uno de estos estados:
- Transmitido satisfactoriamente: el documento fue aceptado y recibe un Sello de Recepción.
- Ajustado: el documento fue modificado por otro documento posterior (por ejemplo, una Nota de Crédito que lo afecta).
- Observado: el Ministerio agregó observaciones al documento, pero sigue siendo fiscalmente válido.
- Rechazado: el documento fue rechazado por errores de estructura, cálculo o datos. Debe corregirse y retransmitirse dentro de las 24 horas siguientes a la notificación de rechazo.
Ese plazo de 24 horas es estricto. Tu sistema debe detectar rechazos rápidamente y facilitar la corrección y reenvío antes de que se venza la ventana.
Transmisión en contingencia
El modo de contingencia (tipoModelo: 2) existe para cuando no puedes conectarte a los servidores del Ministerio. Solo aplica a estos tipos de DTE: CCFE, FE, FEXE, NRE, NCE, NDE y FSEE. Los tipos CLE, CRE, DCLE y CDE no soportan contingencia.
Cuando operas en contingencia, tu sistema debe reintentar la conexión al menos cada 15 minutos. Una vez que se restablezca la comunicación, tienes 72 horas desde el Evento de Contingencia para transmitir todos los documentos acumulados.
El campo tipoContingencia (valores del 1 al 5) indica el motivo: desde fallas de internet hasta desastres naturales. Tu sistema debe registrar el motivo y el momento exacto en que inició la contingencia, porque ambos datos van en el JSON del DTE.
Invalidación de documentos
Hay dos razones por las que puedes invalidar un DTE que ya tiene Sello de Recepción:
- Errores: equivocaciones en la captura de datos — fecha incorrecta, nombre mal escrito, descripción equivocada de un item, etc.
- Afectaciones: modificaciones de negocio — cambios de precio, sustitución de servicios, o la rescisión total de la operación.
Solo puedes invalidar documentos que ya fueron aceptados (es decir, que tienen Sello de Recepción). Para FE y FEXE, la invalidación está permitida dentro de los 3 meses siguientes a la fecha de emisión. En el caso de un CLE, la NCE relacionada también debe pasar por el proceso de invalidación.
El Evento de Invalidación debe transmitirse al Ministerio el día siguiente a la obtención del Sello de Recepción de la invalidación. Desde el lado de desarrollo, esto significa que tu sistema necesita rastrear los estados de cada documento y aplicar estas ventanas de tiempo automáticamente para evitar que una invalidación se pase de plazo.
Reglas de redondeo: un detalle que rompe integraciones
Este es uno de los puntos donde más fallan las integraciones. La normativa establece reglas precisas de redondeo que tu sistema debe respetar:
- A nivel de item (
cuerpoDocumento): los cálculos deben usar 8 decimales. Esto aplica aprecioUni,ventaGravada,ventaExentayventaNoSuj. - A nivel de resumen (
resumen): los totales se redondean a 2 decimales. - Tolerancia: se permite una diferencia de +/- $0.01 entre la suma de los items y los totales del resumen. Si tu diferencia excede ese centavo, el documento será rechazado.
// Correcto: 8 decimales en items
const precioUni = 10.12345678;
const ventaGravada = parseFloat((cantidad * precioUni).toFixed(8));
// Correcto: 2 decimales en resumen
const totalGravada = parseFloat(
items.reduce((sum, item) => sum + item.ventaGravada, 0).toFixed(2)
);Si estás migrando desde un sistema que usa 2 decimales en todo, vas a necesitar ajustar la precisión de los cálculos intermedios. Este cambio parece menor, pero es la causa de una cantidad considerable de rechazos en integraciones nuevas.
Cambios en la versión 1.2
La versión 1.2 de la normativa introduce un cambio específico en la sección 7: se agrega la funcionalidad de "compra por cuenta de terceros" para las Facturas de Exportación (FEXE, código 11). Esto permite documentar operaciones donde una entidad realiza compras de exportación en nombre de otra. Este cambio también es la razón por la que el esquema JSON de FEXE pasó de v1 a v2.
En concreto, se agregan tres campos en la sección "Compra por cuenta de terceros":
- Campo 72:
compraCuentaTerceros— un objeto onull. Es obligatorio cuando la operación aplica, y solo se usa en FEXE. - Campo 73: Número de documento de identificación del tercero — alfanumérico, máximo 20 caracteres.
- Campo 74: Nombre del tercero — alfanumérico, máximo 250 caracteres.
La estructura en tu JSON se ve así:
// Sección 7: Compra por cuenta de terceros (solo FEXE, v1.2)
interface CompraCuentaTerceros {
nit: string; // Campo 73: documento de identificación, máx. 20 caracteres
nombre: string; // Campo 74: nombre o razón social, máx. 250 caracteres
}En el JSON del DTE, el campo compraCuentaTerceros es este objeto cuando aplica, o null cuando la operación no involucra compra por cuenta de un tercero.
El Ministerio de Hacienda otorga un período de adaptación de 3 meses a partir de la fecha de publicación (17 de noviembre de 2025) para que los sistemas incorporen este cambio. La fecha límite para tener tu sistema actualizado es febrero de 2026.
Si tu sistema no maneja facturas de exportación, este cambio no te afecta directamente. Pero si estás construyendo una plataforma de facturación electrónica de uso general, debes implementar los campos adicionales para FEXE que soportan esta modalidad.
Prueba el Generador de Plantillas DTE
Prueba el Generador de Plantillas DTE
Selecciona cualquiera de los 11 tipos de DTE, configura las opciones y obtén un JSON de plantilla listo para usar en tu sistema.
Ir al Generador DTE →Referencias:
- Portal de Facturación Electrónica — factura.gob.sv
- Esquemas JSON oficiales — Ministerio de Hacienda
- Manual Técnico de Facturación Electrónica — Ministerio de Hacienda
- Catálogos del Sistema de Transmisión — Ministerio de Hacienda (PDF)
- Catálogos del Sistema de Transmisión v1.2 — Ministerio de Hacienda (XLSX)
- Normativa de Cumplimiento DTE v1.2 — Ministerio de Hacienda (PDF)