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
  1. Los topes
  2. Cómo se mide el consumo
  3. La forma de un error
  4. Qué reintentar
  5. Idempotencia: reintentar sin duplicar

Los topes

PlanPor minuto y por llavePor día y por llaveLlaves
Gratis—0 (sin API)0
Pro1202.0003
Max12020.00010
Corporativo120Sin topeSin tope
Prueba y sandbox1202001
  • 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 429 con error_code: "rate_limited" y la cabecera Retry-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é.

HTTPerror_codeQué pasóQué hacer
401unauthenticatedNo mandaste credencial.Agrega Authorization: Bearer sk_live_....
401api_key_invalidLlave desconocida, revocada o vencida.No reintentes: cambia la llave.
403plan_feature_lockedEl plan de la cuenta no incluye la API (o MCP).No reintentes: hace falta otro plan.
403api_key_route_not_exposedEsa ruta no se puede usar con una llave.Hazlo en la app.
403api_key_scope_insufficientLlave de lectura en una ruta que escribe.Usa una llave de escritura.
404not_foundRuta inexistente, o un recurso que no existe en tu cuenta.Revisa la ruta y el id.
405method_not_allowedMétodo equivocado para esa ruta.Revisa GET/POST/PUT/PATCH/DELETE.
409p. ej. session_not_trainable, po_idempotency_key_reusedEl estado no permite la operación, o una llave de idempotencia se reusó con otro contenido.No reintentes igual: lee error_params.
422validation_error u otro códigoEl cuerpo o los parámetros no son válidos.Corrige la petición.
429rate_limitedTope por minuto o por día.Espera Retry-After segundos.
500internal_errorFallo inesperado del servidor.Reintenta con espera creciente.
503server_busyEl servidor está saturado.Reintenta con espera creciente.

Qué reintentar

  • Sí: 429 (después de Retry-After), 500, 503 y los cortes de red.
  • No: los 4xx restantes. 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:

RutaCómoAl repetir
POST /inventory/log-poCabecera 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/poCabecera Idempotency-KeyIgual que log-po.
POST /inventory-sync/profiles, .../refresh, .../profiles/{id}/run, .../profiles/applyCabecera Idempotency-Key (hasta 128 caracteres)Devuelve el primer resultado; no crea ni corre dos veces.
POST /inventory-sync/profiles/{id}/ingestCampo batch_id del lotereplayed: true y la misma corrida. Mismo batch_id con otras filas: 409 inventory_sync_batch_id_reused.
POST /purchase-orders/{id}/erp-referenceEs idempotente por (orden, referencia)changed: false. Otra referencia para el mismo documento: 409 erp_reference_conflict.
POST /inventory/po/importCada orden del archivo lleva su propia llaveImportar 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