Proteside Docs

Convenções

Formato das requisições, paginação por cursor, erros, limite de requisições, idempotência e auditoria da API v1.

Estas regras valem para todos os endpoints da API v1. Siga-as uma vez no seu cliente HTTP e todas as integrações ficam mais simples.

Requisições e respostas

  • JSON em snake_case. Requisições com corpo (POST e PATCH) precisam do header Content-Type: application/json, e o corpo precisa ser um objeto JSON. Sem isso, a resposta é 400 invalid_request.
  • Campos desconhecidos são rejeitados. Um campo que o endpoint não aceita gera 400 invalid_request com a mensagem Unexpected field "nome_do_campo". Isso evita que um erro de digitação passe despercebido.
  • Datas são strings ISO-8601 em UTC, como 2026-10-06T12:00:00.000Z. Nos filtros (since, period_start, period_end), envie também em UTC.
  • IDs são UUIDs, como 0c1d2e3f-1111-4222-8333-444455556666. Um id fora do formato gera 400.
  • Respostas de sucesso trazem Cache-Control: no-store. Criações respondem 201, as demais operações 200.

Paginação

As listas usam paginação por cursor. Envie limit (de 1 a 100, padrão 50) e, para as páginas seguintes, o cursor recebido:

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"
}

Quando next_cursor vem null, não há mais páginas. Trate o cursor como um valor opaco: não monte nem altere o conteúdo dele. Um cursor inválido gera 400 Invalid cursor.

Os itens vêm do mais novo para o mais antigo (por data de criação; em GET /scripts, pela data em que o script foi visto pela primeira vez). Duas listas não são paginadas e devolvem tudo de uma vez, sem next_cursor: GET /stores/{id}/pages e 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 pendentes = await listAll('/scripts', { status: 'needs_review' });
console.log(`${pendentes.length} scripts aguardando revisão`);

Erros

Todo erro vem no mesmo formato, com um código estável em error e uma mensagem em inglês em message:

{ "error": "invalid_request", "message": "justification must have at least 10 characters" }

Use error na sua lógica; message serve para logs e pode mudar. Alguns erros trazem campos extras, como limit e plan em plan_limit.

errorHTTPQuando aconteceO que fazer
invalid_request400Corpo malformado, campo desconhecido, valor fora da faixa, cursor ou limit inválidosCorrija a requisição; não repita igual
unauthorized401Token ausente, malformado, revogado ou expiradoConfira o token; crie outro se foi revogado ou expirou
insufficient_scope403O token não tem o escopo do endpoint, ou tentou agir numa filha sem orgs:manageCrie um token com o escopo indicado na tabela de escopos
plan_limit403Limite de lojas do plano atingido, ou relatório PCI DSS fora do planoFaça upgrade do plano ou suspenda uma loja que não usa mais (a API não reativa lojas suspensas)
not_found404O recurso não existe ou não pertence à organização em que você está agindoConfira o id e o org_id
conflict409Domínio já protegido, nome de política repetido, alerta reaberto em duplicidade ou Idempotency-Key reutilizada em outra rotaLeia message; ajuste os dados ou gere outra chave
rate_limited429Mais de 120 requisições por minuto na organizaçãoAguarde o tempo de Retry-After e tente de novo
internal_error500Erro inesperado no ProtesideTente de novo com backoff; se persistir, fale com o suporte

Um recurso de outra organização responde 404, não 403: a API não revela se o id existe em outro lugar.

Limite de requisições

O limite é de 120 requisições por minuto por organização dona do token, numa janela deslizante de 60 segundos. Ele é compartilhado por todos os tokens da organização e, para parceiros, também pelas chamadas feitas nas organizações filhas.

Toda resposta autenticada traz:

HeaderSignificado
X-RateLimit-LimitLimite por minuto (120)
X-RateLimit-RemainingRequisições restantes na janela (valor aproximado)
X-RateLimit-ResetMomento, em segundos Unix, em que você pode contar com a janela renovada

Ao passar do limite, a resposta é 429 rate_limited com Retry-After: 60. Chamadas com token inválido (401) não contam; chamadas com token válido mas sem escopo (403) contam.

Trate todo 429 do mesmo jeito

Em caso de instabilidade interna, a API prefere recusar a requisição com 429 a deixar passar tráfego sem controle. Por isso você pode receber 429 mesmo abaixo do limite. Não tente adivinhar a causa: espere o Retry-After e repita.

Repita com espera crescente os erros 429 e 500 e as falhas de rede. Os demais 4xx não melhoram com nova tentativa.

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);
}

Idempotência

Envie o header Idempotency-Key (1 a 255 caracteres) em qualquer POST para poder repeti-lo com segurança, por exemplo depois de um timeout. Se a mesma chave chegar de novo em até 24 horas, a API não executa a operação outra vez: devolve o mesmo status e o mesmo corpo da primeira resposta, com o 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":"minhaloja.com.br"}'
  • O header é ignorado em GET, PATCH e DELETE.
  • A mesma chave usada com outro método ou outro caminho gera 409 conflict.
  • Respostas de erro também são guardadas: se a primeira tentativa recebeu 400, repetir com a mesma chave devolve o mesmo 400 por 24 horas. Corrigiu a requisição? Gere uma chave nova.
  • Downloads de PDF e CSV não são guardados.

Use uma chave nova (UUID) para cada operação

A chave vale para toda a organização dona do token, e a comparação considera só o método e o caminho, sem a query string e sem o corpo. Na prática:

  • Reutilizar a chave com um corpo diferente devolve a resposta da primeira chamada, sem aviso.
  • Parceiros: POST /stores?org_id=A e POST /stores?org_id=B com a mesma chave são tratados como a mesma operação, e a segunda chamada recebe a loja criada na filha A.
  • Duas requisições simultâneas com a mesma chave podem ser executadas as duas. Repita só depois que a primeira terminar ou estourar o timeout.

Auditoria

Toda alteração feita pela API fica registrada na auditoria com o autor api:<prefixo do token>, por exemplo api:ps_live_Ab3dEf6hIj9kLm2nOp5qRs8t. O mesmo rótulo aparece nos dados: em authorized_by de um script autorizado e em resolved_by_label de um alerta resolvido. Por isso vale criar um token por integração: a auditoria mostra qual sistema fez cada mudança.

Próximos passos

Nesta página