Saltar al contenido principal

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 se creó en Ocote con correlativo asignado y 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.

EscenarioHTTPsuccessdte_successcontingencyrejecteddte_stateobservacionesQué hacer
MH aceptó con sello200truetruefalsefalse""[]Éxito. Guardar y seguir.
MH no respondió200truefalsetruefalse""[]Guardar. Esperar revalidación automática.
MH rechazó por datos200falsefalsefalsetrue"RECHAZADO"[...]Corregir datos, reintentar con mismo external_ref.
Duplicado devuelto200truetruefalsefalse""[]Idem primer caso. is_duplicate: true.
Validación local falló400Corregir payload y reenviar.
API key inválida401Revisar autenticación.
Rate limit excedido429Esperar 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.

EscenarioHTTPsuccessinvalidateddte_successcontingencyrejectedQué hacer
MH aceptó la invalidación200truetruetruefalsefalseConfirmar al usuario.
MH no respondió200truetruefalsetruefalseRegistrar como pendiente.
MH rechazó la invalidación200falsetruefalsefalsetrueRevisar observaciones. Puede requerir NC.
Ya estaba invalidado200truetruetruefalsefalseIdempotencia. Sin cambios.
Documento no invalidable400Consultar 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:

DeprecadoEquivalente actual
amountamount_gross
amount_retamount_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