Saltar al contenido principal

Contingencia

El Ministerio de Hacienda se cae. A veces. No por mucho tiempo, pero pasa. El API Connect está diseñado para que esto no rompa tu integración y para que tú no tengas que implementar colas, reintentos ni state machines.

Qué es contingencia

Cuando emites un DTE vía API, Ocote hace dos cosas:

  1. Registra el documento internamente — le asigna correlativo, lo firma, lo guarda.
  2. Lo envía al MH y espera respuesta — normalmente con sello de recepción.

Si el paso 2 falla (MH no responde, timeout, error de infraestructura del lado MH), el documento sigue existiendo en Ocote con correlativo válido y firmado. Simplemente no tiene sello MH todavía. Eso es contingencia.

La normativa MH contempla este escenario: un contribuyente puede emitir documentos en contingencia mientras el MH no está disponible, y ellos se validan a posteriori cuando MH vuelve.

Qué ves tú en el response

Un documento en contingencia responde así:

{
"success": true,
"dte_success": false,
"contingency": true,
"rejected": false,
"posted": true,
"is_duplicate": false,

"document_id": "a1b2c3d4-...",
"external_ref": "VENTA-2026-0042",
"control_number": "DTE-01-M001P001-000000000000123",
"generation_code": "A1B2C3D4-...",
"reception_stamp": "",

"dte_state": "",
"dte_message": "",
"observaciones": [],

"ticket_url": "https://ocote.io/api/connect/file/...",
"json_url": ""
}

Tres cosas a notar:

  • success: true — la operación está completa desde el punto de vista del API. No reintentes.
  • dte_success: false y reception_stamp: "" — el sello MH aún no existe.
  • ticket_url sí existe, json_url es vacío — el ticket se puede imprimir en contingencia, el JSON firmado con sello solo existe después de la validación MH.

Qué pasa detrás

Una vez que el documento queda en contingencia, Ocote lo añade a una cola interna de revalidación automática. Periódicamente, un worker reintenta el envío al MH hasta que:

  • El MH responde con sello → el documento pasa a dte_success: true y contingency: false.
  • El MH responde con rechazo → el documento pasa a rejected: true.

Este proceso es invisible para ti. No necesitas llamar ningún endpoint, no necesitas implementar polling. El documento se actualizará solo.

Tiempo típico: la revalidación corre cada pocos minutos. Si el MH vuelve rápido, el documento sella en 5-10 minutos. Si el MH está caído por horas, se queda en contingencia hasta que vuelva.

Qué debes hacer tú

Al recibir el response con contingency: true:

  • Marca el documento en tu sistema como "Pendiente MH" o similar.
  • Muestra al usuario que la transacción está completa (tiene correlativo, tiene ticket, es fiscal).
  • No reintentes el POST de emisión. El documento ya existe; reintentar sería redundante (te devolverá is_duplicate: true) pero no acelera nada.

Para saber cuándo selló:

  • Opción A (recomendada): no hagas nada. Si tu sistema no necesita conocer el sello en tiempo real, ignóralo. El documento es fiscalmente válido desde que tiene correlativo.
  • Opción B: consulta /status/{external_ref} ocasionalmente para ese documento y revisa si dte_success pasó a true.
  • Opción C: agenda un barrido periódico (cada hora, cada día) que consulta /status de tus documentos con contingency: true y actualiza tu BD.
El ticket PDF es válido en contingencia

Para documentos tipo 01 (Factura), el ticket_url devuelve un PDF listo para imprimir aunque esté en contingencia. El ticket incluye una nota indicando el estado de transmisión. Es legal entregárselo al cliente final.

Polling responsable

Si eliges la Opción C, un patrón saludable:

def sync_pending_documents():
"""
Corre cada hora via cron. Busca documentos en contingencia
en tu BD y consulta el estado actual en Ocote.
"""
pending = db.query(Sale).filter(Sale.mh_status == "PENDING").all()

for sale in pending:
r = requests.get(
f"https://ocote.io/api/connect/status/{sale.external_ref}",
headers={"Authorization": f"Bearer {API_KEY}"},
)
data = r.json()

if data["dte_success"]:
sale.mh_status = "ISSUED"
sale.reception_stamp = data["reception_stamp"]
db.commit()
elif data["rejected"]:
sale.mh_status = "REJECTED"
sale.mh_observaciones = data["observaciones"]
db.commit()
# Si sigue en contingency, no hacer nada, reintentar en el proximo ciclo

No es necesario poll más seguido que cada hora para la mayoría de casos de uso. Polling agresivo (cada minuto) consume rate limit innecesariamente.

Rechazo vs contingencia — no los confundas

Son escenarios distintos y requieren acciones distintas:

ContingenciaRechazo
Flag principalcontingency: truerejected: true
successtruefalse
Causa típicaMH caído, timeout, infraestructuraDatos inválidos en el payload (NIT mal, email mal, actividad inexistente)
¿Documento fiscalmente emitido?Sí, con correlativoSí, con correlativo, pero el MH lo repudió
¿Ticket válido?Técnicamente no — conviene no entregarlo hasta corregir
¿Qué haces?Nada, Ocote revalida soloCorregir datos y reintentar con mismo external_ref

Ver Retry e idempotencia para el flujo de rechazo.

Qué pasa si el MH nunca vuelve

Caso límite: MH caído por días (ha pasado). Ocote sigue reintentando. Mientras tanto, desde el punto de vista fiscal y operativo:

  • Todos los documentos en contingencia son legalmente válidos.
  • El cliente final recibió ticket.
  • Tu contabilidad los cuenta como emitidos para efectos de ventas del día.
  • Cuando MH vuelva, todos se validarán en batch.

La única situación verdaderamente mala sería que MH esté caído para siempre — y si eso pasa, probablemente tengamos problemas más grandes que este API.

Invalidación en contingencia

Si intentas anular un documento mientras el MH está caído, la invalidación también queda en contingencia:

{
"success": true,
"invalidated": true,
"contingency": true,
"dte_success": false,
"rejected": false
}

Mismo patrón: la anulación está registrada, se revalidará cuando MH vuelva. No reintentes.

Ver también