Límites, errores y reintentos
Los topes por minuto y por día de cada plan, cómo se mide el consumo, la forma de los errores, qué reintentar y cómo evitar duplicados con llaves de idempotencia.
En esta página
Los topes
| Plan | Por minuto y por llave | Por día y por llave | Llaves |
|---|---|---|---|
| Gratis | — | 0 (sin API) | 0 |
| Pro | 120 | 2.000 | 3 |
| Max | 120 | 20.000 | 10 |
| Corporativo | 120 | Sin tope | Sin tope |
| Prueba y sandbox | 120 | 200 | 1 |
- Los topes son por llave: dos integraciones con llaves distintas no se estorban.
- La ventana por minuto son los últimos 60 segundos; la diaria, las últimas 24 horas.
- Al pasarte recibes
429conerror_code: "rate_limited"y la cabeceraRetry-After: 60. Espera esos segundos antes de reintentar. - Las 120 por minuto alcanzan para el trabajo real de una integración (un envío nocturno y su consulta de estado). Si las rozas, casi siempre hay un bucle: filtra en el servidor (
?signal=PEDIR_YA) en vez de pedir todo y descartar.
Cómo se mide el consumo
Cada llamada que pasa todos los controles suma uno al día (UTC) de esa llave. Las rechazadas — llave inválida, ruta no expuesta, llave de lectura escribiendo, plan sin API, tope superado — no cuentan. Un administrador ve el consumo del mes por día y por llave en la pantalla API; las llaves revocadas siguen apareciendo con lo que consumieron.
La forma de un error
Los errores no traen data. Traen detail (texto en inglés, para personas, que puede cambiar de redacción) y un error_code estable con sus error_params. Decide siempre por error_code o por el código HTTP.
HTTP/1.1 422 Unprocessable Entity
{"detail":[{"type":"less_than_equal","loc":["query","limit"],
"msg":"Input should be less than or equal to 500","input":"9999","ctx":{"le":500}}],
"error_code":"validation_error"}En los errores de validación (422 validation_error), detail es una lista: cada elemento dice en loc qué campo falló y en msg por qué.
| HTTP | error_code | Qué pasó | Qué hacer |
|---|---|---|---|
| 401 | unauthenticated | No mandaste credencial. | Agrega Authorization: Bearer sk_live_.... |
| 401 | api_key_invalid | Llave desconocida, revocada o vencida. | No reintentes: cambia la llave. |
| 403 | plan_feature_locked | El plan de la cuenta no incluye la API (o MCP). | No reintentes: hace falta otro plan. |
| 403 | api_key_route_not_exposed | Esa ruta no se puede usar con una llave. | Hazlo en la app. |
| 403 | api_key_scope_insufficient | Llave de lectura en una ruta que escribe. | Usa una llave de escritura. |
| 404 | not_found | Ruta inexistente, o un recurso que no existe en tu cuenta. | Revisa la ruta y el id. |
| 405 | method_not_allowed | Método equivocado para esa ruta. | Revisa GET/POST/PUT/PATCH/DELETE. |
| 409 | p. ej. session_not_trainable, po_idempotency_key_reused | El estado no permite la operación, o una llave de idempotencia se reusó con otro contenido. | No reintentes igual: lee error_params. |
| 422 | validation_error u otro código | El cuerpo o los parámetros no son válidos. | Corrige la petición. |
| 429 | rate_limited | Tope por minuto o por día. | Espera Retry-After segundos. |
| 500 | internal_error | Fallo inesperado del servidor. | Reintenta con espera creciente. |
| 503 | server_busy | El servidor está saturado. | Reintenta con espera creciente. |
Qué reintentar
- Sí:
429(después deRetry-After),500,503y los cortes de red. - No: los
4xxrestantes. No van a cambiar esperando; reintentarlos solo gasta tu cuota. - Una lectura se puede reintentar siempre.
- Una escritura solo se reintenta sin riesgo si la ruta es idempotente (abajo). Si no lo es y se cortó la conexión, consulta primero si se aplicó.
import time, uuid, requests
def call(method, url, key, *, json=None, idempotency_key=None, tries=5):
headers = {"Authorization": f"Bearer {key}"}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key # same value on every retry
for attempt in range(tries):
r = requests.request(method, url, headers=headers, json=json, timeout=60)
if r.status_code == 429: # rate limited
time.sleep(int(r.headers.get("Retry-After", "60")))
continue
if r.status_code >= 500: # internal_error, server_busy
time.sleep(2 ** attempt)
continue
return r
r.raise_for_status()
# One order = one idempotency key, generated ONCE and reused on every retry.
key_for_this_order = str(uuid.uuid4())Idempotencia: reintentar sin duplicar
Una orden de compra registrada dos veces es stock que «viene en camino» dos veces y una compra duplicada. Por eso las escrituras que crean algo aceptan una llave que las hace seguras de repetir:
| Ruta | Cómo | Al repetir |
|---|---|---|
POST /inventory/log-po | Cabecera Idempotency-Key (un UUID por orden) | 200 con replayed: true y la orden original. Misma llave con otras líneas: 409 po_idempotency_key_reused. |
POST /inventory/po | Cabecera Idempotency-Key | Igual que log-po. |
POST /inventory-sync/profiles, .../refresh, .../profiles/{id}/run, .../profiles/apply | Cabecera Idempotency-Key (hasta 128 caracteres) | Devuelve el primer resultado; no crea ni corre dos veces. |
POST /inventory-sync/profiles/{id}/ingest | Campo batch_id del lote | replayed: true y la misma corrida. Mismo batch_id con otras filas: 409 inventory_sync_batch_id_reused. |
POST /purchase-orders/{id}/erp-reference | Es idempotente por (orden, referencia) | changed: false. Otra referencia para el mismo documento: 409 erp_reference_conflict. |
POST /inventory/po/import | Cada orden del archivo lleva su propia llave | Importar el mismo archivo otra vez no duplica órdenes. |
curl -X POST "https://app.stockai.es/api/v1/inventory/log-po?session_id=$SESSION" \
-H "Authorization: Bearer $STOCKAI_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2a0e-1b7d-4c55-9a51-3f0f4e0b8d21" \
-d '{"items":[{"sku":"SKU-001","recommended_qty":120,"final_qty":100,
"status":"modified","unit_cost":2.5,"supplier":"Distribuidora Andina"}]}'
# First call: 201, "replayed": false -> the order was created
# Same call again (same key, same lines): 200, "replayed": true -> the FIRST order, nothing new
# Same key, different lines: 409 po_idempotency_key_reused