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.

Não coloque a chave em aplicativo de celular, página web ou repositório público. A API é feita para chamadas de servidor para servidor.

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çãoRotaEscopo
ListarGET /api/v1/clientsclients:read
DetalharGET /api/v1/clients/{id}clients:read
CriarPOST /api/v1/clientsclients:write
AtualizarPATCH /api/v1/clients/{id}clients:write
RemoverDELETE /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

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

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."] }
}
StatuscodeQuando
400key_in_queryChave enviada na URL
400bad_requestJSON malformado ou parâmetro inválido
401unauthenticatedChave ausente, errada, vencida ou revogada
403https_requiredChamada sem HTTPS
403insufficient_scopeA chave não tem o escopo da operação
403ip_not_allowedIP fora da lista da chave
404not_foundRegistro ou rota inexistente, ou de outra empresa
405method_not_allowedMétodo não suportado na rota
409conflictConflito com o estado atual (ex.: documento já cadastrado)
409idempotency_conflictMesma Idempotency-Key com corpo diferente
413payload_too_largeCorpo acima do limite
415unsupported_media_typeCorpo sem application/json
422validation_failedCampos inválidos (detalhe em errors)
429rate_limitedLimite de requisições atingido
500server_errorFalha 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

RotaChaveRetorno
GET /api/v1/pingnãoNome do sistema, versão da API e hora do servidor
GET /api/v1/mesimId e nome da chave, empresa, escopos e limite
GET /api/v1/openapi.jsonnãoEspecificaçã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

  1. Separe t e v1 do cabeçalho X-Brotas-Signature.
  2. Recuse se a diferença entre t e a hora atual passar de 300 segundos.
  3. Calcule HMAC-SHA256 de "<t>.<corpo bruto>" com o segredo e compare com v1 em 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

Segurança

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.