Skip to content

Inbound — Hardware Accelero → ionCLASS

O que é este documento — o detalhe do fluxo INBOUND (FLUXO A): tudo que o hardware Accelero empurra para o backend do ionCLASS por webhook — eventos de catraca (entry/exit), chegada de responsável na portaria (guardian_arrival), sincronização de cadastro (roster), veículos e o ping de saúde. Cobre autenticação HMAC, deduplicação, o roteamento por tipo de evento e como cada evento é aplicado ao domínio.

Para a visão geral das duas direções e da abstração de driver, veja Visão geral. O sentido oposto (backend → hardware) está em Outbound.


1. Os quatro endpoints de webhook

Todos são POST, versionados em v1, chaveados pela escola na própria URL (a escola é resolvida a partir do {school} da rota) e autenticados por HMAC-SHA256 — nunca por token de sessão. São rotas públicas (sem autenticação de sessão): a autorização é a assinatura do corpo bruto.

EndpointO que fazResposta
POST api/v1/integrations/accelero/{school}/eventsRecebe o evento (HMAC), grava o registro do evento recebido e aplica ao domínio numa fila202 { accepted, event_id }
POST api/v1/integrations/accelero/{school}/rosterRecebe o cadastro (HMAC) e faz upsert de alunos/responsáveis200 { students[], skipped[] }
POST api/v1/integrations/accelero/{school}/vehiclesRecebe a sincronização de um veículo (HMAC) e faz upsert/remoção202 { accepted }
POST api/v1/integrations/accelero/{school}/pingResponde o "pong" do teste de saúde (sem efeito)200 { pong: true }

2. Autenticação HMAC (FLUXO A)

A assinatura é validada na fronteira da requisição, antes de qualquer processamento — assinatura ausente/errada devolve 403 e nada é gravado.

  • O segredo é a chave de eventos (entrada) da escola (armazenada criptografada). Ela é separada da chave de comandos (saída) que assina o FLUXO B — cada direção tem sua própria chave.
  • Cálculo:
esperado = HMAC-SHA256(corpo_bruto_da_requisição, chave de eventos (entrada))
confere  = comparação em tempo constante entre `esperado` e o header `X-Accelero-Signature`
  • Qualquer um destes → 403 (e um registro de skip): escola inexistente na rota, escola sem integração ativa, chave de entrada nula, header ausente, ou assinatura divergente.
  • O roster reusa a mesma verificação; o ping inbound (o "pong") é assinado com a mesma chave de entrada, provando que o canal plugin → backend está saudável.

⚠️ O HMAC é calculado sobre a chave de eventos (entrada) (o valor em claro, descriptografado). Ambos os lados — plugin e backend — precisam do mesmo segredo em claro; a criptografia é só o repouso no banco.


3. Webhook de eventos: events

3.1 Recepção — ack rápido

O backend extrai event_type, event_id, student_external_id, occurred_at e o payload bruto inteiro, e:

  1. Deduplica por (sistema, event_id). Há um índice único sobre esse par (janela ≥ 24h por FR-105, mas o índice a garante permanentemente): se já existe um registro com o mesmo event_id, ele é retornado sem reprocessar — reenvio do mesmo evento nunca duplica efeito.
  2. Resolve o aluno por matrícula = student_external_id, escopado à escola. Essa é a chave de correlação de identidade (matrícula == student_external_id == pesCodigo/education->id no Accelero). Se student_external_id vier mas nenhum aluno casar, grava o registro assim mesmo com o aluno vazio e loga um skip (o processamento depois marca como falha quando o tipo exige aluno).
  3. Grava o registro do evento recebido com status "recebido" e o payload bruto completo.
  4. Enfileira o processamento (assíncrono) e devolve 202 imediatamente — o webhook responde rápido; o trabalho pesado acontece na fila.

Por que persistir antes de processar? O registro é a prova durável do que o hardware disse, independente de o processamento dar certo. Ele guarda o payload bruto, o estado do processamento, o carimbo de conclusão e o eventual erro, e é a mesma linha que o dedupe consulta.

3.2 Aplicação — roteia por tipo

O processamento recarrega o evento pelo id; se já tiver sido concluído, retorna (idempotente). Envolve o tratamento num try/catch: sucesso → marca como processado; falha → marca como falho e re-lança o erro, para a fila reprocessar erros genuínos (ver Runbook de tempo real).

roteia por event_type:
  'exit'             → trata a saída física
  'entry'            → reconcilia a entrada
  'guardian_arrival' → registra a chegada do responsável   (documento no payload bruto)
  default            → tipo inválido → falha → retry na fila

entry/exit são o contrato original; guardian_arrival é aditivo — um plugin antigo que só mande entry/exit continua funcionando (validação in:entry,exit,guardian_arrival).

3.3 exit — saída física

A saída física sempre notifica os responsáveis, e então resolve o domínio de retirada (ver o funil dual-source em Ciclo de retirada):

  • Passe solo autorizado — se o aluno tem um passe de retirada solo aprovado para hoje, é saída autorizada, não retirada: transiciona para off_premises, consome o passe, encerra o sinal de chegada ativo, e não emite confirmação de retirada.
  • Já retirado — o operador venceu a corrida; grava uma confirmação secundária de auditoria (origem: hardware) e não muda estado.
  • Caso geral — a catraca é autoritativa: funila pela resolução da corrida de origem com método hardware. Resolve para picked_up quando havia um sinal de chegada (responsável tinha sinalizado) ou para off_premises (saída não sinalizada) quando não havia.

O ator de um registro manual (admin, com a catraca offline) viaja no payload bruto, em recorded_by_user_id; uma leitura real de hardware não tem usuário por trás (sem ator). Sem aluno mapeado, loga erro e não muda estado — o processamento marca o evento como falho.

3.4 entry — reconciliação de entrada

O aluno cruzou um leitor de entrada → fica on_premises. Cobre tanto a primeira chegada (off_premises → on_premises) quanto o retorno no mesmo dia após uma retirada (picked_up → on_premises) — o Accelero classifica entry/exit pelo canal físico do leitor, nunca pelo estado do aluno. O único evento ignorado é o de um aluno on_premises (releitura duplicada). Não envolve retirada e não aparece em card público — só o monitor privado reflete a presença.

3.5 guardian_arrival — chegada de responsável

O gatilho que cobre quem chegou na portaria sem avisar pelo app: o responsável é reconhecido pela facial no leitor da própria escola. O documento (CPF) viaja no payload bruto (guardian_external_id), não em student_external_id — um CPF de responsável não é matrícula de aluno, e confundir os dois era exatamente o bug que fazia esses eventos sumirem.

  • Resolve o responsável por CPF vinculado à escola; exige que ele tenha conta de app — sem conta, não há usuário a quem vincular o sinal (loga skip).
  • Cria um sinal de chegada com origem facial, sem geofence (a leitura na catraca já é a prova de presença) e sem filtro de presença: varre todos os filhos com vínculo aprovado para awaiting_pickup — inclusive os que o sistema achava off_premises/picked_up —, porque a facial prova que o adulto está na portaria e o estado de retirada pode estar desatualizado.
  • Janela de 5 min de idempotência, igual à do app: se já há sinal ativo recente do mesmo responsável (inclusive um sinal do app), ele é retornado intacto — o sinal do app vence o desempate por carregar intenção/detalhe que a catraca não tem.
  • Emite o evento de sinal de chegada (+ versão pública) e notifica a portaria/monitor.

4. Fluxo webhook → fila → aplicação (ASCII)

 Accelero (catraca/facial)
        │  POST /api/v1/integrations/accelero/{school}/events
        │  header X-Accelero-Signature = HMAC-SHA256(body, chave de entrada)

 Verificação HMAC na fronteira da requisição
        │  HMAC inválido/ausente ─────────────► 403 (nada gravado)
        │  HMAC ok

 Recepção do evento
        │  dedupe (sistema, event_id) ────► duplicado: retorna o registro, sem efeito
        │  resolve o aluno por matrícula = student_external_id
        │  grava o registro do evento recebido (payload bruto, status=recebido)
        │  enfileira o processamento assíncrono

 202 { accepted, event_id }         (webhook responde rápido)

  ── fila ───────────────────────────────────────────────────────
 Processamento assíncrono
        │  roteia por event_type
        │   exit             → trata a saída ────────► resolução da corrida / off_premises / conf. secundária
        │   entry            → reconcilia a entrada ─► on_premises + broadcast
        │   guardian_arrival → registra a chegada ───► sinal de chegada (facial) no board
        │  sucesso → marca como processado
        │  falha   → marca como falho + re-lança → retry na fila

 broadcast em school.{id}.monitor (StudentStateChanged) + notificações

5. Webhook de cadastro: roster

O hardware é a fonte do cadastro físico: o plugin empurra o roster (em lotes) e o backend faz upsert reusando a mesma lógica do import CSV, idempotente por escola + matrícula para alunos e por CPF para responsáveis.

  • Devolve o mapa matrícula → identificador (UUID) do aluno deste lote, para o plugin popular seu identity map local (necessário para resolver o identificador interno do aluno em chamadas outbound).
  • Aluno sem matrícula ou sem foto é pulado (coletado em skipped[]), não derruba o lote.
  • Vínculos que estouram o limite de responsáveis da escola são coletados em skipped[] (chave aditiva — plugin antigo ignora). Matrículas referenciadas por um responsável mas trazidas num lote anterior são hidratadas do banco antes do vínculo (links cross-lote não se perdem).
  • Se o responsável trouxer vehicles[], eles são sincronizados uma vez por CPF a partir do roster.

6. Webhook de veículos: vehicles

Mudança de um veículo (upsert/delete) vinda do Accelero. O backend resolve o responsável por CPF (precisa estar vinculado à escola) e faz upsert/remoção do veículo do responsável, auditado; 202 rápido, idempotente por responsável + placa. Responsável desconhecido/não vinculado é no-op (não erro) — placa de alguém que a escola não conhece não é nada a espelhar.

Este fluxo nunca enfileira chamada outbound — é justamente o que impede o loop do sync bidirecional: só as ações de app/admin empurram para fora; escritas inbound param aqui. O delete promove o veículo mais antigo a favorito se removeu o favorito (espelha a remoção de veículo do lado do app).


7. Webhook de saúde: ping — o "pong"

O endpoint de ping só devolve { pong: true }sem efeito colateral. Ele é o lado inbound do ping-pong de saúde: quando o admin clica "Testar integração" no painel, o backend faz o ping outbound (FLUXO B, ver Outbound); o plugin, ao responder, chama de volta este endpoint assinado com a chave de eventos (entrada), provando que o canal plugin → backend também está de pé. É a única rota inbound sem persistência nem idempotência.


8. O registro do evento recebido

CampoPapel
identificador do sistema + event_idpar do índice único (dedupe)
event_typeentry / exit / guardian_arrival
student_external_idmatrícula crua vinda do hardware (= matrícula do aluno)
aluno resolvidoresolvido da matrícula (vazio até casar)
hora do evento (occurred_at)≈ agora numa leitura real; anterior num registro manual
payload brutocorpo bruto inteiro — inclui guardian_external_id e recorded_by_user_id
estado do processamentociclo de vida: recebido → processado / falho, com carimbo e erro

Ver também

Documentação do ionCLASS