Códigos de error
Toda respuesta de error de la API es un ProblemDetail (RFC 9457) con una propiedad extra codigo, estable entre versiones. detail es para ti; codigo es para tu programa. type es urn:finora:pos:<área>:<codigo-en-kebab>.
{
"type": "urn:finora:pos:checkout:sesion-vencida",
"title": "Sesión vencida",
"status": 410,
"detail": "La sesión venció al intentar usarla",
"instance": "/api/v1/checkout/sesiones/CS…/medio",
"codigo": "SESION_VENCIDA"
}
Clave de API (X-API-Key)
| HTTP | codigo | Cuándo | Qué hacer |
|---|---|---|---|
| 401 | CLAVE_INVALIDA | Cabecera ausente, clave inexistente o revocada. | Revisar la clave en Claves API. |
| 403 | CLAVE_SIN_ACCESO_AL_COMERCIO | El merchantCode (o el de la intención / del terminal) no está en el alcance de la clave. | No reintentar. |
| 404 | COMERCIO_NO_ENCONTRADO | merchantCode inexistente. | Corregir. |
Checkout (servidor del comercio)
| HTTP | codigo | Cuándo |
|---|---|---|
| 400 | PETICION_INVALIDA | monto ≤ 0, moneda no ISO, TTL fuera de 60–3600, URL no http(s), JSON ilegible. |
| 403 | TERMINAL_AJENO | El terminal no es del comercio ni de una agencia suya. |
| 404 | SESION_NO_ENCONTRADA | No existe o no es del alcance de la clave (no se confirma cuál). |
| 404 | TERMINAL_NO_ENCONTRADO, ENTREGA_NO_ENCONTRADA | |
| 409 | COBRO_EN_CURSO | Cancelar con autorización en vuelo o terminal que ya aceptó. |
Checkout (token de cliente)
| HTTP | codigo | Cuándo | Qué hace el SDK |
|---|---|---|---|
| 401 | TOKEN_INVALIDO | Token ausente, inválido o de otra sesión; sesión inexistente. | Pantalla de error final; finora-error. |
| 410 | SESION_VENCIDA | Venció al intentar usarla (queda EXPIRADA). | Resultado EXPIRADA. |
| 409 | SESION_CERRADA | Ya tiene veredicto, se canceló o expiró. | Consulta /estado y muestra lo que diga. |
| 409 | COBRO_EN_CURSO | Autorización en vuelo u orden viva/aceptada. | Espera y sondea. |
| 400 | MEDIO_NO_DISPONIBLE | Riel inexistente/inactivo, efectivo, fuera de mín/máx, tarjeta sin terminal. | Vuelve al selector con el aviso. |
| 400 | DATOS_INCOMPLETOS | Faltan o no validan los campos del riel; detail los lista con el code entre comillas simples. | Marca los campos y enfoca el primero. |
| 409 | TERMINAL_OCUPADO | El terminal ya tiene otra orden viva. | Vuelve al selector. |
| 429 | DEMASIADAS_PETICIONES | Más de 60/min sobre la misma sesión (Retry-After: 60). | El sondeo sigue a su ritmo. |
Códigos de la sesión (no son errores HTTP)
codigo | Con estado | Significa |
|---|---|---|
TERMINAL_SIN_RESPUESTA | CREADA | El terminal no tomó la orden a tiempo; la sesión se reabre. |
CANCELADA_EN_TERMINAL | CREADA | El operador canceló en el terminal. |
CANCELADA_POR_CLIENTE / CANCELADA_POR_COMERCIO | CANCELADA | Quién la retiró. |
DE 39 (00, 51…) | APROBADA / RECHAZADA / EN_DUDA | Respuesta del banco. Tabla en Sandbox. |
Cobro empujado y TMS
| HTTP | codigo | Cuándo |
|---|---|---|
| 403 | DESPACHO_NO_PERMITIDO | El comercio no manda sobre ese terminal (regla D9). |
| 404 | TERMINAL_NO_ENCONTRADO, INTENCION_NO_ENCONTRADA, COMANDO_NO_ENCONTRADO | |
| 409 | TERMINAL_OCUPADO | Orden viva en el terminal. Reintentar más tarde o cancelar la anterior. |
| 409 | ESTADO_INVALIDO | Intención pagada, anulada, vencida, ya despachada o con cobro en duda. |
| 409 | TRANSICION_INVALIDA | Aceptar sin entregar, cancelar tras aceptar. |
| 400 | PAYLOAD_INVALIDO | Tipo de comando desconocido o payload incompleto. |
| 403 | COMANDO_NO_PERMITIDO | Comando sensible sin autor; ACTIVAR sobre BLOQUEADO; COBRAR a suspendido. |
Portal (Bearer del usuario)
| HTTP | codigo | Cuándo |
|---|---|---|
| 401 | NO_AUTENTICADO | Sin token, vencido o usuario desactivado (la cadena responde 401 sin cuerpo). |
| 403 | SIN_COMERCIO, FUERA_DE_ALCANCE, PERMISO_REQUERIDO, SIN_DERECHO_DE_DESCARGA, ENLACE_INVALIDO | |
| 404 | NO_ENCONTRADO | También para recursos de otro comercio. |
| 410 | ENLACE_VENCIDO | El enlace de descarga caducó: pide otro. |
| 400 / 409 | PETICION_INVALIDA / CONFLICTO | |
| 503 | ARTEFACTO_NO_DISPONIBLE, REPORTE_NO_DISPONIBLE |
Patrón recomendado
const r = await fetch(url, opciones);
if (!r.ok) {
const p = await r.json().catch(() => ({}));
switch (p.codigo) {
case "TERMINAL_OCUPADO": return reintentarEn(30_000);
case "CLAVE_INVALIDA": return alertarOperaciones("clave revocada");
case "PETICION_INVALIDA": throw new Error(p.detail); // bug tuyo: no reintentar
default: throw new Error(`${r.status} ${p.codigo ?? "SIN_CODIGO"}`);
}
}