Skip to main content

Responses and states

This page describes the full semantics of the response from the issuance and query endpoints. Read it alongside Retry and idempotency and Contingency — the three documents together form the mental model of the API.

Overall response structure

Every API response (HTTP 200) includes these master fields in the body:

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

"document_id": "a1b2c3d4-1234-5678-9abc-def012345678",
"external_ref": "SALE-2026-000042",
"control_number": "DTE-01-M001P001-000000000000123",
"generation_code": "A1B2C3D4-1234-5678-9ABC-DEF012345678",
"reception_stamp": "20260421154532...",

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

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

Fields are grouped into three blocks:

  • Decision flags — booleans that tell you what happened. You use them to branch your logic.
  • Identifiers — what you need to persist the document on your side.
  • Diagnostics — textual information, only relevant when something didn't go perfectly.

The six flags that matter

success

The operation was registered in Ocote. This includes the happy path (MH stamp received) and contingency cases (MH did not respond, but the document exists and will be revalidated).

success: true means you can mark the document as issued on your side. It does not necessarily mean MH has stamped it yet — that's what dte_success is for.

dte_success

MH returned a reception stamp. This is the only condition that guarantees immediate fiscal validity.

dte_success: true always implies success: true. The reverse does not hold.

contingency

MH did not respond (timeout, down, infrastructure error). The document was created in Ocote with an assigned correlative and will be revalidated automatically when MH comes back.

When contingency: true, also success: true and dte_success: false. See Contingency.

rejected

MH received the document and rejected it due to recipient data (malformed email, invalid NIT, non-existent economic activity, etc.). The document exists in Ocote with an assigned correlative but does not have an MH stamp.

When rejected: true, also success: false, dte_success: false, and observaciones contains the list of MH errors. You must correct the data and retry with the same external_ref.

posted

The document has a fiscal correlative (control_number) assigned. Once posted: true, the control_number never changes, even if the document is rejected and retried.

is_duplicate

The external_ref you sent already existed and the previous document was in a successful state. The API returns the already-issued document without attempting to reissue.

is_duplicate: true is a safety signal: your retry arrived, everything is fine, the reception_stamp is the original one. See Retry and idempotency.

Master table: issuance endpoints

Applies to POST /invoice, POST /credit-memo, and POST /suex.

ScenarioHTTPsuccessdte_successcontingencyrejecteddte_stateobservacionesWhat to do
MH accepted with stamp200truetruefalsefalse""[]Success. Store and move on.
MH did not respond200truefalsetruefalse""[]Store. Wait for automatic revalidation.
MH rejected by data200falsefalsefalsetrue"RECHAZADO"[...]Correct data, retry with same external_ref.
Duplicate returned200truetruefalsefalse""[]Same as first case. is_duplicate: true.
Local validation failed400Fix payload and resend.
Invalid API key401Check authentication.
Rate limit exceeded429Wait Retry-After seconds.

Master table: invalidation endpoint

Applies to POST /invalidate/{id_or_ref} and POST /invalidate/suex/{id_or_ref}. Includes the extra invalidated flag.

ScenarioHTTPsuccessinvalidateddte_successcontingencyrejectedWhat to do
MH accepted the invalidation200truetruetruefalsefalseConfirm to user.
MH did not respond200truetruefalsetruefalseMark as pending.
MH rejected the invalidation200falsetruefalsefalsetrueCheck observaciones. May need NC instead of cancellation.
Already invalidated200truetruetruefalsefalseIdempotent. No changes.
Document not invalidatable400Check status; may be outside the MH window.

invalidated: true means the API attempted to invalidate. If also dte_success: true, the invalidation has an MH stamp. If rejected: true, MH rejected the invalidation and the original document is still valid.

The observaciones field

List of strings with MH error messages. Only populated when rejected: true. Real example:

{
"rejected": true,
"dte_state": "RECHAZADO",
"codigo_msg": "096",
"observaciones": [
"Campo #/receptor/correo no cumple el formato requerido",
"El valor del campo #/resumen/totalNoGravado debe ser tipo numero"
]
}

Rule: show observaciones directly to the end user (it's readable text in Spanish, which is the language MH operates in). codigo_msg and dte_state are for your logs.

See MH codes for common code reference.

Deprecated fields (still supported)

Query endpoints (GET /status/{id}) retain two legacy fields to avoid breaking existing integrations:

DeprecatedCurrent equivalent
amountamount_gross
amount_retamount_retention

Both still return the same value. For new integrations, use the current names. If you already consume the old ones, no change is required.

Decision tree for your code

The canonical response-handling pattern:

r = requests.post("https://ocote.io/api/connect/invoice", headers=h, json=payload)

if r.status_code == 401:
raise AuthError()
if r.status_code == 429:
time.sleep(int(r.headers["Retry-After"]))
# retry
if r.status_code == 400:
raise PayloadError(r.json()["detail"])
if r.status_code != 200:
raise UpstreamError()

data = r.json()

if data["success"]:
if data["contingency"]:
save_as_pending_mh(data) # no stamp yet
else:
save_as_issued(data) # stamped
elif data["rejected"]:
show_to_user(data["observaciones"])
# User corrects, you retry with same external_ref
else:
alert_ops("Unexpected state", data)

Three branches of success, one of rejected, one sanity check. That's the whole tree.

Query endpoint response

GET /status/{id_or_ref} returns the same flags plus the amounts block:

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

"document_id": "a1b2c3d4-...",
"external_ref": "SALE-2026-000042",
"control_number": "DTE-03-M001P001-000000000000189",
"generation_code": "A1B2C3D4-...",
"reception_stamp": "20260421154532...",

"doc_type": "03",
"posted_date_time": "2026-04-21T10:42:15-06:00",
"customer_name": "ENLACES EL SALVADOR, S.A. DE C.V.",

"amount_taxable": 1000.00,
"amount_vat": 130.00,
"amount_gross": 1130.00,
"amount_retention": 11.30,
"amount_total": 1118.70,

"amount": 1130.00,
"amount_ret": 11.30,

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

"invalidated": false,
"credit_memo_id": null
}

ticket_url is null when the document does not generate a ticket (CCF, SUEX, NC — only invoice type 01 generates a thermal ticket).

invalidated and credit_memo_id are informational: if the document was directly invalidated, invalidated: true. If it was cancelled via credit note (case of CCF outside the direct-invalidation window), credit_memo_id points to the UUID of the credit note.

See also