Proteside Docs

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 (POST y PATCH) necesitan el header Content-Type: application/json, y el cuerpo debe ser un objeto JSON. Sin esto, la respuesta es 400 invalid_request.
  • Los campos desconocidos se rechazan. Un campo que el endpoint no acepta genera 400 invalid_request con el mensaje Unexpected 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. Un id con formato incorrecto genera 400.
  • Las respuestas exitosas incluyen Cache-Control: no-store. Las creaciones responden 201 y 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.

listar-tudo.js
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.

errorHTTPCuándo ocurreQué hacer
invalid_request400Cuerpo mal formado, campo desconocido, valor fuera de rango, cursor o limit inválidosCorrige la solicitud; no la repitas igual
unauthorized401Token ausente, mal formado, revocado o vencidoRevisa el token; crea otro si fue revocado o venció
insufficient_scope403El token no tiene el alcance del endpoint, o intentó actuar en una organización hija sin orgs:manageCrea un token con el alcance indicado en la tabla de alcances
plan_limit403Se alcanzó el límite de tiendas del plan, o el reporte PCI DSS no está incluido en el planMejora el plan o suspende una tienda que ya no uses (la API no reactiva tiendas suspendidas)
not_found404El recurso no existe o no pertenece a la organización en la que estás actuandoRevisa el id y el org_id
conflict409Dominio ya protegido, nombre de política repetido, alerta reabierta por duplicado o Idempotency-Key reutilizada en otra rutaLee message; ajusta los datos o genera otra clave
rate_limited429Más de 120 solicitudes por minuto en la organizaciónEspera el tiempo de Retry-After y vuelve a intentarlo
internal_error500Error inesperado en ProtesideVuelve 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:

HeaderSignificado
X-RateLimit-LimitLímite por minuto (120)
X-RateLimit-RemainingSolicitudes restantes en la ventana (valor aproximado)
X-RateLimit-ResetMomento, 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.

retry.js
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, PATCH y DELETE.
  • 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 mismo 400 durante 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=A y POST /stores?org_id=B con 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.

Próximos pasos

En esta página