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.
| Scenario | HTTP | success | dte_success | contingency | rejected | dte_state | observaciones | What to do |
|---|---|---|---|---|---|---|---|---|
| MH accepted with stamp | 200 | true | true | false | false | "" | [] | Success. Store and move on. |
| MH did not respond | 200 | true | false | true | false | "" | [] | Store. Wait for automatic revalidation. |
| MH rejected by data | 200 | false | false | false | true | "RECHAZADO" | [...] | Correct data, retry with same external_ref. |
| Duplicate returned | 200 | true | true | false | false | "" | [] | Same as first case. is_duplicate: true. |
| Local validation failed | 400 | — | — | — | — | — | — | Fix payload and resend. |
| Invalid API key | 401 | — | — | — | — | — | — | Check authentication. |
| Rate limit exceeded | 429 | — | — | — | — | — | — | Wait 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.
| Scenario | HTTP | success | invalidated | dte_success | contingency | rejected | What to do |
|---|---|---|---|---|---|---|---|
| MH accepted the invalidation | 200 | true | true | true | false | false | Confirm to user. |
| MH did not respond | 200 | true | true | false | true | false | Mark as pending. |
| MH rejected the invalidation | 200 | false | true | false | false | true | Check observaciones. May need NC instead of cancellation. |
| Already invalidated | 200 | true | true | true | false | false | Idempotent. No changes. |
| Document not invalidatable | 400 | — | — | — | — | — | Check 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:
| Deprecated | Current equivalent |
|---|---|
amount | amount_gross |
amount_ret | amount_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
- Retry and idempotency — how to retry when
rejected: true. - Contingency — what happens when
contingency: true. - Conventions — general HTTP semantics and optional fields.