---
title: "Rate limits — API v3 de AllSign"
type: explanation
canonical_url: "https://allsign.io/developers/docs/rate-limits"
api_version: "2026-07-11"
agent_rules: [reintento-429]
---

# Límites de tasa (rate limits)

El límite es **por API key**, en una ventana deslizante de 60 segundos, y lo cuenta un contador que comparten todas las réplicas de la API. Cada respuesta autenticada —incluidas las `4xx` de scope o de validación, y hasta la de un cuerpo que no es JSON válido— cuenta, y te dice cuánto te queda y cuánto esperar, así que no tienes que adivinar.

## Para agentes y asistentes de IA

-   **Ante un `429`, espera los segundos de `Retry-After` antes de reintentar; si el `429` no trae `Retry-After`, no lo reintentes.** Con una API key válida, todo `429` de la v3 es problem+json `RATE_LIMITED`, y el de un límite agotado trae `Retry-After` y `retryAfter` en segundos, iguales a `RateLimit-Reset`: reintentar antes solo junta más `429`. El único sin `Retry-After` es el del recordatorio a firmante, que se libera cada 4 horas.

## Headers en cada respuesta

Mandamos **dos formatos juntos** para máxima compatibilidad: el trío clásico y el par del *draft* IETF, más el entorno.

```
HTTP/1.1 200 OK
RateLimit-Limit: 100
RateLimit-Remaining: 98
RateLimit-Reset: 60
RateLimit: "default";r=98;t=60
RateLimit-Policy: "default";q=100;w=60
AllSign-Environment: live
```

| Header | Qué es |
| --- | --- |
| `RateLimit-Limit` | Techo de peticiones en la ventana. |
| `RateLimit-Remaining` | Cuántas te quedan en la ventana actual. |
| `RateLimit-Reset` | **Segundos-delta** (NO epoch): cuánto esperar para tener cupo seguro. Vale lo que dura la ventana de la política (`60`, o `300` en el enrolamiento de tabletas) y a veces un segundo menos: como la ventana es deslizante, es el techo de la espera, no el instante exacto en que se libera la siguiente petición. |
| `RateLimit` | Header *draft* IETF combinado: el nombre de la política, `r` = remaining y `t` = segundos al reinicio. El techo viaja en `RateLimit-Policy`. |
| `RateLimit-Policy` | La política que te está midiendo: `"default";q=100;w=60` = la de tu plan, con cuota de 100 por ventana de 60 s. Las demás políticas están en [Endpoints con límite propio](#endpoints-con-limite-propio). |
| `AllSign-Environment` | `live`, `test` o `dev`. |

El *bucket* es **por API key**, no por IP: varias IPs o varios *workers* con la misma key comparten el cupo, y cada key del tenant tiene el suyo. El contador vive en un almacén compartido por todas las réplicas de la API, así que `RateLimit-Remaining` baja de uno en uno aunque cada petición la atienda una réplica distinta. Si ese almacén deja de responder, cada proceso cuenta por su lado mientras vuelve; por eso la última palabra la tiene el `429` con su `Retry-After`.

### Cuánto te toca

El techo viene de tu **plan de suscripción activo** y aplica igual a las llaves `dev`, `test` y `live` del mismo tenant.

| Plan | Límite |
| --- | --- |
| Sin plan activo | 10 req/min |
| Freemium | 10 req/min |
| Esencial · Colabora · Conecta | 100 req/min |
| Business | 150 req/min |

Si tu suscripción tiene un `api_rpm_override` acordado con nosotros, ese valor gana sobre el del plan.

### Endpoints con límite propio

Algunas rutas tienen además un **tope propio**, porque cada llamada cuesta bastante más que una petición normal, o un **cupo por IP**, porque no llevan API key. Cada uno tiene su nombre de política, y ese nombre es el que ves en `RateLimit-Policy` del `429` cuando se agota.

| Endpoint | Política | Cupo | Por qué |
| --- | --- | --- | --- |
| `POST /v3/constancias` | `constancias` | 20/min por key | Cada llamada es una emisión facturada ante el PSC. Se cuenta aparte y además gasta una petición de tu plan. |
| `POST /v3/constancias/verify` | `constancias-verify` | 60/min por key | Cada llamada corre los cuatro checks criptográficos de la constancia. También gasta una de tu plan. |
| `POST /v3/documents/{id}/send` | — | 250 participantes/min por key | Se cuentan **participantes** invitados, no llamadas: cada uno es un envío. Su `429` se reconoce por `X-RateLimit-Reason: invite-fanout`. |
| `POST /v3/signing-sessions/{id}/init` y `GET /v3/signing-sessions/{id}/policy` | `signing-session` | 10/min por IP, compartidos entre las dos | Son rutas públicas, sin API key: se limitan por IP y no tocan tu cupo. |
| `POST /v3/devices/enroll` | `device-enroll` | 5 cada 5 min por IP | Frena que alguien adivine códigos de enrolamiento de un solo uso. |
| `POST /v3/devassist/chat` | `devassist` | 10/min por IP | Cada turno dispara una llamada a un modelo de lenguaje. No lleva API key, así que no toca tu cupo. |

Una respuesta que pasó trae los `RateLimit-*` de tu plan (`default`), también en las constancias; en un `429` traen la política que se agotó. Si `RateLimit-Policy` nombra algo que no es `default`, subir de plan no lo cambia.

## El 429

Todo `429` de la v3 hecho con una API key válida es `application/problem+json` con `code: "RATE_LIMITED"`. Cuando se agota un límite —el de tu plan, un tope propio o un cupo por IP— trae el header `Retry-After` y **además** el campo `retryAfter` en el cuerpo, ambos en segundos e iguales a `RateLimit-Reset`, junto con los `RateLimit-*` de la política agotada (con `RateLimit-Remaining: 0`). No tienes que parsear headers si prefieres leer el body.

```
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 60
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 60
RateLimit: "default";r=0;t=60
RateLimit-Policy: "default";q=100;w=60
AllSign-Environment: live

{
  "type": "https://allsign.io/developers/docs/errors#RATE_LIMITED",
  "title": "Rate limited",
  "status": 429,
  "detail": "Rate limit exceeded. Limit: 100 requests per minute.",
  "instance": "/v3/documents",
  "code": "RATE_LIMITED",
  "requestId": "req_...",
  "retryAfter": 60
}
```

Uno por IP tiene la misma forma; cambian la política y la ventana. Este lo medimos contra la API:

```
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 300
RateLimit-Limit: 5
RateLimit-Remaining: 0
RateLimit-Reset: 300
RateLimit: "device-enroll";r=0;t=300
RateLimit-Policy: "device-enroll";q=5;w=300

{
  "type": "https://allsign.io/developers/docs/errors#RATE_LIMITED",
  "title": "Rate limited",
  "status": 429,
  "detail": "Rate limit exceeded. Limit: 5 requests per 300 seconds.",
  "instance": "/v3/devices/enroll",
  "code": "RATE_LIMITED",
  "requestId": "req_...",
  "retryAfter": 300
}
```

El texto de `detail` cambia con la ventana («per minute», «per 300 seconds»). No lo parsees: decide con `code` y espera `retryAfter`.

El `429` es **seguro de reintentar** (idempotency-safe): la petición no se procesó y tampoco gastó cupo, así que reintentar no duplica nada. Aun así, para `POST` combina el reintento con tu `Idempotency-Key` como red de seguridad.

### Cómo distinguir cada 429

| Caso | Cómo lo reconoces | Qué hacer |
| --- | --- | --- |
| Límite de tu plan | `RateLimit-Policy: "default"` y `Retry-After`. | Espera `Retry-After`. Si te pasa seguido, baja el ritmo o sube de plan. |
| Tope propio o cupo por IP | `RateLimit-Policy` con otro nombre (`constancias`, `constancias-verify`, `signing-session`, `device-enroll`, `devassist`) y `Retry-After`. | Espera `Retry-After`. Subir de plan no lo cambia. |
| Invitaciones de `send` | `X-RateLimit-Reason: invite-fanout`, `Retry-After` y los miembros `limit`, `requested` y `remaining`. | Espera `Retry-After` o invita a menos firmantes por envío. No se cobró ni se invitó a nadie. |
| Recordatorio a firmante | **Sin** `Retry-After` ni `retryAfter`. | No lo reintentes: es un recordatorio cada 4 horas por firmante. |

En el de invitaciones, los `RateLimit-*` son los de tu plan y no los del tope de invitaciones, así que `RateLimit-Remaining` no viene en `0`. Lo que dice cuánto esperar es `Retry-After`:

```
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 60
X-RateLimit-Limit: 250
X-RateLimit-Remaining: 12
X-RateLimit-Reason: invite-fanout
RateLimit-Limit: 100
RateLimit-Remaining: 87
RateLimit-Reset: 60
RateLimit-Policy: "default";q=100;w=60

{
  "type": "https://allsign.io/developers/docs/errors#RATE_LIMITED",
  "title": "Rate limited",
  "status": 429,
  "detail": "Invitation limit reached: 250 participants per minute. This limit counts participants, not calls, and is independent of your plan's request limit.",
  "instance": "/v3/documents/doc_.../send",
  "code": "RATE_LIMITED",
  "requestId": "req_...",
  "limit": 250,
  "requested": 30,
  "remaining": 12,
  "retryAfter": 60
}
```

El del **recordatorio a firmante** (`POST /v3/documents/{id}/signers/{signerId}/remind`) también trae los `RateLimit-*` de tu plan, pero la hora en que se libera va solo dentro de `detail`: **no trae `Retry-After`** (ni `retryAfter`, ni un miembro `nextAllowedAt`). No lo reintentes con backoff: guarda el `nextAllowedAt` de la respuesta `200` del recordatorio anterior y no vuelvas a recordar antes de esa hora.

```
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
RateLimit-Limit: 100
RateLimit-Remaining: 91
RateLimit-Reset: 60
RateLimit-Policy: "default";q=100;w=60

{
  "type": "https://allsign.io/developers/docs/errors#RATE_LIMITED",
  "title": "Rate limited",
  "status": 429,
  "detail": "Reminder rate-limited. Next allowed at 2026-10-02T21:30:08.632417+00:00.",
  "instance": "/v3/documents/doc_.../signers/sgr_.../remind",
  "code": "RATE_LIMITED",
  "requestId": "req_..."
}
```

Sin una API key válida (sin `Authorization`, con una key revocada o mal copiada) la v3 no te mide por minuto: el `401` no trae `RateLimit-*`. Esas peticiones sí cuentan contra un **tope diario por IP**, y su `429` no es problem+json: es un JSON con `"error": "daily_ip_limit_exceeded"`, el header `X-RateLimit-Reason: daily-ip` y un `Retry-After` que puede ser de horas. No lo esperes: arregla la credencial. Con una key válida ese tope no aplica.

## Reintentar con backoff

El patrón correcto: al ver `429`, **espera lo que diga `Retry-After`** (equivale a `retryAfter` y a `RateLimit-Reset`), súmale un poco de *jitter* y reintenta. No reintentes antes: el cupo se libera conforme salen de la ventana las peticiones viejas, y mientras tanto solo juntas más `429`. El *jitter* importa si varios *workers* comparten la key: sin él, todos vuelven en el mismo segundo y se topan otra vez. Si el `429` no trae `Retry-After`, no reintentes: es el freno de 4 horas del recordatorio a firmante, y un *backoff* de segundos solo te daría más `429`.

```
async function requestWithRetry(doRequest, { maxRetries = 5 } = {}) {
  for (let attempt = 0; ; attempt++) {
    const res = await doRequest();
    if (res.status !== 429 || attempt >= maxRetries) return res;
    const retryAfter = Number(res.headers.get("Retry-After"));
    if (!Number.isFinite(retryAfter) || retryAfter <= 0) return res;
    const jitterMs = Math.random() * 1000;
    await new Promise((r) => setTimeout(r, retryAfter * 1000 + jitterMs));
  }
}
```

```
import random
import time

def request_with_retry(do_request, max_retries=5):
    for attempt in range(max_retries + 1):
        resp = do_request()
        if resp.status_code != 429 or attempt == max_retries:
            return resp
        retry_after = resp.headers.get("Retry-After")
        if retry_after is None:
            return resp
        time.sleep(int(retry_after) + random.random())
```

Para no llegar al `429`: cuando `RateLimit-Remaining` llegue a `0`, espera `RateLimit-Reset` segundos antes de la siguiente petición.

## Diferencia con v2

Si vienes de v2, tres cosas cambiaron en los headers de rate limit:

| v2 | v3 |
| --- | --- |
| `X-RateLimit-Reset` | `RateLimit-Reset` |
| `Reset` = **epoch** (timestamp Unix) | `Reset` = **segundos-delta** (cuántos segundos faltan) |
| Prefijo `X-` en todos | Se cae el prefijo `X-` (RFC 6648) |

Si tu cliente v2 hacía `reset - now()` para calcular la espera, en v3 el valor **ya es** la espera en segundos — úsalo directo.

**En v3 ya no viajan los `X-RateLimit-*` de v2**: una respuesta con tu API key trae solo los `RateLimit-*` sin prefijo, también en las constancias y en sus `429`. Quedan dos rastros, los dos en un `429`: el de invitaciones de `POST /v3/documents/{id}/send`, que trae `X-RateLimit-Limit`, `X-RateLimit-Remaining` y `X-RateLimit-Reason: invite-fanout` (sin `X-RateLimit-Reset`); y el tope diario de una petición sin key válida, que trae `X-RateLimit-Reason: daily-ip` y los `X-DailyRateLimit-*`, con un `X-DailyRateLimit-Reset` que es un **epoch**. Para saber cuánto esperar, lee **solo** `Retry-After` (o `retryAfter`) y los `RateLimit-*`.
