Errores
Todos los errores 4xx/5xx siguen este shape:
{
"error": "human readable message in Spanish neutral",
"code": "MACHINE_READABLE_CODE",
"details": { "...": "..." },
"request_id": "01HZQ7K8..."
}
Campos:
| Campo |
Tipo |
Notas |
error |
string |
Mensaje en español neutro, listo para mostrar al usuario |
code |
string |
UPPER_SNAKE_CASE estable, machine-readable |
details |
object |
Opcional. Información extra (campo inválido, retry_after, etc.) |
request_id |
string |
ULID/UUID para grep en logs |
Status codes
| Status |
Cuándo |
Ejemplo code |
| 400 |
Body malformado o validation pre-request |
INVALID_BODY, VALIDATION_ERROR |
| 401 |
Sin auth o token inválido/expirado |
INVALID_CREDENTIALS, INVALID_REFRESH_TOKEN |
| 403 |
Auth válida pero falta scope/role |
FORBIDDEN, MISSING_SCOPE |
| 404 |
Recurso no existe (o cross-tenant) |
NOT_FOUND |
| 409 |
Conflicto idempotencia o estado |
IDEMPOTENCY_KEY_CONFLICT, ALREADY_EXISTS |
| 422 |
Validation post-parse (negocio) |
VALIDATION_ERROR |
| 429 |
Rate limit |
RATE_LIMIT_EXCEEDED |
| 500 |
Error interno |
INTERNAL_ERROR |
| 501 |
Endpoint not implemented yet |
NOT_IMPLEMENTED |
| 503 |
Dependencia caída (DB/Redis) |
DEPENDENCY_UNAVAILABLE |
Códigos comunes
Auth (/auth/*)
code |
Status |
Descripción |
INVALID_CREDENTIALS |
401 |
Login fallido (email o password) |
ACCOUNT_LOCKED |
401 |
5 intentos fallidos → bloqueo 15 min |
ACCOUNT_INACTIVE |
401 |
users.is_active = false |
INVALID_REFRESH_TOKEN |
401 |
Cookie/body no matchea o expiró |
ARCO (/user/*)
code |
Status |
Descripción |
DELETE_REQUEST_PENDING |
409 |
Ya hay un request en curso (window 30d) |
EXPORT_TOO_LARGE |
422 |
Data > 100MB → procesar offline |
MCP (mcp.wiservet.com)
code |
Status |
Descripción |
INVALID_API_KEY |
401 |
Prefix incorrecto, revocada, o expirada |
MISSING_SCOPE |
403 |
El tool requiere un scope no asignado |
RATE_LIMIT_EXCEEDED |
429 |
Daily o burst limit |
SQL_BLOCKED |
400 |
Regex pre-execute detectó DML/DDL/escalación |
STATEMENT_TIMEOUT |
500 |
Query > 5s |
Manejo recomendado en cliente
async function call<T>(req: Request): Promise<T> {
const res = await fetch(req)
if (res.ok) return res.json()
const err = await res.json().catch(() => ({}))
if (res.status === 401 && err.code === 'INVALID_REFRESH_TOKEN') {
redirectToLogin()
} else if (res.status === 429) {
const retryAfter = parseInt(res.headers.get('Retry-After') ?? '60', 10)
await sleep(retryAfter * 1000)
return call(req) // reintento simple, ojo con loops
}
throw new ApiError(err.code ?? 'UNKNOWN', err.error ?? 'API error', err.request_id)
}
Reportar errores
Si encontrás un código no documentado o un comportamiento inconsistente: incluí request_id + body completo en soporte@wiservet.com.