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:
- Registra el documento internamente — le asigna correlativo, lo firma, lo guarda.
- 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: falseyreception_stamp: ""— el sello MH aún no existe.ticket_urlsí existe,json_urles 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: trueycontingency: 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 sidte_successpasó atrue. - Opción C: agenda un barrido periódico (cada hora, cada día) que consulta
/statusde tus documentos concontingency: truey actualiza tu BD.
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:
| Contingencia | Rechazo | |
|---|---|---|
| Flag principal | contingency: true | rejected: true |
success | true | false |
| Causa típica | MH caído, timeout, infraestructura | Datos inválidos en el payload (NIT mal, email mal, actividad inexistente) |
| ¿Documento fiscalmente emitido? | Sí, con correlativo | Sí, con correlativo, pero el MH lo repudió |
| ¿Ticket válido? | Sí | Técnicamente no — conviene no entregarlo hasta corregir |
| ¿Qué haces? | Nada, Ocote revalida solo | Corregir 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
- Respuestas y estados — los flags maestros.
- Retry e idempotencia — qué hacer con
rejected: true(distinto a contingencia). - Consultar estado — endpoint
/statuspara polling.