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=truecalcula 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.
{
"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:
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étodo | Ruta | Qué 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.