Respuestas y estados
Esta página describe la semántica completa del response de los endpoints de emisión y consulta. Léela junto con Retry e idempotencia y Contingencia — los tres documentos forman el modelo mental del API.
Estructura general del response
Toda respuesta del API (HTTP 200) incluye estos campos maestros en el body:
{
"success": true,
"dte_success": true,
"contingency": false,
"rejected": false,
"posted": true,
"is_duplicate": false,
"document_id": "a1b2c3d4-1234-5678-9abc-def012345678",
"external_ref": "VENTA-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/..."
}
Los campos se agrupan en tres bloques:
- Flags de decisión — booleanos que indican qué pasó. Los usas para ramificar tu lógica.
- Identificadores — lo que necesitas para guardar el documento en tu sistema.
- Diagnóstico — información textual relevante solo cuando algo no salió perfecto.
Los seis flags que importan
success
La operación quedó registrada en Ocote. Esto incluye los casos felices (sello MH recibido) y los casos de contingencia (MH no respondió, pero el documento existe y se revalidará).
success: true significa que puedes marcar el documento como emitido desde tu lado. No significa necesariamente que el MH ya selló — para eso está dte_success.
dte_success
El MH devolvió sello de recepción. Esta es la única condición que garantiza validez fiscal inmediata.
dte_success: true implica success: true siempre. Lo contrario no.
contingency
El MH no respondió (timeout, caído, rechazó con error de infraestructura). El documento sí se creó en Ocote con correlativo asignado y sí se revalidará automáticamente cuando el MH vuelva.
Cuando contingency: true, también success: true y dte_success: false. Ver Contingencia.
rejected
El MH recibió el documento y lo rechazó por datos del receptor (email mal formado, NIT inválido, actividad económica inexistente, etc.). El documento existe en Ocote con correlativo asignado pero no tiene sello MH.
Cuando rejected: true, también success: false, dte_success: false, y observaciones contiene la lista de errores del MH. Debes corregir los datos y reintentar con el mismo external_ref.
posted
El documento tiene correlativo fiscal (control_number) asignado. Una vez posted: true, el control_number nunca cambia, aunque el documento se rechace y se reintente.
is_duplicate
El external_ref que enviaste ya existía y el documento anterior estaba en estado exitoso. El API te devuelve el documento ya emitido sin intentar reemitir.
is_duplicate: true es una señal de seguridad: tu retry llegó, todo está bien, el reception_stamp es el original. Ver Retry e idempotencia.
Tabla maestra: endpoints de emisión
Aplica a POST /invoice, POST /credit-memo y POST /suex.
| Escenario | HTTP | success | dte_success | contingency | rejected | dte_state | observaciones | Qué hacer |
|---|---|---|---|---|---|---|---|---|
| MH aceptó con sello | 200 | true | true | false | false | "" | [] | Éxito. Guardar y seguir. |
| MH no respondió | 200 | true | false | true | false | "" | [] | Guardar. Esperar revalidación automática. |
| MH rechazó por datos | 200 | false | false | false | true | "RECHAZADO" | [...] | Corregir datos, reintentar con mismo external_ref. |
| Duplicado devuelto | 200 | true | true | false | false | "" | [] | Idem primer caso. is_duplicate: true. |
| Validación local falló | 400 | — | — | — | — | — | — | Corregir payload y reenviar. |
| API key inválida | 401 | — | — | — | — | — | — | Revisar autenticación. |
| Rate limit excedido | 429 | — | — | — | — | — | — | Esperar Retry-After segundos. |
Tabla maestra: endpoint de invalidación
Aplica a POST /invalidate/{id_or_ref} y POST /invalidate/suex/{id_or_ref}. Incluye un flag extra invalidated.
| Escenario | HTTP | success | invalidated | dte_success | contingency | rejected | Qué hacer |
|---|---|---|---|---|---|---|---|
| MH aceptó la invalidación | 200 | true | true | true | false | false | Confirmar al usuario. |
| MH no respondió | 200 | true | true | false | true | false | Registrar como pendiente. |
| MH rechazó la invalidación | 200 | false | true | false | false | true | Revisar observaciones. Puede requerir NC. |
| Ya estaba invalidado | 200 | true | true | true | false | false | Idempotencia. Sin cambios. |
| Documento no invalidable | 400 | — | — | — | — | — | Consultar estado; puede estar fuera de ventana. |
invalidated: true significa que el API intentó invalidar. Si además dte_success: true, la invalidación quedó con sello MH. Si rejected: true, el MH rechazó la invalidación y el documento original sigue vigente.
Campo observaciones
Lista de strings con los mensajes de error del MH. Solo poblada cuando rejected: true. Ejemplo real:
{
"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"
]
}
Regla: presenta observaciones directamente al usuario final (es texto legible en español). El codigo_msg y dte_state son para tus logs.
Ver Códigos MH para la referencia de códigos frecuentes.
Campos deprecados (aún soportados)
Los endpoints de consulta (GET /status/{id}) mantienen dos campos antiguos para no romper integraciones existentes:
| Deprecado | Equivalente actual |
|---|---|
amount | amount_gross |
amount_ret | amount_retention |
Ambos siguen devolviendo el mismo valor. Si empiezas una integración nueva, usa los nombres actuales. Si ya consumes los viejos, no necesitas cambiar nada.
Decision tree para tu código
El patrón canónico de manejo de response:
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"]))
# reintentar
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) # sin sello todavia
else:
save_as_issued(data) # con sello
elif data["rejected"]:
show_to_user(data["observaciones"])
# El usuario corrige y reintentas con mismo external_ref
else:
alert_ops("Estado inesperado", data)
Tres ramas de success, una de rejected, una de sanity check. Ese es todo el árbol.
Respuesta del endpoint de consulta
GET /status/{id_or_ref} devuelve los mismos flags, más los montos:
{
"success": true,
"dte_success": true,
"contingency": false,
"rejected": false,
"posted": true,
"document_id": "a1b2c3d4-...",
"external_ref": "VENTA-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 es null cuando el documento no genera ticket (CCF, SUEX, NC — solo la factura tipo 01 genera ticket térmico).
invalidated y credit_memo_id son informativos: si el documento fue anulado directamente, invalidated: true. Si fue anulado vía NC (caso CCF fuera de ventana de 3 meses), credit_memo_id apunta al UUID de la nota de crédito.
Ver también
- Retry e idempotencia — cómo reintentar cuando
rejected: true. - Contingencia — qué pasa cuando
contingency: true. - Convenciones — semántica general de HTTP y campos opcionales.