Convenciones
Formato de las solicitudes, paginación por cursor, errores, límite de solicitudes, idempotencia y auditoría de la API v1.
Estas reglas aplican a todos los endpoints de la API v1. Síguelas una vez en tu cliente HTTP y todas las integraciones serán más simples.
Solicitudes y respuestas
- JSON en
snake_case. Las solicitudes con cuerpo (POSTyPATCH) necesitan el headerContent-Type: application/json, y el cuerpo debe ser un objeto JSON. Sin esto, la respuesta es400 invalid_request. - Los campos desconocidos se rechazan. Un campo que el endpoint no acepta genera
400 invalid_requestcon el mensajeUnexpected field "nome_do_campo". Así se evita que un error de tipeo pase desapercibido. - Las fechas son strings ISO-8601 en UTC, como
2026-10-06T12:00:00.000Z. En los filtros (since,period_start,period_end), envíalas también en UTC. - Los IDs son UUIDs, como
0c1d2e3f-1111-4222-8333-444455556666. Unidcon formato incorrecto genera400. - Las respuestas exitosas incluyen
Cache-Control: no-store. Las creaciones responden201y las demás operaciones,200.
Paginación
Las listas usan paginación por cursor. Envía limit (de 1 a 100, predeterminado 50) y, para las páginas siguientes, el
cursor recibido:
curl -s "https://app.proteside.com/api/v1/alerts?status=open&limit=100" \
-H "Authorization: Bearer $PROTESIDE_TOKEN"{
"data": [{ "id": "e1f2a3b4-8888-4999-8000-111122223333", "type": "PIX_TAMPERED", "...": "..." }],
"next_cursor": "WyIyMDI2LTEwLTA2VDA5OjQwOjAwLjAwMFoiLCJlMWYyYTNiNC04ODg4LTQ5OTktODAwMC0xMTExMjIyMjMzMzMiXQ"
}Cuando next_cursor viene en null, no hay más páginas. Trata el cursor como un valor opaco: no armes ni modifiques su
contenido. Un cursor inválido genera 400 Invalid cursor.
Los elementos vienen del más nuevo al más antiguo (por fecha de creación; en GET /scripts, por la fecha en que se vio
el script por primera vez). Dos listas no están paginadas y devuelven todo de una vez, sin next_cursor:
GET /stores/{id}/pages y GET /reports/pci/schedule.
const BASE = 'https://app.proteside.com/api/v1';
async function listAll(path, params = {}) {
const items = [];
let cursor = null;
do {
const qs = new URLSearchParams({ ...params, limit: '100' });
if (cursor) qs.set('cursor', cursor);
const res = await fetch(`${BASE}${path}?${qs}`, {
headers: { Authorization: `Bearer ${process.env.PROTESIDE_TOKEN}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const page = await res.json();
items.push(...page.data);
cursor = page.next_cursor;
} while (cursor);
return items;
}
const pendientes = await listAll('/scripts', { status: 'needs_review' });
console.log(`${pendientes.length} scripts esperando revisión`);Errores
Todos los errores vienen con el mismo formato, con un código estable en error y un mensaje en inglés en message:
{ "error": "invalid_request", "message": "justification must have at least 10 characters" }Usa error en tu lógica; message sirve para los logs y puede cambiar. Algunos errores traen campos extra, como
limit y plan en plan_limit.
error | HTTP | Cuándo ocurre | Qué hacer |
|---|---|---|---|
invalid_request | 400 | Cuerpo mal formado, campo desconocido, valor fuera de rango, cursor o limit inválidos | Corrige la solicitud; no la repitas igual |
unauthorized | 401 | Token ausente, mal formado, revocado o vencido | Revisa el token; crea otro si fue revocado o venció |
insufficient_scope | 403 | El token no tiene el alcance del endpoint, o intentó actuar en una organización hija sin orgs:manage | Crea un token con el alcance indicado en la tabla de alcances |
plan_limit | 403 | Se alcanzó el límite de tiendas del plan, o el reporte PCI DSS no está incluido en el plan | Mejora el plan o suspende una tienda que ya no uses (la API no reactiva tiendas suspendidas) |
not_found | 404 | El recurso no existe o no pertenece a la organización en la que estás actuando | Revisa el id y el org_id |
conflict | 409 | Dominio ya protegido, nombre de política repetido, alerta reabierta por duplicado o Idempotency-Key reutilizada en otra ruta | Lee message; ajusta los datos o genera otra clave |
rate_limited | 429 | Más de 120 solicitudes por minuto en la organización | Espera el tiempo de Retry-After y vuelve a intentarlo |
internal_error | 500 | Error inesperado en Proteside | Vuelve a intentarlo con backoff; si persiste, comunícate con soporte |
Un recurso de otra organización responde 404, no 403: la API no revela si el id existe en otro lugar.
Límite de solicitudes
El límite es de 120 solicitudes por minuto por organización dueña del token, en una ventana deslizante de 60 segundos. Lo comparten todos los tokens de la organización y, para los partners, también las llamadas hechas en las organizaciones hijas.
Toda respuesta autenticada incluye:
| Header | Significado |
|---|---|
X-RateLimit-Limit | Límite por minuto (120) |
X-RateLimit-Remaining | Solicitudes restantes en la ventana (valor aproximado) |
X-RateLimit-Reset | Momento, en segundos Unix, en que puedes contar con la ventana renovada |
Al superar el límite, la respuesta es 429 rate_limited con Retry-After: 60. Las llamadas con un token inválido
(401) no cuentan; las llamadas con un token válido pero sin alcance (403) sí cuentan.
Trata todos los 429 de la misma forma
Ante una inestabilidad interna, la API prefiere rechazar la solicitud con 429 antes que dejar pasar tráfico sin
control. Por eso puedes recibir 429 incluso por debajo del límite. No intentes adivinar la causa: espera el
Retry-After y repite.
Repite con espera creciente los errores 429 y 500 y las fallas de red. Los demás 4xx no mejoran con un nuevo
intento.
async function protesideFetch(url, init = {}, attempt = 0) {
const res = await fetch(url, init);
const retryable = res.status === 429 || res.status >= 500;
if (!retryable || attempt >= 4) return res;
const retryAfter = Number(res.headers.get('Retry-After'));
const waitMs = retryAfter > 0
? retryAfter * 1000
: Math.min(30_000, 1000 * 2 ** attempt) + Math.random() * 500;
await new Promise((r) => setTimeout(r, waitMs));
return protesideFetch(url, init, attempt + 1);
}Idempotencia
Envía el header Idempotency-Key (1 a 255 caracteres) en cualquier POST para poder repetirlo de forma segura, por
ejemplo después de un timeout. Si la misma clave vuelve a llegar dentro de las 24 horas, la API no ejecuta la
operación otra vez: devuelve el mismo estado y el mismo cuerpo de la primera respuesta, con el header
Idempotency-Replayed: true.
curl -s -X POST "https://app.proteside.com/api/v1/stores" \
-H "Authorization: Bearer $PROTESIDE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3b8f2c1e-6d4a-4f0b-9c2e-7a1d5e8f9b30" \
-d '{"domain":"mitienda.com"}'- El header se ignora en
GET,PATCHyDELETE. - La misma clave usada con otro método u otra ruta genera
409 conflict. - Las respuestas de error también se guardan: si el primer intento recibió
400, repetir con la misma clave devuelve el mismo400durante 24 horas. ¿Corregiste la solicitud? Genera una clave nueva. - Las descargas de PDF y CSV no se guardan.
Usa una clave nueva (UUID) para cada operación
La clave vale para toda la organización dueña del token, y la comparación considera solo el método y la ruta, sin la query string y sin el cuerpo. En la práctica:
- Reutilizar la clave con un cuerpo distinto devuelve la respuesta de la primera llamada, sin aviso.
- Partners:
POST /stores?org_id=AyPOST /stores?org_id=Bcon la misma clave se tratan como la misma operación, y la segunda llamada recibe la tienda creada en la organización hija A. - Dos solicitudes simultáneas con la misma clave pueden ejecutarse ambas. Repite solo después de que la primera termine o agote el timeout.
Auditoría
Todo cambio hecho por la API queda registrado en la auditoría con el autor api:<prefijo del token>, por ejemplo
api:ps_live_Ab3dEf6hIj9kLm2nOp5qRs8t. La misma etiqueta aparece en los datos: en authorized_by de un script
autorizado y en resolved_by_label de una alerta resuelta. Por eso conviene crear un token por integración: la
auditoría muestra qué sistema hizo cada cambio.