Crear crédito fiscal
Emite un comprobante de crédito fiscal electrónico en El Salvador.
Emite un comprobante de crédito fiscal electrónico (CCF) a un cliente contribuyente de IVA. Úsalo desde tu ERP, SaaS o POS.
En lugar de armar el JSON del DTE, separar el IVA de los precios, calcular la retención o percepción de IVA cuando aplique, firmar digitalmente y llamar a recepción del MH, solo envías cliente, ítems y pagos. Lumen abstrae el proceso, transmite y te devuelve PDF/JSON con URLs prefirmadas.
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. A diferencia de la factura de consumidor final, el receptor (customer) es siempre obligatorio.
Endpoint
https://api-sandbox.lumendte.com/v1/tax-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
El CCF exige receptor contribuyente. Este es el payload mínimo para emitir a contado:
{
"customer": {
"name": "Distribuidora Morazán S.A. de C.V.",
"taxIdentityNumber": "0614-280310-102-8",
"taxRegistrationNumber": "298145-4",
"taxpayerType": "MEDIUM",
"economicActivityCode": "46900",
"address": {
"complement": "Blvd. Los Próceres, San Salvador",
"departmentCode": "06",
"districtCode": "13"
}
},
"items": [
{
"description": "Servicio de consultoría",
"quantity": 1,
"unitPrice": 113.0
}
],
"payments": [
{
"paymentCode": "01",
"amount": 113.0
}
]
}Respuesta exitosa
Status: 201 Created
Guarda generationCode y controlNumber. El generationCode también lo necesitas para emitir notas de crédito que hagan referencia a este CCF.
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, el crédito fiscal es válido igualmente, los archivos se generan en la primera llamada a los endpoints de descarga. No vuelvas a emitir el comprobante.
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 |
Procesado (PROCESSED)
{
"status": "PROCESSED",
"generationCode": "FF84E5DB-79C5-42CE-B415-EC510C53EFB5",
"controlNumber": "DTE-03-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 y no tiene validez fiscal. receivedStamp es null. El PDF y el JSON sí quedan disponibles.
{
"status": "DRAFT",
"generationCode": "FF84E5DB-79C5-42CE-B415-EC510C53EFB5",
"controlNumber": "DTE-03-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
facturación y certificado, la API responde 404 con
COMPANY_CREDENTIAL_NOT_FOUND en lugar de crear un borrador.
environment: SANDBOX o PRODUCTION.
Estructura completa del payload
Si necesitas nombre comercial, contacto, descuentos, ítems 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": "Distribuidora Morazán S.A. de C.V.",
"tradeName": "Comercial Morazán",
"taxIdentityNumber": "0614-280310-102-8",
"taxRegistrationNumber": "298145-4",
"email": "facturacion@morazan.com",
"phone": "+503 2255-8888",
"taxpayerType": "MEDIUM",
"economicActivityCode": "46900",
"address": {
"complement": "Blvd. Los Próceres, 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": 113.0
}
]
}Customer
Receptor contribuyente. Siempre obligatorio en CCF, incluida su dirección.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
name | string | Sí | Nombre o razón social del cliente. |
taxIdentityNumber | string | Sí | NIT (14 dígitos) o DUI (9 dígitos) del cliente, con o sin guiones. |
taxRegistrationNumber | string | Sí | NRC del cliente, entre 2 y 8 dígitos (con o sin guiones) |
taxpayerType | string | Sí | SMALL, MEDIUM o LARGE. |
economicActivityCode | string | Sí | Código de actividad económica, entre 5 y 6 caracteres (ej. 46900). Ver Actividades económicas |
address | object | Sí | Dirección del cliente. Hacienda la exige en el CCF |
address.complement | string | Sí | Complemento de dirección (calle, colonia, etc.). |
address.departmentCode | string | Sí | Código de departamento. Envíalo junto con districtCode. Ver Departamentos |
address.districtCode | string | Sí | Código de distrito. Debe existir para el departmentCode enviado; el municipio se deduce automáticamente. Ver Distritos |
tradeName | string | No | Nombre comercial del cliente. |
email | string | No | Correo del cliente. |
phone | string | No | Teléfono nacional o internacional (ej. 2255-8888, +503 2255-8888) |
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). En ítems gravados Lumen separa el IVA; en exentos o no sujetos el precio se toma tal cual |
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). Default: 0 |
Ejemplo: 2 unidades a $565.00 con $56.50 de descuento.
{
"description": "Servicio de consultoría",
"quantity": 2,
"unitPrice": 565.0,
"discountAmount": 56.5
}La línea queda en $950.00 sin IVA (1000.00 − 50.00) más $123.50 de IVA: total $1,073.50, que es 1130.00 − 56.50. En ítems gravados Lumen separa el IVA del descuento y calcula el IVA sobre el monto ya descontado; tú no tienes que separarlo. En ítems exentos o no sujetos el descuento se toma tal cual.
Significados de taxType:
| Valor | Significado |
|---|---|
TAXABLE | Venta gravada |
EXEMPT | Venta exenta |
NOT_SUBJECT | Venta no sujeta |
Significados de itemType:
| 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.
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 | 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 ignora 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. En ventas a crédito envía siempre el monto real de la venta.
Retención y percepción de IVA
No envías estos montos: Lumen los calcula con el taxpayerType de tu empresa y el del cliente. Solo aplican cuando el monto gravado neto (después de descuentos y sin IVA) es de $100.00 o más:
| Tu empresa | Cliente | Resultado |
|---|---|---|
SMALL o MEDIUM | LARGE | Retención del 1%: el total a pagar disminuye |
LARGE | SMALL o MEDIUM | Percepción del 1%: se registra la percepción en el comprobante |
| Otra combinación | — | Sin retención ni percepción |
Ejemplo de solicitud
curl -X POST https://api-sandbox.lumendte.com/v1/tax-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 '{
"customer": {
"name": "Distribuidora Morazán S.A. de C.V.",
"taxIdentityNumber": "0614-280310-102-8",
"taxRegistrationNumber": "298145-4",
"taxpayerType": "MEDIUM",
"economicActivityCode": "46900",
"address": {
"complement": "Blvd. Los Próceres, San Salvador",
"departmentCode": "06",
"districtCode": "13"
}
},
"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 NIT o el NRC 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-03-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 (ERP, SaaS o backend B2B) 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 | ECONOMIC_ACTIVITY_NOT_FOUND | El customer.economicActivityCode no existe. Usa un código del catálogo de Actividades económicas | — |
| 404 | DISTRICT_NOT_FOUND | El customer.address.districtCode no existe para el departmentCode enviado. Revisa el catálogo de Distritos | — |
| 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 | Problema con el emisor o el receptor: NIT/NRC inexistente o inactivo, o emisor no autorizado para emitir CCF | REJECTED |
| 502 | MH_RECEPTION_SERVICE_ERROR | Error de servicio de Hacienda. Usa el status del body para distinguir el caso | 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 |