Crear factura
Emite una factura electrónica de consumidor final en El Salvador.
Lumen firma el DTE y lo transmite a Hacienda en una sola llamada. Úsalo desde tu POS, Saas, ERP o checkout cuando el cliente paga y necesitas el documento tributario listo.
La emisión es síncrona: en una sola petición obtienes el resultado final de Hacienda.
Método: POST
Nota
Es necesario el envío del token de acceso <access_token> para hacer uso de este endpoint. Para ver cómo generar el token, vea Cómo generar un token de acceso.
Recuerde: El token de acceso caduca cada hora.
Antes de emitir, la empresa, el establecimiento y el punto de venta deben existir.
Endpoint
https://api-sandbox.lumendte.com/v1/invoicesHeaders
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
Authorization | string | Sí | Bearer <access_token> |
Content-Type | string | Sí | application/json |
x-company-tax-number | string | Sí | NRC de la empresa emisora (con o sin guiones) |
x-establishment-code | string | Sí | Código dado por MH para el establecimiento (ej. M001) |
x-point-of-sale-code | string | Sí | Código dado por MH para el punto de venta (ej. P001) |
Idempotency-Key | string | Sí | Identificador estable de esta venta en tu sistema (1 a 128 caracteres, sin espacios). Ver Idempotencia |
Payload mínimo
Para operaciones de hasta $25,000 USD, la normativa permite omitir los datos del cliente. Este es el payload mínimo para emitir una factura de contado:
{
"items": [
{
"description": "Servicio de consultoría",
"quantity": 1,
"unitPrice": 113.0
}
],
"payments": [
{
"paymentCode": "01",
"amount": 113.0
}
]
}Respuesta exitosa
Status: 201 Created
Si la misma Idempotency-Key ya tiene una factura en PROCESSED, la API responde 200 OK con el mismo cuerpo: el generationCode, el controlNumber y el receivedStamp guardados, y URLs temporales nuevas. No vuelve a transmitir el documento a Hacienda.
Guarda generationCode y controlNumber. Las URLs pdfUrl y jsonUrl expiran en 15 minutos: descárgalas o reenvíalas al cliente de inmediato desde tu backend. No las persistas en tu base de datos para uso posterior; cuando expiren, pide unas nuevas con Descargar PDF y Descargar JSON usando el generationCode.
Si pdfUrl o jsonUrl llegan en null, la factura es válida igualmente, los archivos se generan en la primera llamada a los endpoints de descarga. No vuelvas a emitir la factura.
El campo status indica el resultado:
status | Cuándo aplica |
|---|---|
PROCESSED | Hacienda aceptó el documento |
DRAFT | Solo en sandbox, sin credenciales de facturación y certificado: no se firma ni se transmite |
Procesada (PROCESSED)
{
"status": "PROCESSED",
"generationCode": "FF84E5DB-79C5-42CE-B415-EC510C53EFB5",
"controlNumber": "DTE-01-M001P001-000000000000001",
"receivedStamp": "20219E9D4DC0292F4681AD759B0B0F5CA99DC23G",
"environment": "SANDBOX",
"pdfUrl": "https://files.example.com/invoices/1/FF84E5DB-79C5-42CE-B415-EC510C53EFB5.pdf?X-Amz-Expires=900",
"jsonUrl": "https://files.example.com/invoices/1/FF84E5DB-79C5-42CE-B415-EC510C53EFB5.json?X-Amz-Expires=900"
}Borrador sandbox (DRAFT)
En sandbox sin credenciales listas, Lumen crea el documento como borrador: no llama a firma ni a Hacienda. receivedStamp es null. El PDF y el JSON sí quedan disponibles.
{
"status": "DRAFT",
"generationCode": "FF84E5DB-79C5-42CE-B415-EC510C53EFB5",
"controlNumber": "DTE-01-M001P001-000000000000001",
"receivedStamp": null,
"environment": "SANDBOX",
"pdfUrl": "https://files.example.com/invoices/1/FF84E5DB-79C5-42CE-B415-EC510C53EFB5.pdf?X-Amz-Expires=900",
"jsonUrl": "https://files.example.com/invoices/1/FF84E5DB-79C5-42CE-B415-EC510C53EFB5.json?X-Amz-Expires=900"
}Sandbox vs producción
DRAFT solo ocurre en sandbox. En producción, si faltan las credenciales de
cumplimiento listas, la API responde 404 con COMPANY_CREDENTIAL_NOT_FOUND
en lugar de crear un borrador.
environment: SANDBOX o PRODUCTION.
Estructura completa del payload
Si la venta es mayor a $25,000 USD, o necesitas informar datos del cliente, descuentos, productos exentos o pagos a crédito/mixtos, envía el objeto completo. No se aceptan campos adicionales a los documentados: si envías uno desconocido, la petición se rechaza con 400 INVALID_PARAMETERS.
{
"customer": {
"name": "Juan Perez",
"email": "juan.perez@example.com",
"phone": "7777-7777",
"document": {
"type": "DUI",
"number": "01010101-1"
},
"address": {
"complement": "Col. Escalón, San Salvador",
"departmentCode": "06",
"districtCode": "13"
}
},
"items": [
{
"sku": "PROD-001",
"description": "Servicio de consultoría",
"quantity": 1,
"unitPrice": 113.0,
"taxType": "TAXABLE",
"itemType": "SERVICE",
"unitOfMeasure": "59",
"discountAmount": 0
}
],
"payments": [
{
"paymentCode": "01",
"amount": 56.5
},
{
"paymentCode": "03",
"amount": 56.5,
"reference": "AUTH-998877"
}
]
}Customer
Datos del cliente. Puedes omitirlo en ventas de hasta $25,000 USD. En ventas mayores a $25,000 USD la normativa exige identificar al cliente.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
name | string | Sí | Nombre completo del cliente |
document | object | No | Documento de identificación del cliente |
document.type | string | Solo si envías document | DUI, NIT, PASSPORT, RESIDENT_CARD, OTHER |
document.number | string | Solo si envías document | Número de documento con o sin guiones. |
email | string | No | Correo del cliente |
phone | string | No | Teléfono nacional o internacional (ej. 7777-7777, +14155552671) |
address.complement | string | No | Complemento de dirección (calle, colonia, etc.) |
address.departmentCode | string | No | Código de departamento. Envíalo junto con districtCode. Ver Departamentos |
address.districtCode | string | No | Código de distrito. Envíalo junto con departmentCode; el municipio se deduce automáticamente. Ver Distritos |
Items
Líneas del documento. Mínimo 1, máximo 2000 (si te pasas, la API responde 400 INVOICE_ITEMS_LIMIT_EXCEEDED).
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
description | string | Sí | Descripción del ítem |
quantity | number | Sí | Cantidad (> 0) |
unitPrice | number | Sí | Precio unitario con IVA incluido (> 0) |
sku | string | null | No | Código interno del producto |
taxType | string | No | TAXABLE (default), EXEMPT, NOT_SUBJECT |
itemType | string | No | GOODS, SERVICE (default), BOTH |
unitOfMeasure | string | No | Unidad de medida del producto. Default: "59" (Unidad). Ver Unidades de medida |
discountAmount | number | No | Descuento monetario de la línea, con IVA incluido igual que unitPrice (≥ 0 y ≤ quantity × unitPrice). Es el que se resta del total. Por defecto es 0. |
Ejemplo: 2 unidades a $56.50 con $5.00 de descuento.
{
"description": "Servicio de consultoría",
"quantity": 2,
"unitPrice": 56.5,
"discountAmount": 5.0
}La línea queda en $108.00 con IVA incluido (113.00 − 5.00), de los cuales $12.42 son IVA. Lumen calcula el IVA sobre el monto ya descontado; tú no tienes que separarlo.
Significados de taxType:
Indica si el producto o servicio a facturar es gravado, exento o no sujeto a IVA.
| Valor | Significado |
|---|---|
TAXABLE | Venta gravada |
EXEMPT | Venta exenta |
NOT_SUBJECT | Venta no sujeta |
Significados de itemType:
Indica si el ítem es un producto, un servicio o ambos.
| Valor | Significado |
|---|---|
GOODS | Productos o bienes |
SERVICE | Servicio |
BOTH | Implica tanto servicio como producto a la vez |
Payments
Formas de pago. Mínimo 1. Puedes enviar más de una si la transacción se pagó con varios medios (por ejemplo 50 % con tarjeta y 50 % en efectivo).
Lumen establece la condición de la operación (CAT-016) automáticamente según los pagos enviados:
| Pagos enviados | Condición |
|---|---|
Solo contado (cualquier código ≠ 13) | Contado |
Solo crédito (13) | Crédito |
Crédito (13) + contado | Mixto |
| Campo JSON | Tipo | Requerido | Descripción |
|---|---|---|---|
paymentCode | string | Sí | Código de forma de pago. Ver Formas de pago |
amount | number | Sí | Monto del pago. Envíalo siempre con el valor real de esa forma de pago. |
reference | string | No | Referencia o código de autorización. Se omite en efectivo (01) y en crédito puro |
creditTerm | string | null | Condicional | Tipo de plazo CAT-018. Requerido si paymentCode es 13 (ej. 01 = Días) |
creditPeriod | integer | Condicional | Cantidad de días/meses/años según creditTerm (1–999). Requerido si paymentCode es 13 |
methodDetail | string | null | Condicional | Requerido si paymentCode es 99 (Otros) |
Cuando paymentCode es 13 (cuentas por pagar), debes indicar plazo y periodo. Ejemplo para un pago a crédito de 30 días:
{
"payments": [
{
"paymentCode": "13",
"amount": 150.0,
"creditTerm": "01",
"creditPeriod": 30
}
]
}creditTerm (01) = días; creditPeriod (30) = 30 días de plazo.
Ejemplo de solicitud
curl -X POST https://api-sandbox.lumendte.com/v1/invoices \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer tu_access_token" \
-H "x-company-tax-number: 123456-7" \
-H "x-establishment-code: M001" \
-H "x-point-of-sale-code: P001" \
-H "Idempotency-Key: order-9981" \
-d '{
"items": [
{
"description": "Servicio de consultoría",
"quantity": 1,
"unitPrice": 113.0
}
],
"payments": [
{
"paymentCode": "01",
"amount": 113.0
}
]
}'Idempotencia
Idempotency-Key es obligatorio para todas las integraciones. Identifica una venta de tu empresa en el ambiente actual (SANDBOX o PRODUCTION) y para este tipo de DTE.
Usa un identificador estable de tu sistema, por ejemplo el id del pedido, id interno, etc. Si la petición hace timeout o falla la red, reenvía el mismo POST con la misma clave, internamente se conserva el correlativo y no se emite un segundo DTE.
No omitas el header ni envíes un valor vacío o solo espacios. En esos casos, la petición queda sin protección contra duplicados y cada POST genera y transmite un DTE nuevo.
| Regla | Comportamiento |
|---|---|
| Formato | De 1 a 128 caracteres, sin espacios al inicio, al final o en medio |
| Vacío o solo espacios | No permitido. Se procesa sin protección contra duplicados |
| Valor inválido | 400 INVALID_IDEMPOTENCY_KEY. No se transmite a Hacienda |
| Mismo establecimiento y punto de venta | Obligatorio en cada reintento. |
| Otro establecimiento o punto de venta | 409 IDEMPOTENCY_KEY_CONTEXT_MISMATCH. No se emite nada y no se mueve el correlativo |
Para corregir un rechazo del DTE ante Hacienda, reenvía el POST con la misma Idempotency-Key, el mismo x-establishment-code, el mismo x-point-of-sale-code y el body corregido. Esto es útil en casos cuando por ejemplo el número de identificación del cliente es incorrecto.
Errores
Todas las respuestas de error comparten esta estructura:
{
"errorCode": "MH_RECEPTION_TAXPAYER_ERROR",
"message": "The taxpayer is not active at the tax authority.",
"statusCode": 422,
"status": "REJECTED",
"generationCode": "FF84E5DB-79C5-42CE-B415-EC510C53EFB5",
"controlNumber": "DTE-01-M001P001-000000000000001",
"observations": ["..."]
}| Campo | Cuándo aparece |
|---|---|
errorCode | Siempre disponible. Código estable para manejar el error en tu código |
message | Siempre disponible. En errores de validación (INVALID_PARAMETERS) es un arreglo con cada problema |
statusCode | Siempre disponible. Código HTTP |
status, generationCode, controlNumber | Solo serán reportados en el error si se intentó emitir el DTE. |
observations | Solo si Hacienda rechazó el documento por alguna razón y nos informan el motivo. |
Cómo actuar según status
El campo status indica hasta qué etapa avanzó la emisión del documento. Úsalo para decidir si debes corregir y reintentar la petición o verificar primero el documento para evitar una emisión duplicada.
Al reintentar, envía la misma Idempotency-Key, el mismo x-establishment-code y el mismo x-point-of-sale-code. Consulta Idempotencia para conocer todas las reglas de reutilización de la clave.
| Status en el error | Qué significa | Qué hacer |
|---|---|---|
| (ausente) | El documento no se creó y no se transmitió a Hacienda | Revisa la respuesta, corrige la causa y reintenta la misma petición |
FAILED | Ocurrió un fallo interno y el DTE no fue transmitido a Hacienda | Intenta nuevamente la petición con el mismo Idempotency-Key y el mismo body. Si persiste, contacta a soporte |
REJECTED | Hacienda recibió el DTE y lo rechazó | Corrige los datos según message y observations. Reenvía el mismo POST con la misma Idempotency-Key y el body corregido. |
Códigos de error
Cada error incluye un errorCode estable que no cambia entre versiones. Úsalo para decidir qué hacer en tu integración (POS, ERP, SaaS o checkout) y para mostrar a tus usuarios mensajes claros en su idioma. No dependas del texto de message, que está en inglés y puede cambiar, ni solo del código HTTP, porque varios errores comparten el mismo.
| HTTP | errorCode | Causa y solución | status |
|---|---|---|---|
| 400 | INVALID_PARAMETERS | El body no cumple las validaciones (campo faltante, valor inválido o campo no permitido). Revisa message | — |
| 400 | MISSING_COMPANY_TAX_NUMBER | Falta el header x-company-tax-number | — |
| 400 | MISSING_ESTABLISHMENT_CODE | Falta el header x-establishment-code | — |
| 400 | MISSING_POINT_OF_SALE_CODE | Falta el header x-point-of-sale-code | — |
| 400 | INVOICE_ITEMS_LIMIT_EXCEEDED | Enviaste más de 2000 ítems | — |
| 400 | INVOICE_ITEM_DISCOUNT_EXCEEDS_LINE_TOTAL | El discountAmount de un ítem es mayor que quantity × unitPrice. El message indica qué ítem corregir | — |
| 400 | INVALID_IDEMPOTENCY_KEY | Idempotency-Key tiene más de 128 caracteres, espacios o caracteres de control. No se transmite a Hacienda | — |
| 400 | MH_RECEPTION_VALIDATION_ERROR | Hacienda rechazó un campo, formato, fecha o cálculo | REJECTED |
| 401 | UNAUTHORIZED | Falta Authorization, o el token es inválido o expiró. | — |
| 401 | TENANT_CREDENTIAL_NOT_FOUND | El clientId de tu client credentials no existe o está inactivo | — |
| 404 | COMPANY_NOT_FOUND | No existe una empresa con ese NRC en tu cuenta | — |
| 404 | ESTABLISHMENT_NOT_FOUND | El establecimiento no existe para esa empresa | — |
| 404 | POINT_OF_SALE_NOT_FOUND | El punto de venta no existe o está inactivo en ese establecimiento | — |
| 404 | COMPANY_CREDENTIAL_NOT_FOUND | Faltan las credenciales y el certificado de Hacienda (obligatorio en producción) | — |
| 409 | IDEMPOTENCY_KEY_IN_PROGRESS | Ya hay una emisión en curso para esta Idempotency-Key. Espera y reintenta la misma clave | — |
| 409 | IDEMPOTENCY_KEY_NOT_RETRYABLE | La clave pertenece a un documento en CONTINGENCY y no admite reintento | — |
| 409 | IDEMPOTENCY_KEY_CONTEXT_MISMATCH | La clave ya existe, pero el establecimiento o el punto de venta no coinciden con el documento original | — |
| 409 | MH_RECEPTION_DOCUMENT_ERROR | Hacienda reporta un registro duplicado | REJECTED |
| 422 | MH_RECEPTION_DOCUMENT_ERROR | Hacienda rechazó el documento por normativa, correlativo o documento relacionado | REJECTED |
| 422 | MH_RECEPTION_TAXPAYER_ERROR | El emisor está inactivo, no existe o no está autorizado para emitir facturas ante Hacienda | REJECTED |
| 502 | MH_RECEPTION_SERVICE_ERROR | Error de servicio de Hacienda. Revisa status para saber si fue rechazo o respuesta no confiable | REJECTED |
| 502 | MH_RECEPTION_INVALID_RESPONSE | Hacienda devolvió una respuesta inválida o sin sello de recepción | |
| 504 | MH_RECEPTION_TIMEOUT | Hacienda no respondió a tiempo | |
| 5xx | Otros | Fallas de firma electrónica, de autenticación ante Hacienda o internas. Reintenta con la misma Idempotency-Key; si persiste, contacta a soporte | Ver status del body |