Contingencia y resultados inciertos
Casi todas las emisiones terminan con un sello. Esta guía es para las otras: la que quedó firmada sin respuesta del Ministerio, la que Hacienda negó, y la que nadie sabe cómo terminó porque el proceso se cayó en medio. Las tres tienen una salida clara, y ninguna de las tres es «volver a emitir».
Los tres finales de una emisión
Una llamada a POST /v1/dte puede terminar de tres maneras, y conviene distinguirlas antes de escribir una sola línea de manejo de errores:
- Sellado.
200, conselloRecibido. Es un documento fiscal. - Firmado sin veredicto.
202, conestado: "contingencia". El documento existe y se le debe a Hacienda. - Negado.
422conmh_rejected. Hacienda lo leyó y dijo que no.
Y hay un cuarto caso que no es una respuesta: no hubo ninguna. Su proceso se cayó, la conexión se cortó, el plazo se agotó. Ese es el más incómodo y el que más cuidado pide.
Qué hacer en cada caso
| Lo que recibió | Qué significa | Qué hacer | Qué no hacer |
|---|---|---|---|
200 sellado | Hay sello. Documento fiscal. | Guardar el jws y el selloRecibido. | — |
202 contingencia | Firmado; el Ministerio no contestó. | Guardar el jws y el numeroControl. Facta retransmite. | Volver a emitir. |
422 mh_rejected | Hacienda lo negó. El correlativo se gastó. | Corregir los datos y reemitir reusando ese mismo número (§167). | Tratarlo como una caída y reintentar igual. |
422 validation_failed | No cumple el esquema oficial. No se gastó nada. | Corregir el campo que nombra details.issues. | — |
422 no_storage_destination | La empresa no tiene destino conectado. No se gastó nada. | Conectar un destino desde la app y repetir. | Reintentar en bucle: esperar no lo arregla. |
409 idempotency_in_flight | La primera petición con esa llave sigue trabajando. | Reintentar en unos segundos con la misma llave. | Cambiar la Idempotency-Key. |
503 service_unavailable / correlative_unavailable | Algo de Facta no contestó. No se gastó número. | Reintentar con la misma llave. | — |
| sin respuesta | Nadie sabe qué pasó. | Repetir la misma petición, con la misma llave. Si no puede, reconciliar. | Emitir de nuevo con una llave nueva. |
El 202, de cerca
Un 202 significa que Facta firmó el documento con el certificado de su empresa y no pudo obtener el sello: el servicio del Ministerio no respondió, tardó de más, o contestó algo que no era un sello.
Para entonces los bytes ya existen. Esa firma es irreversible, y el documento se le debe a Hacienda. La reserva queda retenida en contingencia y se retransmite sola; los plazos de 24 y 72 horas son de la ley, no de Facta.
La respuesta trae todo lo que hace falta para seguir adelante: numeroControl, codigoGeneracion, jws y un detalle con la razón. Lo que no trae —y no podría— es selloRecibido, fhProcesamiento y representacionGrafica: la hoja necesita el sello y el código QR, así que todavía no se puede dibujar.
const dte = await facta.emitir(venta, { idempotencyKey: pedido.id });
if (dte.estado === "contingencia") {
// El documento EXISTE y está firmado. Guarde el jws y el número de control;
// el sello llegará cuando Facta retransmita. No vuelva a emitir.
await pedido.guardar({ jws: dte.jws, numeroControl: dte.numeroControl, sello: null });
return;
}
await pedido.guardar({ jws: dte.jws, numeroControl: dte.numeroControl, sello: dte.selloRecibido });El rechazo, de cerca
Un rechazo es un 422 y no un 502, y la distinción no es estética: Hacienda leyó el documento y dio un veredicto sobre sus datos. Esperar no lo arregla; corregir sí.
Y el correlativo ya se gastó. El §167 de la normativa permite que el documento corregido reuse ese mismo número y ese mismo código de generación, así que la respuesta los nombra:
{
"error": {
"code": "mh_rejected",
"message": "[receptor.nit] NIT CONTRIBUYENTE NO EXISTE",
"details": {
"estado": "RECHAZADO",
"descripcionMsg": "[receptor.nit] NIT CONTRIBUYENTE NO EXISTE",
"observaciones": [],
"codigoGeneracion": "E4311553-DADF-4168-829E-B6B7E50F1D41",
"numeroControl": "DTE-03-M001P001-000000000000177"
}
}
}Con el SDK, esos dos valores salen de error.spent, y las palabras exactas del Ministerio de error.mhObservations. Guárdelas: son la explicación que alguien va a pedir después.
Un detalle que ahorra un susto: si repite la petición con la misma Idempotency-Key y el mismo cuerpo, la respuesta guardada vuelve —el mismo 422, el mismo correlativo gastado— con Idempotency-Replayed: true. Eso es lo que evita que un reintento automático queme un segundo número.
Cuando no hubo respuesta
Es el caso que obliga a pensar. Usted mandó la petición y no sabe si llegó a reservar, a firmar o a transmitir.
- Repita la misma petición, con la misma
Idempotency-Keyy el mismo cuerpo. Si la primera terminó, vuelve su respuesta guardada. Si sigue trabajando, es un409 idempotency_in_flighty se reintenta en unos segundos. - Si el
409nombradetails.codigoGeneracion, es que la primera ya había empezado algo irreversible. Pregunte por ese código conGET /v1/dte/{codigoGeneracion}en vez de esperar las 24 horas del reclamo. - Si perdió la
Idempotency-Key—por eso conviene que sea el número de pedido y no un aleatorio—, reconcilie por fecha.
Reconciliar después de una caída
// Su proceso se cayó y no sabe qué alcanzó a emitir.
// 1) Pregunte por la venta con la MISMA Idempotency-Key: si la primera
// petición terminó, la respuesta guardada vuelve con Idempotency-Replayed.
// 2) Si no sabe si llegó a reservar, pregunte por el día:
const pagina = await facta.listDocuments({ desde: "2026-09-02", hasta: "2026-09-02" });
// 3) Un hueco en su numeración puede ser un RECHAZADO, que no aparece en la
// lista. Pregunte por su código, que sí contesta por los rechazados:
const uno = await facta.consultar("E4311553-DADF-4168-829E-B6B7E50F1D41");
console.log(uno.estado); // "rechazado", "sellado", "contingencia"…Otras guías
Emitir un DTE
Cómo llamar a POST /v1/dte, qué llega firmado por dentro y las tres respuestas que puede recibir.
Leer →Preparar y firmar por separado
Por qué separar la reserva del correlativo de la firma, el prepareToken y sus 15 minutos, y qué pasa si el documento cambia entre las dos llamadas.
Leer →