factadesarrolladores API 0.1.0-esqueleto

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, con selloRecibido. Es un documento fiscal.
  • Firmado sin veredicto. 202, con estado: "contingencia". El documento existe y se le debe a Hacienda.
  • Negado. 422 con mh_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é significaQué hacerQué no hacer
200 selladoHay sello. Documento fiscal.Guardar el jws y el selloRecibido.
202 contingenciaFirmado; el Ministerio no contestó.Guardar el jws y el numeroControl. Facta retransmite.Volver a emitir.
422 mh_rejectedHacienda 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_failedNo cumple el esquema oficial. No se gastó nada.Corregir el campo que nombra details.issues.
422 no_storage_destinationLa 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_flightLa primera petición con esa llave sigue trabajando.Reintentar en unos segundos con la misma llave.Cambiar la Idempotency-Key.
503 service_unavailable / correlative_unavailableAlgo de Facta no contestó. No se gastó número.Reintentar con la misma llave.
sin respuestaNadie 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.

typescript
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:

jsonrespuesta 422
{
  "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.

  1. Repita la misma petición, con la misma Idempotency-Key y el mismo cuerpo. Si la primera terminó, vuelve su respuesta guardada. Si sigue trabajando, es un 409 idempotency_in_flight y se reintenta en unos segundos.
  2. Si el 409 nombra details.codigoGeneracion, es que la primera ya había empezado algo irreversible. Pregunte por ese código con GET /v1/dte/{codigoGeneracion} en vez de esperar las 24 horas del reclamo.
  3. 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

typescript
// 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

Escriba para buscar Por ejemplo: idempotencia, contingencia, 422, emitir.

moverse Enter abrir Esc cerrar