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:
- Registers the document internally — assigns a correlative, signs it, stores it.
- 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: falseandreception_stamp: ""— the MH stamp does not exist yet.ticket_urlexists,json_urlis 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: trueandcontingency: 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 whetherdte_successflipped totrue. - Option C: schedule a periodic sweep (hourly, daily) that queries
/statuson your documents withcontingency: trueand updates your DB.
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:
| Contingency | Rejection | |
|---|---|---|
| Main flag | contingency: true | rejected: true |
success | true | false |
| Typical cause | MH down, timeout, infrastructure | Invalid data in payload (bad NIT, bad email, nonexistent activity code) |
| Document fiscally issued? | Yes, with correlative | Yes, with correlative, but MH repudiated it |
| Ticket valid? | Yes | Technically no — better not to deliver until corrected |
| What you do? | Nothing, Ocote revalidates alone | Correct 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
- Responses and states — the master flags.
- Retry and idempotency — what to do with
rejected: true(distinct from contingency). - Query status —
/statusendpoint for polling.