Errores y Límites de Velocidad
Todos los errores siguen la misma estructura. Usa el code para manejo programático.
Estructura de errores
{
"error": "Invalid API key",
"code": "unauthorized",
"requestId": "f243456d-1f26-4716-833f-c4804aa38c2a"
}El campo error es legible por humanos. El campo code es estable y legible por máquina — activa tu lógica sobre este.
Códigos de estado HTTP
| Estado | Código | Significado |
|---|---|---|
| 200OK | — | La solicitud fue exitosa. |
| 201Created | — | Recurso creado. Retornado por endpoints POST. |
| 204No Content | — | Recurso eliminado. Retornado por endpoints DELETE. |
| 400Bad Request | invalid_request | El body / query / params falló la validación Zod. El campo details.issues lista los campos problemáticos. |
| 400Bad Request | invalid_idempotency_key | El encabezado Idempotency-Key fue suministrado pero está mal formado (debe ser de 8–200 caracteres). |
| 401Unauthorized | unauthorized | Clave API faltante o inválida. |
| 401Unauthorized | key_expired | La clave API ha expirado. |
| 403Forbidden | insufficient_scope | La clave carece del scope requerido para este método (lectura vs escritura). |
| 404Not Found | not_found | El recurso no existe o no pertenece a tu organización. |
| 409Conflict | idempotency_conflict | La Idempotency-Key fue reutilizada con un body de solicitud diferente. |
| 422Unprocessable | invalid_transition | La transición de estado de la versión SOP no es permitida desde el estado actual. |
| 429Too Many Requests | rate_limited | Se excedió el límite de velocidad por clave. Ver encabezado Retry-After. |
| 500Internal Server Error | internal_error | Error en el lado del servidor. Cita el requestId cuando contactes con soporte. |
Errores de validación
Una entrada inválida obtiene un 400 con code: "invalid_request". El campo details.issues es el árbol de errores Zod aplanado — muéstralo directamente al usuario o regístralo para depuración.
{
"error": "Invalid body",
"code": "invalid_request",
"details": {
"issues": {
"fieldErrors": {
"title": ["Required"],
"nodeType": ["Required"]
},
"formErrors": []
}
},
"requestId": "..."
}Idempotencia
Los POSTs aceptan un encabezado Idempotency-Key (8–200 caracteres). El servidor almacena la respuesta en caché durante 24 horas, indexada por apiKeyId + Idempotency-Key. Las repeticiones de la misma clave + body retornan el estado y JSON almacenados en caché, más un encabezado Idempotent-Replay: true. Reutilizar una clave con un body diferente retorna 409 idempotency_conflict. PATCH y DELETE son inherentemente idempotentes en el mismo id, así que el encabezado es opcional allí.
# Primera llamada — crea el recurso y almacena la respuesta en caché durante 24h.
curl -X POST https://sopflow.io/api/v1/nodes \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9c3b7d6e-4a4d-4e2c-9b6f-cb1e2d3f4567" \
-d '{"parentId":null,"title":"Engineering","nodeType":"department"}'
# Misma clave + mismo body → respuesta 201 idéntica, con encabezado Idempotent-Replay: true.
# Misma clave + DIFERENTE body → 409 idempotency_conflict.Usa un UUID fresco por operación lógica (no reutilices uno entre lotes). La caché es local del proceso — un reinicio del servidor la borra, pero esa ventana es lo suficientemente pequeña como para que las tormentas de reintentos no sean una preocupación en nuestra escala.
IDs de solicitud
Cada respuesta lleva un encabezado X-Request-Id, y cada envolvente de error lo repite como requestId. Cita ese ID cuando reportes problemas para que podamos hacer grep en nuestro registro de la solicitud exacta — mucho más rápido que describir el síntoma. También puedes enviar el tuyo propio (un UUID, tu ID de trace, etc.) en el camino y lo propagaremos de vuelta.
Límites de velocidad
Los límites son por clave API: 600 req/min en general y 120 req/min adicionales en escrituras (POST / PATCH / DELETE). Cuando se agota cualquiera de los dos buckets, la respuesta es 429 rate_limited con un encabezado Retry-After (segundos a esperar).
Los buckets son por clave, no por organización, así que un script descontrolado en una clave no desnutrirá otra. Los encabezados de paginación X-Page-Limit, X-Has-More se retornan en cada endpoint de lista.
Reintento con backoff exponencial
Reintenta solo en respuestas 429 y 5xx. Usa backoff exponencial con jitter para evitar manadas de trueno:
async function fetchWithRetry(url, options, maxRetries = 3) {
let delay = 500; // ms
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, options);
if (res.status === 429) {
// Rate limited — espera y reintenta con backoff exponencial
const retryAfter = res.headers.get("Retry-After");
const wait = retryAfter ? Number(retryAfter) * 1000 : delay;
await new Promise((r) => setTimeout(r, wait));
delay *= 2;
continue;
}
return res; // cualquier otro estado — retorna al llamador
}
throw new Error("Max retries exceeded");
}