Límites de uso y cuotas
Límites de peticiones por segundo de la API de Constaia, cabeceras RateLimit, 429 con Retry-After, límites de ficheros y lotes, y cómo reintentar con backoff.
Constaia limita cuántas peticiones puede hacer cada clave de API por segundo para proteger el servicio y repartirlo de forma justa. Esta página explica qué se aplica hoy, qué está en despliegue y cómo diseñar tu integración para no tropezar con los límites.
Qué se aplica hoy
- Por clave de API: cada clave tiene su propio contador. Dos claves de la misma cuenta no se restan entre sí.
- Ventana fija de 1 segundo.
- Límite por defecto: 10 peticiones por segundo. Algunas cuentas tienen un límite configurado distinto; el valor
real de tu clave viene siempre en
RateLimit-Limit. - Modo test igual que modo real: las claves
ck_test_tienen el mismo límite de peticiones por segundo (sin cuota de créditos). - Cuenta cualquier petición autenticada a
/v1: analizar, clasificar, listar, consultar un análisis, etc.
Cabeceras
Cada respuesta autenticada incluye el estado del límite:
| Cabecera | Significado |
|---|---|
RateLimit-Limit | Peticiones permitidas en la ventana. |
RateLimit-Remaining | Peticiones que te quedan en la ventana actual. |
RateLimit-Reset | Segundos hasta que se reinicia la ventana. |
RateLimit-Policy | Política aplicada, por ejemplo 10;w=1 (10 peticiones, ventana de 1 s). |
Retry-After | Solo en 429: segundos que debes esperar. |
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: req_01J9Z8Q3K4M5N6P7Q8R9S0T1V2
RateLimit-Limit: 10
RateLimit-Remaining: 7
RateLimit-Reset: 1
RateLimit-Policy: 10;w=1Las cabeceras RateLimit-* están expuestas por CORS, aunque tu clave nunca debe usarse desde un navegador.
Los SDK guardan las de la última respuesta: constaia.lastRateLimit en JavaScript, $constaia->lastRateLimit en PHP
y client.last_rate_limit en Python, con limit, remaining, reset, policy y retryAfter (retry_after en
Python).
Respuesta 429
Si superas el límite, recibes 429 con Retry-After:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 1
RateLimit-Limit: 10
RateLimit-Remaining: 0
RateLimit-Reset: 1
RateLimit-Policy: 10;w=1
{
"error": {
"type": "rate_limited",
"code": "rate_limited",
"message": "Demasiadas peticiones. Reintenta en un momento.",
"request_id": "req_01J9Z8Q3K4M5N6P7Q8R9S0T1V3"
}
}Una petición rechazada con 429 no llega a procesarse ni se cobra.
Niveles por plan (en despliegue)
En despliegue
Hoy solo se aplica el límite de peticiones por segundo descrito arriba (10 por defecto). La tabla siguiente es el
esquema por niveles que se está desplegando: las columnas de análisis simultáneos y páginas por minuto todavía no se
aplican. Diseña tu integración para respetar RateLimit-* y Retry-After y no tendrás que cambiar nada cuando entren
en vigor.
| Nivel | Peticiones/s | Análisis simultáneos | Páginas/min | Cuándo |
|---|---|---|---|---|
| Gratis | 2 | 2 | 60 | Sin compras |
| Pago | 10 | 10 | 600 | Con algún pack comprado |
| Empresa | A medida | A medida | A medida | Contrato |
En modo test el límite de peticiones por segundo será el mismo que el de tu nivel, sin cuota de créditos.
Otros límites
| Límite | Valor | Error si lo superas |
|---|---|---|
| Tamaño por fichero | 20 MB | 413 file_too_large |
| Páginas de PDF, análisis síncrono | 30 | 422 too_many_pages |
Páginas de PDF, async: true o lotes | 200 | 422 too_many_pages |
| Documentos por lote | 1–100 | 400 batch_too_large / 400 empty_batch |
Tipos en expect | 1–20 | 422 invalid_parameter |
metadata | 20 claves; clave ≤ 40 caracteres; valor ≤ 500 | 422 invalid_parameter |
Descarga de file_url | 15 s, https, IP pública | 422 file_url_unreachable / 400 invalid_file_url |
Idempotency-Key | 255 caracteres | 400 invalid_idempotency_key |
| Resultados por página al listar | 1–100 (10 por defecto) | 422 invalid_parameter |
| Espera en análisis síncrono | 30 s; después, 202 y resultado por webhook | — |
Cómo reintentar
Reglas:
- Reintenta solo lo reintentable:
429,409 idempotency_in_progress, 5xx (salvo501) y errores de red. Ver errores. - En
429, espera exactamente lo que indicaRetry-After. - En el resto, usa backoff exponencial con jitter: 0,5 s, 1 s, 2 s, 4 s… con un tope, más un componente aleatorio para que tus procesos no reintenten todos a la vez.
- Manda una
Idempotency-Keyen cada POST y reutilízala en los reintentos, para no cobrar dos veces. Ver idempotencia. - Limita el número de intentos (3–5) y registra el
X-Request-Id.
Los SDK de JavaScript, PHP y Python ya lo hacen: reintentan 429, 408, 409 idempotency_in_progress, 5xx (salvo
501) y errores de red, respetan Retry-After (JavaScript y Python esperan como máximo 60 s), usan backoff exponencial
con jitter y reutilizan la Idempotency-Key. Por defecto hacen 2 reintentos (maxRetries en JavaScript,
max_retries en PHP y Python).
Con el SDK solo tienes que ajustar maxRetries:
import { Constaia } from "@constaia/sdk";
export const constaia = new Constaia({ maxRetries: 4, timeout: 90_000 });Si llamas a la API con fetch directamente:
const RETRYABLE = new Set([408, 429, 500, 502, 503, 504]);
export async function postWithRetry(url: string, body: FormData | string, headers: Record<string, string>, maxRetries = 4) {
const idempotencyKey = crypto.randomUUID();
for (let attempt = 0; ; attempt++) {
try {
const res = await fetch(url, {
method: "POST",
body,
headers: { ...headers, Authorization: `Bearer ${process.env.CONSTAIA_API_KEY}`, "Idempotency-Key": idempotencyKey },
});
const payload = await res.json();
const inProgress = res.status === 409 && payload?.error?.code === "idempotency_in_progress";
if ((!RETRYABLE.has(res.status) && !inProgress) || attempt >= maxRetries) {
if (!res.ok) throw Object.assign(new Error(payload.error?.message), { status: res.status, error: payload.error });
return payload;
}
const retryAfter = Number(res.headers.get("retry-after"));
await sleep(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : backoff(attempt));
} catch (err) {
if ((err as { status?: number }).status || attempt >= maxRetries) throw err;
await sleep(backoff(attempt)); // error de red
}
}
}
const backoff = (attempt: number) => Math.min(8000, 500 * 2 ** attempt) * (0.5 + Math.random() / 2);
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));Limita la concurrencia en tu cliente
Reintentar bien no basta si lanzas cientos de análisis a la vez: la mayoría acabarán en 429. Limita cuántas
peticiones tienes en vuelo. Los tres SDK lo hacen en el cliente (4 por defecto); las peticiones que sobran esperan
su turno:
| SDK | Opción |
|---|---|
| JavaScript | new Constaia({ maxConcurrency: 4 }) |
| PHP | new Client(null, ['max_concurrency' => 4]), usado por analyzeMany() para analizar en paralelo |
| Python | Constaia(max_concurrency=4) o AsyncConstaia(max_concurrency=4), con hilos o tareas de asyncio |
El límite es por cliente (por proceso). Con varios workers, el límite por segundo de la clave es compartido: mantén
concurrencia × workers acorde con él.
import { readdir } from "node:fs/promises";
import path from "node:path";
import { Constaia } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";
const constaia = new Constaia({ maxConcurrency: 4, maxRetries: 4 }); // como máximo 4 peticiones en vuelo
const dir = process.argv[2] ?? "./docs";
const files = (await readdir(dir)).filter((f) => /\.(jpe?g|png|webp|heic|pdf)$/i.test(f));
const results = await Promise.all(
files.map(async (name) => {
const analysis = await constaia.analyze(await fromPath(path.join(dir, name)), { expect: "es_dni" });
return { name, status: analysis.verdict?.status };
}),
);
console.table(results);Sin SDK, limita tú la concurrencia: p-limit en JavaScript o un asyncio.Semaphore en Python.
Mucho volumen: usa lotes
Para procesar cientos de documentos, un lote es más eficiente que cientos de llamadas: una
sola petición POST /v1/batches admite hasta 100 documentos, cuenta como una petición para el límite por
segundo, responde 202 al momento y te avisa con el webhook batch.completed. Guía completa:
lotes masivos.
Controlar el gasto
Próximamente: tope de gasto mensual
Habrá un tope opcional de créditos al mes por cuenta, con avisos por email al 80 % y al 100 %. Todavía no está disponible.
Mientras tanto:
- Configura el umbral de saldo bajo en el panel y suscríbete al webhook
credits.low(solo modo real, se envía una vez hasta que recargues). Ver webhooks. - Consulta
GET /v1/balanceantes de trabajos grandes. Lo que puedes gastar escredits_available + free_tier_remaining. - Desarrolla y prueba con claves
ck_test_: nunca cobran. Ver modo test.
Más en créditos y facturación.
Pedir límites más altos
Si necesitas más peticiones por segundo, escribe a hola@constaia.com con tu cuenta, el volumen previsto (documentos al día y picos) y el caso de uso. Los límites se pueden ampliar por cuenta. El plan Empresa incluye límites a medida.
Siguientes pasos
Errores
Formato de error de la API de Constaia, todos los códigos por estado HTTP, clases de error de los SDK de JavaScript y PHP y qué errores conviene reintentar.
Idempotencia
Usa la cabecera Idempotency-Key para reintentar análisis, clasificaciones y lotes sin procesarlos ni cobrarlos dos veces, con ejemplos en varios lenguajes.