Skip to main content

Contingency

The Ministry of Finance goes down. Sometimes. Not for long, but it happens. The API Connect is designed so that this does not break your integration and so you do not have to implement queues, retries, or state machines.

What contingency is

When you issue a DTE via the API, Ocote does two things:

  1. Registers the document internally — assigns a correlative, signs it, stores it.
  2. Sends it to MH and awaits response — normally with a reception stamp.

If step 2 fails (MH unresponsive, timeout, MH-side infrastructure error), the document still exists in Ocote with a valid correlative and signature. It just doesn't have the MH stamp yet. That is contingency.

MH regulations contemplate this scenario: a taxpayer can issue documents in contingency while MH is unavailable, and they get validated a-posteriori when MH comes back.

What you see in the response

A document in contingency responds like this:

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

"document_id": "a1b2c3d4-...",
"external_ref": "SALE-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": ""
}

Three things to note:

  • success: true — the operation is complete from the API's point of view. Do not retry.
  • dte_success: false and reception_stamp: "" — the MH stamp does not exist yet.
  • ticket_url exists, json_url is empty — the ticket can be printed in contingency, the signed JSON with stamp only exists after MH validation.

What happens behind the scenes

Once a document falls into contingency, Ocote adds it to an internal automatic revalidation queue. Periodically, a worker reattempts sending to MH until:

  • MH responds with a stamp → the document transitions to dte_success: true and contingency: false.
  • MH responds with a rejection → the document transitions to rejected: true.

This process is invisible to you. You do not need to call any endpoint, you do not need to implement polling. The document will update itself.

Typical timing: revalidation runs every few minutes. If MH comes back fast, the document is stamped in 5–10 minutes. If MH is down for hours, the document stays in contingency until it returns.

What you should do

When you receive a response with contingency: true:

  • Mark the document in your system as "Pending MH" or similar.
  • Show the user that the transaction is complete (it has a correlative, it has a ticket, it is fiscal).
  • Do not retry the issuance POST. The document already exists; retrying would be redundant (it would return is_duplicate: true) but would not speed anything up.

To know when it gets stamped:

  • Option A (recommended): do nothing. If your system does not need to know the stamp in real time, ignore it. The document is fiscally valid from the moment it has a correlative.
  • Option B: query /status/{external_ref} occasionally for that document and check whether dte_success flipped to true.
  • Option C: schedule a periodic sweep (hourly, daily) that queries /status on your documents with contingency: true and updates your DB.
The PDF ticket is valid in contingency

For type 01 (Invoice) documents, ticket_url returns a PDF ready for printing even if the document is in contingency. The ticket includes a note indicating the transmission state. It is legal to hand it to the end customer.

Responsible polling

If you choose Option C, a healthy pattern:

def sync_pending_documents():
"""
Runs hourly via cron. Finds documents in contingency
in your DB and queries current status in 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()
# If still in contingency, do nothing, retry on next cycle

There is no reason to poll more often than hourly for most use cases. Aggressive polling (every minute) consumes rate limit unnecessarily.

Rejection vs contingency — don't confuse them

They are distinct scenarios that require distinct actions:

ContingencyRejection
Main flagcontingency: truerejected: true
successtruefalse
Typical causeMH down, timeout, infrastructureInvalid data in payload (bad NIT, bad email, nonexistent activity code)
Document fiscally issued?Yes, with correlativeYes, with correlative, but MH repudiated it
Ticket valid?YesTechnically no — better not to deliver until corrected
What you do?Nothing, Ocote revalidates aloneCorrect data and retry with same external_ref

See Retry and idempotency for the rejection flow.

What if MH never comes back?

Edge case: MH down for days (it has happened). Ocote keeps retrying. Meanwhile, from the fiscal and operational point of view:

  • All contingency documents are legally valid.
  • The end customer received a ticket.
  • Your accounting counts them as issued for day-sales purposes.
  • When MH returns, all will be validated in batch.

The only truly bad situation would be MH down forever — and if that happens we likely have bigger problems than this API.

Invalidation in contingency

If you try to invalidate a document while MH is down, the invalidation also falls into contingency:

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

Same pattern: the cancellation is registered, will be revalidated when MH comes back. Do not retry.

See also