Saltar al contenido

Errores y Límites de Velocidad

Todos los errores siguen la misma estructura. Usa el code para manejo programático.

Estructura de errores

Json
{
  "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

EstadoCódigoSignificado
200OK—La solicitud fue exitosa.
201Created—Recurso creado. Retornado por endpoints POST.
204No Content—Recurso eliminado. Retornado por endpoints DELETE.
400Bad Requestinvalid_requestEl body / query / params falló la validación Zod. El campo details.issues lista los campos problemáticos.
400Bad Requestinvalid_idempotency_keyEl encabezado Idempotency-Key fue suministrado pero está mal formado (debe ser de 8–200 caracteres).
401UnauthorizedunauthorizedClave API faltante o inválida.
401Unauthorizedkey_expiredLa clave API ha expirado.
403Forbiddeninsufficient_scopeLa clave carece del scope requerido para este método (lectura vs escritura).
404Not Foundnot_foundEl recurso no existe o no pertenece a tu organización.
409Conflictidempotency_conflictLa Idempotency-Key fue reutilizada con un body de solicitud diferente.
422Unprocessableinvalid_transitionLa transición de estado de la versión SOP no es permitida desde el estado actual.
429Too Many Requestsrate_limitedSe excedió el límite de velocidad por clave. Ver encabezado Retry-After.
500Internal Server Errorinternal_errorError 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.

Json
{
  "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í.

Shell
# 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:

Javascript
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");
}

Recuperar sin duplicar trabajo

Conserva estado y X-Request-Id sin registrar la cabecera de autorización. Corrige validación y permisos; no repitas indefinidamente.

Para reintentar el mismo POST, conserva Idempotency-Key y cuerpo. Una operación distinta necesita otra clave. Sigue las reglas de reintento y comprueba el resultado antes de otra modificación.