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 (POSTePATCH) precisam do headerContent-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_requestcom a mensagemUnexpected 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. Umidfora do formato gera400. - Respostas de sucesso trazem
Cache-Control: no-store. Criações respondem201, as demais operações200.
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.
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.
error | HTTP | Quando acontece | O que fazer |
|---|---|---|---|
invalid_request | 400 | Corpo malformado, campo desconhecido, valor fora da faixa, cursor ou limit inválidos | Corrija a requisição; não repita igual |
unauthorized | 401 | Token ausente, malformado, revogado ou expirado | Confira o token; crie outro se foi revogado ou expirou |
insufficient_scope | 403 | O token não tem o escopo do endpoint, ou tentou agir numa filha sem orgs:manage | Crie um token com o escopo indicado na tabela de escopos |
plan_limit | 403 | Limite de lojas do plano atingido, ou relatório PCI DSS fora do plano | Faça upgrade do plano ou suspenda uma loja que não usa mais (a API não reativa lojas suspensas) |
not_found | 404 | O recurso não existe ou não pertence à organização em que você está agindo | Confira o id e o org_id |
conflict | 409 | Domínio já protegido, nome de política repetido, alerta reaberto em duplicidade ou Idempotency-Key reutilizada em outra rota | Leia message; ajuste os dados ou gere outra chave |
rate_limited | 429 | Mais de 120 requisições por minuto na organização | Aguarde o tempo de Retry-After e tente de novo |
internal_error | 500 | Erro inesperado no Proteside | Tente 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:
| Header | Significado |
|---|---|
X-RateLimit-Limit | Limite por minuto (120) |
X-RateLimit-Remaining | Requisições restantes na janela (valor aproximado) |
X-RateLimit-Reset | Momento, 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.
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,PATCHeDELETE. - 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 mesmo400por 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=AePOST /stores?org_id=Bcom 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.