Saltar al contenido

Para desarrolladores

API de facturación electrónica para El Salvador

Integra la emisión de DTE en tu ERP, POS, e-commerce o app con una API REST documentada en público. Envías la venta; sudte calcula, valida contra el esquema del MH, firma, transmite y te devuelve el sello en la misma respuesta.

POST /v1/documents 201 · sellado
{
  "status": "processed",
  "control_number": "DTE-01-M001P001-000000000000042",
  "generation_code": "8C1F7E2A-4B6D-4E3A-9F1C-2D5B7A9E0C34",
  "reception_stamp": "2026A3F9C1D47E8B4F2A9C0D6E1B5A7C3F8D2E4B",
  "totals": { "subtotal": "17.00", "iva": "1.96", "total": "17.00" },
  "pdf_url": "https://api.sudte.com/v1/documents/doc_01JA…/pdf"
}

Un endpoint para todos los tipos de DTE

Facturas, créditos fiscales y notas se emiten con el mismo POST /v1/documents: cambias el campo type (código del catálogo CAT-002) y sudte aplica las reglas de cada documento. Es más simple de integrar y de mantener que un endpoint distinto por tipo, y el mismo modelo servirá para Guatemala y Costa Rica.

Envías datos de negocio, no el JSON del MH

Mandas receptor, ítems, descuentos, retenciones y forma de pago con nombres claros. sudte se encarga de lo que más rechazos causa:

  • Cálculo con aritmética decimal: IVA incluido en la factura, IVA aparte en el crédito fiscal, redondeo de 8 decimales por ítem y 2 en el resumen.
  • Número de control con correlativo atómico por NIT y código de generación único.
  • Validación contra el esquema JSON oficial antes de firmar, con todos los errores juntos.
  • Fecha y hora de emisión en hora de El Salvador, dentro de la holgura de 30 minutos.
  • Total en letras, condición de la operación y formas de pago que cuadran con el total.

Si ya generas el JSON oficial, envíalo en raw y sudte solo lo valida, firma y transmite.

Síncrono para cajas, asíncrono para lotes

Por defecto (mode=sync) la API espera la respuesta del MH hasta 20 segundos y te devuelve el documento con el sello: perfecto para imprimir el ticket en el POS. Si el MH tarda más, recibes 202 Accepted y el resultado llega por webhook. Con mode=async la respuesta es inmediata y todo se notifica por webhook.

Reintentos seguros e integración sin costo

  • Idempotency-Key obligatorio en cada POST: si tu red falla y reintentas, nunca se emite una factura duplicada. Misma clave con otro cuerpo responde 409. Cómo funciona.
  • dry_run=true calcula y valida contra el esquema sin firmar, transmitir ni consumir correlativo. No se cobra.
  • Sandbox real: las API keys sk_test_ emiten contra el ambiente de pruebas (00) del MH, con sellos reales y sin valor legal. Más sobre el sandbox.

Webhooks firmados

Registra uno o más endpoints y recibe document.processed, document.rejected, document.contingency, document.voided y document.delivered, además de eventos de reportes fiscales. Cada entrega lleva la cabecera Sudte-Signature con una firma HMAC-SHA256 y marca de tiempo para que verifiques el origen y evites repeticiones. Ver eventos y verificación.

Errores claros, en español

Todos los errores siguen el estándar RFC 9457 (application/problem+json) con un type estable, un mensaje en español y la lista errors[] con la ruta exacta de cada campo. Los rechazos del MH incluyen su codigoMsg y observaciones originales junto a una explicación accionable.

422 · application/problem+json
{
  "type": "https://sudte.com/errors/validacion",
  "title": "El documento no es válido",
  "status": 422,
  "detail": "Corrige los campos indicados y vuelve a enviar.",
  "request_id": "req_01JA2N4C8F",
  "errors": [
    { "path": "/receiver/nrc", "message": "El NRC es obligatorio en un crédito fiscal." },
    { "path": "/items/0/unit_price", "message": "Debe ser un monto decimal positivo." }
  ]
}

Varias empresas, sucursales y puntos de venta

Una cuenta puede tener varios emisores (uno por NIT), cada uno con sus establecimientos y puntos de venta codificados por el MH (M001, S001, P001…). Si varias compañías de tu ERP comparten el mismo NIT, comparten un solo emisor y una sola secuencia de números de control: nunca habrá duplicados. Puedes crear API keys limitadas a ciertos emisores, ideal para casas de software que facturan por sus clientes.

Ejemplos de código

Emitir una factura a consumidor final en tu lenguaje favorito:

Factura (01)
curl https://api.sudte.com/v1/documents \
  -H "Authorization: Bearer $SUDTE_API_KEY" \
  -H "Idempotency-Key: venta-000123" \
  -H "Content-Type: application/json" \
  -d '{
    "emitter_id": "emi_01J9Z8K2QF4T6W3X5Y7A9B1C2D",
    "type": "01",
    "establishment": "M001",
    "point_of_sale": "P001",
    "receiver": { "name": "Ana López", "email": "ana@example.com" },
    "items": [{ "description": "Café molido 1 lb", "quantity": "2",
                "unit_price": "8.50", "tax": "gravada" }],
    "payment": { "condition": "1",
                 "methods": [{ "code": "01", "amount": "17.00" }] }
  }'
const res = await fetch('https://api.sudte.com/v1/documents', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SUDTE_API_KEY}`,
    'Idempotency-Key': 'venta-000123',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    emitter_id: 'emi_01J9Z8K2QF4T6W3X5Y7A9B1C2D',
    type: '01',
    establishment: 'M001',
    point_of_sale: 'P001',
    receiver: { name: 'Ana López', email: 'ana@example.com' },
    items: [
      { description: 'Café molido 1 lb', quantity: '2', unit_price: '8.50', tax: 'gravada' },
    ],
    payment: { condition: '1', methods: [{ code: '01', amount: '17.00' }] },
  }),
});

const dte = await res.json();
console.log(dte.control_number, dte.reception_stamp);
import os
import requests

r = requests.post(
    "https://api.sudte.com/v1/documents",
    headers={
        "Authorization": f"Bearer {os.environ['SUDTE_API_KEY']}",
        "Idempotency-Key": "venta-000123",
    },
    json={
        "emitter_id": "emi_01J9Z8K2QF4T6W3X5Y7A9B1C2D",
        "type": "01",
        "establishment": "M001",
        "point_of_sale": "P001",
        "receiver": {"name": "Ana López", "email": "ana@example.com"},
        "items": [{
            "description": "Café molido 1 lb",
            "quantity": "2",
            "unit_price": "8.50",
            "tax": "gravada",
        }],
        "payment": {"condition": "1", "methods": [{"code": "01", "amount": "17.00"}]},
    },
    timeout=30,
)
dte = r.json()
print(dte["control_number"], dte["reception_stamp"])
<?php
$ch = curl_init('https://api.sudte.com/v1/documents');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('SUDTE_API_KEY'),
        'Idempotency-Key: venta-000123',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'emitter_id' => 'emi_01J9Z8K2QF4T6W3X5Y7A9B1C2D',
        'type' => '01',
        'establishment' => 'M001',
        'point_of_sale' => 'P001',
        'receiver' => ['name' => 'Ana López', 'email' => 'ana@example.com'],
        'items' => [[
            'description' => 'Café molido 1 lb',
            'quantity' => '2',
            'unit_price' => '8.50',
            'tax' => 'gravada',
        ]],
        'payment' => ['condition' => '1', 'methods' => [['code' => '01', 'amount' => '17.00']]],
    ]),
]);
$dte = json_decode(curl_exec($ch), true);
echo $dte['control_number'], ' ', $dte['reception_stamp'];
body := strings.NewReader(`{
  "emitter_id": "emi_01J9Z8K2QF4T6W3X5Y7A9B1C2D",
  "type": "01",
  "establishment": "M001",
  "point_of_sale": "P001",
  "receiver": {"name": "Ana López", "email": "ana@example.com"},
  "items": [{"description": "Café molido 1 lb", "quantity": "2",
             "unit_price": "8.50", "tax": "gravada"}],
  "payment": {"condition": "1", "methods": [{"code": "01", "amount": "17.00"}]}
}`)

req, _ := http.NewRequest("POST", "https://api.sudte.com/v1/documents", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SUDTE_API_KEY"))
req.Header.Set("Idempotency-Key", "venta-000123")
req.Header.Set("Content-Type", "application/json")

res, err := http.DefaultClient.Do(req)

Endpoints principales

MétodoRutaQué hace
POST /v1/documents Emitir un DTE (sync o async, con dry_run opcional)
GET /v1/documents Listar y buscar documentos
GET /v1/documents/{id} Estado actualizado, sello y totales
GET /v1/documents/{id}/pdf Representación gráfica con QR
GET /v1/documents/{id}/json JSON oficial firmado con sello
POST /v1/documents/{id}/send Reenviar JSON y PDF por correo
POST /v1/documents/{id}/retry Reintentar o corregir un rechazado
POST /v1/documents/{id}/void Invalidar ante el MH
POST /v1/emitters Registrar una empresa emisora (NIT)
PUT /v1/emitters/{id}/credentials Cargar certificado y credenciales del MH
POST /v1/webhook-endpoints Suscribirse a eventos
GET /v1/catalogs/{code} Catálogos oficiales CAT-001 a CAT-033

La referencia completa está en la documentación y el contrato se descarga en openapi.yaml o openapi.json. Para empezar, sigue el inicio rápido.

Actualizado el 12 de octubre de 2026.

FAQ

Preguntas frecuentes

¿Otra duda? Escríbenos a ventas@sudte.com.

¿Tienen SDK oficiales?

La API es REST con JSON y su contrato OpenAPI 3.1 es público, así que puedes generar un cliente tipado en cualquier lenguaje hoy mismo. Los SDK oficiales para TypeScript, Python, PHP y Go están en desarrollo y se generarán desde ese mismo contrato.

¿Puedo enviar el JSON del MH ya armado?

Sí. Si tu sistema ya genera el JSON oficial, envíalo en el campo raw y sudte solo lo valida contra el esquema, lo firma, lo transmite y guarda el sello. Lo normal es enviar datos de negocio y dejar que sudte calcule y arme el documento.

¿Cuánto tarda la respuesta?

En modo síncrono, lo que tarde el MH en sellar, normalmente unos segundos. Si pasan 20 segundos sin respuesta del MH, la API devuelve 202 con el documento en proceso y te notifica el resultado por webhook.

¿Qué pasa si envío dos veces la misma venta?

Si usas la misma Idempotency-Key, la segunda llamada devuelve el mismo documento sin emitir otro. Si reutilizas la clave con un cuerpo distinto, la API responde 409 para que detectes el error.

¿Cómo me entero de que un documento fue rechazado?

La respuesta trae status rejected con el código y las observaciones del MH traducidas, y además se envía el webhook document.rejected. Puedes corregir y reintentar conservando el mismo código de generación, como exige la Normativa.

¿Cuántos ítems admite un documento?

Hasta 2,000 ítems por documento, el máximo que permite el MH. Los montos se envían como texto decimal ("113.00") para no perder precisión.

Tu API key de pruebas en un minuto

Crea tu cuenta y emite contra el ambiente de pruebas del Ministerio de Hacienda hoy mismo. Sin tarjeta y sin hablar con ventas.