Guia da API
Este guia descreve o que é igual em todas as APIs da Brotas System. A referência de cada sistema lista os recursos e campos específicos dele.
Endereço
Cada sistema responde no próprio domínio, no caminho /api/v1. A referência de cada sistema mostra o endereço exato.
https://<domínio do sistema>/api/v1/<recurso>
Só HTTPS é aceito. Uma chamada em HTTP recebe 403 https_required e não é redirecionada, para que a chave não trafegue de novo sem criptografia. Se isso acontecer, considere a chave exposta e gere outra.
Nos sistemas com várias empresas no mesmo endereço, a empresa é definida pela chave. Não existe parâmetro para escolher outra empresa.
Autenticação
Envie a chave no cabeçalho Authorization:
Authorization: Bearer bsk_unifiscal_live_8Hq2kTz0aLw3Rn5c_Xb7...
A chave tem o formato bsk_<sistema>_<live|test>_<id>_<segredo>. O id identifica a chave no painel e nos registros de acesso. O segredo não fica guardado em lugar nenhum: o sistema guarda só uma assinatura dele. Por isso a chave aparece uma única vez, na criação.
- Chave enviada na URL (
?api_key=,?token=) é recusada com400 key_in_query. - Chave inexistente, revogada, vencida ou errada recebe a mesma resposta
401 unauthenticated. - Depois de 10 falhas de um mesmo IP em 5 minutos, o IP fica bloqueado por 15 minutos.
- Cada chave pode ter validade e lista de IPs permitidos. Fora da lista:
403 ip_not_allowed. - Para trocar a chave, crie a nova, atualize sua integração e revogue a antiga no painel.
Escopos
Cada chave recebe escopos no formato <recurso>:read ou <recurso>:write. O escopo *:read libera leitura de todos os recursos. Escrita sempre precisa ser liberada recurso por recurso.
| Operação | Rota | Escopo |
|---|---|---|
| Listar | GET /api/v1/clients | clients:read |
| Detalhar | GET /api/v1/clients/{id} | clients:read |
| Criar | POST /api/v1/clients | clients:write |
| Atualizar | PATCH /api/v1/clients/{id} | clients:write |
| Remover | DELETE /api/v1/clients/{id} | clients:write |
Sem o escopo necessário a resposta é 403 insufficient_scope. Dados sensíveis (CPF completo, saúde, financeiro) têm escopo próprio, descrito na referência do sistema.
Limites
O limite padrão é de 120 requisições por minuto por chave. Toda resposta informa a situação:
RateLimit-Limit: 120
RateLimit-Remaining: 117
RateLimit-Reset: 42
Quem passa do limite recebe 429 rate_limited com Retry-After em segundos. O corpo da requisição pode ter até 1 MB. Envio de arquivo, quando existir, tem rota e limite próprios.
Formato
- JSON em UTF-8. Requisição com corpo precisa de
Content-Type: application/json. - Campos em
snake_case. O mesmo conceito tem o mesmo nome em todos os sistemas:id,name,email,phone,document(CPF ou CNPJ, só números),status,created_at,updated_at. - Data e hora em ISO 8601 com fuso:
2026-10-16T14:30:00-03:00. Só data:2026-10-16. - Valores em dinheiro como texto com duas casas e a moeda ao lado:
"amount": "149.90", "currency": "BRL".
Um registro vem dentro de data:
{ "data": { "id": 42, "name": "Clínica Centro", "created_at": "2026-10-16T14:30:00-03:00" } }
Listas e filtros
GET /api/v1/clients?page=2&per_page=50&sort=-created_at&filter[status]=active&updated_since=2026-10-01T00:00:00-03:00
{
"data": [ { "id": 51, "name": "..." } ],
"meta": { "page": 2, "per_page": 50, "total": 812, "last_page": 17 }
}
per_pagevai até 100. O padrão é 50.sortaceita um campo; o sinal de menos inverte a ordem. Os campos aceitos estão na referência.filter[campo]só funciona nos campos listados na referência. Campo desconhecido gera422.updated_sincetraz o que mudou desde a data informada. É o jeito indicado de sincronizar.
Erros
Todo erro usa o formato da RFC 9457, com Content-Type: application/problem+json. Use o campo code na sua lógica; o texto de title e detail pode mudar.
{
"type": "https://brotassystem.com.br/desenvolvedores/guia.html#erros",
"title": "Dados inválidos",
"status": 422,
"code": "validation_failed",
"detail": "O campo email é obrigatório.",
"request_id": "req_8f3a1c2b9d4e",
"errors": { "email": ["O campo email é obrigatório."] }
}
| Status | code | Quando |
|---|---|---|
| 400 | key_in_query | Chave enviada na URL |
| 400 | bad_request | JSON malformado ou parâmetro inválido |
| 401 | unauthenticated | Chave ausente, errada, vencida ou revogada |
| 403 | https_required | Chamada sem HTTPS |
| 403 | insufficient_scope | A chave não tem o escopo da operação |
| 403 | ip_not_allowed | IP fora da lista da chave |
| 404 | not_found | Registro ou rota inexistente, ou de outra empresa |
| 405 | method_not_allowed | Método não suportado na rota |
| 409 | conflict | Conflito com o estado atual (ex.: documento já cadastrado) |
| 409 | idempotency_conflict | Mesma Idempotency-Key com corpo diferente |
| 413 | payload_too_large | Corpo acima do limite |
| 415 | unsupported_media_type | Corpo sem application/json |
| 422 | validation_failed | Campos inválidos (detalhe em errors) |
| 429 | rate_limited | Limite de requisições atingido |
| 500 | server_error | Falha interna. Informe o request_id ao suporte |
O request_id também volta no cabeçalho X-Request-Id. Você pode enviar o seu próprio (letras, números, hífen e sublinhado, até 64 caracteres) para cruzar com os seus registros.
Idempotência
Rotas POST aceitam o cabeçalho Idempotency-Key. Se a mesma chave voltar em até 24 horas com o mesmo corpo, a API devolve a resposta original sem criar outro registro. Com corpo diferente, a resposta é 409 idempotency_conflict.
curl -X POST https://<sistema>/api/v1/clients \
-H "Authorization: Bearer $CHAVE" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c0a52-pedido-1882" \
-d '{"name":"Clínica Centro","document":"12345678000190"}'
Rotas comuns
| Rota | Chave | Retorno |
|---|---|---|
GET /api/v1/ping | não | Nome do sistema, versão da API e hora do servidor |
GET /api/v1/me | sim | Id e nome da chave, empresa, escopos e limite |
GET /api/v1/openapi.json | não | Especificação OpenAPI 3.1 do sistema |
Webhooks
O administrador cadastra no painel a URL que vai receber os eventos e escolhe quais eventos quer. O sistema gera um segredo, mostrado uma vez.
POST https://sua-empresa.com.br/webhooks/brotas
Content-Type: application/json
X-Brotas-Event: invoice.paid
X-Brotas-Delivery: dlv_01J9Z8K3
X-Brotas-Signature: t=1760635800,v1=5f2c9b...e1a7
{ "id": "evt_01J9Z8K3", "type": "invoice.paid", "created_at": "2026-10-16T14:30:00-03:00", "data": { "id": 991, "amount": "149.90", "currency": "BRL" } }
Como conferir a assinatura
- Separe
tev1do cabeçalhoX-Brotas-Signature. - Recuse se a diferença entre
te a hora atual passar de 300 segundos. - Calcule HMAC-SHA256 de
"<t>.<corpo bruto>"com o segredo e compare comv1em tempo constante.
<?php
$corpo = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_BROTAS_SIGNATURE'] ?? ''), $p);
$ok = isset($p['t'], $p['v1'])
&& abs(time() - (int) $p['t']) <= 300
&& hash_equals(hash_hmac('sha256', $p['t'] . '.' . $corpo, $segredo), $p['v1']);
if (!$ok) { http_response_code(401); exit; }
// Node.js (Express com express.raw)
const crypto = require('crypto');
function assinaturaValida(cabecalho, corpoBruto, segredo) {
const p = Object.fromEntries(String(cabecalho || '').split(',').map(s => s.split('=')));
if (!p.t || !p.v1 || Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
const esperado = crypto.createHmac('sha256', segredo).update(p.t + '.' + corpoBruto).digest('hex');
return esperado.length === p.v1.length && crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(p.v1));
}
Entrega
- Responda com status 2xx em até 10 segundos. Processe o evento depois, em fila, se for demorado.
- Sem 2xx, o sistema tenta de novo em 1 min, 5 min, 30 min, 2 h, 6 h e 24 h.
- O mesmo evento pode chegar mais de uma vez. Use o
iddo evento para ignorar repetidos. - A URL precisa ser HTTPS, na porta 443, e apontar para um endereço público. Redirecionamento não é seguido.
Segurança
- Guarde a chave em variável de ambiente ou cofre de segredos, nunca no código.
- Crie uma chave por integração, com o mínimo de escopos. Assim dá para revogar uma sem parar as outras.
- Restrinja a chave aos IPs dos seus servidores quando eles forem fixos.
- A API não tem CORS liberado: chamadas devem sair do seu servidor, não do navegador.
- Todo acesso fica registrado (chave, rota, status, IP e horário), sem guardar o conteúdo enviado.
- Achou uma falha de segurança? Escreva para o contato da Brotas System antes de divulgar.
Versões
A versão faz parte do caminho (/api/v1). Campo novo, recurso novo e evento novo entram na versão atual, então sua integração deve ignorar campos que não conhece. Mudança que quebra integração existente sai numa versão nova; a anterior continua por 12 meses e passa a responder com os cabeçalhos Deprecation e Sunset.
Brotas System Desenvolvedores