Skip to content

Catálogo de eventos ​

Treze tipos, agrupados em quatro famílias. Todos chegam no mesmo envelope — o que muda é type e data.

TipoQuando dispara
response.createdUma resposta foi finalizada
invite.sentA mensagem do convite saiu
invite.deliveredO provedor confirmou a entrega
invite.openedO convidado abriu o link
invite.startedO convidado começou a preencher
invite.respondedO convite virou resposta
invite.failedBounce, supressão ou falha de envio
invite.revokedO convite foi invalidado
form.goal_reachedUm formulário atingiu a meta configurada
enrollment.droppedUma matrícula passou para evadida
treatment.openedUma resposta virou caso a tratar
treatment.overdueA etapa de um caso passou do prazo
treatment.closedO caso foi encerrado
pingTeste manual pelo painel

response.created ​

Uma resposta foi finalizada — pelo link público ou por convite.

Rascunho não gera evento

Se o formulário permite "salvar e continuar", o progresso parcial não dispara nada. Só a finalização vira response.created.

json
{
  "id": "8f4c1e2a-…",
  "type": "response.created",
  "version": 1,
  "occurredAt": "2026-07-28T13:45:12.001Z",
  "companyId": "c0ffee00-…",
  "data": {
    "formId": "f1a2b3c4-…",
    "formName": "Pesquisa de satisfação — Julho",
    "formSlug": "satisfacao-julho",
    "responseId": "r1a2b3c4-…",
    "inviteId": null,
    "completedAt": "2026-07-28T13:45:11.980Z",
    "respondent": {
      "name": "Ana Prado",
      "email": "ana@exemplo.com",
      "phone": null,
      "externalId": "MAT-4471"
    },
    "answers": {
      "q_nps": 9,
      "q_comentario": "Atendimento muito rápido",
      "q_turno": "opt_manha"
    },
    "dims": {
      "course": "ADM",
      "shift": "Noite"
    },
    "orgUnitId": "u1a2b3c4-…",
    "orgPath": "/r1a2b3c4-…/u1a2b3c4-…/",
    "orgSource": "invite"
  }
}
CampoTipoObservação
formId, formName, formSlugstringIdentificação do formulário
responseIdstringUse em GET /v1/responses/{id} se precisar do registro completo
inviteIdstring | nullnull = veio pelo link público, sem convite
completedAtstring | nullISO 8601 UTC
respondentobjeto | nullVer abaixo
answersobjetoChave = id da pergunta
dimsobjetoCampos de recorte carimbados; chave = chave do campo. Ver abaixo
orgUnitIdstring | nullUnidade em que a resposta aconteceu
orgPathstring | nullCaminho da unidade (/<id>/<id>/) — agrupa por subárvore com um startsWith
orgSourcestring | nullProcedência: invite, link, class, answer ou none

dims traz os campos de recorte que o formulário declara (curso, turma, unidade…), já resolvidos: você recorta por eles sem reler o convite nem interpretar respostas. Os valores são sempre string, na forma canônica em que foram gravados (número como "3", data como "2026-08-29"), então a comparação é a mesma em qualquer lugar. Formulário que não declara campo nenhum manda {}. As mesmas chaves valem como filtro na listagem de respostas: GET /v1/forms/{id}/responses?dim.course=<valor>.

Campo de cadastro guarda o id

Curso, docente e unidade são entidades cadastradas, e o valor que chega é o id delas — não o nome. É o que faz renomear um curso corrigir todo o histórico de uma vez, em vez de deixar o nome antigo congelado nas respostas antigas.

Ao enviar (criar convite com dims), você pode mandar o id ou o código que já usa no seu sistema: nós resolvemos. Ao receber, vem sempre o id.

respondent tem name, email, phone e externalId, todos opcionais. No fluxo por convite ele é montado no servidor a partir do convite — o respondente não consegue forjá-lo. No link público, vem dos parâmetros de prefill, quando houver.

answers segue o formato descrito em Interpretando as respostas. Pergunta oculta por condição ou em seção pulada simplesmente não aparece.


Eventos de convite ​

Os oito eventos invite.* compartilham o mesmo formato de data:

json
{
  "id": "3b7d9e1f-…",
  "type": "invite.delivered",
  "version": 1,
  "occurredAt": "2026-07-28T10:02:41.520Z",
  "companyId": "c0ffee00-…",
  "data": {
    "formId": "f1a2b3c4-…",
    "formName": "Pesquisa de satisfação — Julho",
    "inviteId": "i1a2b3c4-…",
    "status": "delivered",
    "channel": "email",
    "batchId": "b1a2c3d4-…",
    "respondent": {
      "name": "Ana Prado",
      "email": "ana@exemplo.com",
      "phone": null,
      "externalId": "MAT-4471"
    },
    "failReason": null
  }
}
CampoTipoObservação
formId, formNamestringformName pode vir null
inviteIdstringCorrelacione com o convite que você criou
statusstringEstágio para o qual o convite acabou de mudar
channelstring | nullemail ou whatsapp
batchIdstring | nullLote do envio; null se criado individualmente
respondentobjeto | nullMesmos campos, incluindo o seu externalId
failReasonstring | nullSó em invite.failed

Use o externalId

Se você informou externalId ao criar o convite, ele volta em todo evento. É a forma mais direta de amarrar o evento ao registro do seu lado sem manter uma tabela de-para.

invite.sent ​

A mensagem saiu para o convidado — entregue ao provedor de e-mail ou WhatsApp.

Ainda não significa que chegou na caixa de entrada. É invite.delivered que diz isso.

invite.delivered ​

O provedor confirmou a entrega ao destinatário.

No e-mail depende do webhook de status do provedor; no WhatsApp, da confirmação da plataforma. Nem todo envio produz um delivered — a ausência dele não implica falha.

invite.opened ​

O convidado abriu o link pessoal.

Vem da resolução do próprio link (/i/:token), não de pixel de rastreamento. Por isso é bem mais confiável que "abriu o e-mail": não é afetado por bloqueio de imagens nem por pré-carregamento de caixa de entrada.

invite.started ​

Primeira interação real com o formulário — o convidado respondeu alguma coisa.

É o sinal de "engajou mas talvez não termine". Combinado com a ausência de invite.responded, identifica quem abandonou no meio.

invite.responded ​

O convite virou resposta.

Chega junto com um response.created do mesmo preenchimento. Os dois se complementam:

  • invite.responded fecha o ciclo do convite (quem, de qual lote, por qual canal)
  • response.created traz o conteúdo (as respostas)

Se você só quer o conteúdo, assine apenas response.created — o inviteId está lá.

invite.failed ​

Bounce, endereço na lista de supressão, ou falha no envio. O motivo vem em data.failReason:

failReasonSignificadoO que fazer
bouncedO endereço rejeitou permanentementeCorrija o cadastro — reenviar não resolve
suppressedO destinatário está na lista de supressão (bounce anterior ou opt-out)Respeite; não force
send_failedFalha técnica no envioPode tentar de novo mais tarde

Bounce e supressão são definitivos

Reenviar para um endereço que deu bounce prejudica a reputação de entrega de todos os envios da empresa. Trate bounced e suppressed como pedido para parar.

invite.revoked ​

O convite foi invalidado, no painel ou por POST /v1/invites/{id}/revoke. O link para de funcionar imediatamente.


form.goal_reached ​

Um formulário atingiu a meta de respostas configurada numa regra de alerta.

json
{
  "id": "5c8e2f0a-…",
  "type": "form.goal_reached",
  "version": 1,
  "occurredAt": "2026-07-30T18:20:05.310Z",
  "companyId": "c0ffee00-…",
  "data": {
    "formId": "f1a2b3c4-…",
    "formName": "Pesquisa de satisfação — Julho",
    "responseCount": 500,
    "goal": 500,
    "alertRuleId": "a1b2c3d4-…"
  }
}
CampoTipoObservação
responseCountnumberContagem no momento da travessia
goalnumberMeta configurada na regra
alertRuleIdstring | nullQual regra disparou

Dispara na travessia, uma vez só

O evento sai quando a contagem cruza a meta, não a cada resposta acima dela. Rearmar exige resetar a regra no painel.


enrollment.dropped ​

Uma matrícula passou para evadida — pelo painel ou pelo envio de roster (POST /v1/enrollments/import).

json
{
  "id": "9d3b1c77-…",
  "type": "enrollment.dropped",
  "version": 1,
  "occurredAt": "2026-08-14T11:02:44.120Z",
  "companyId": "c0ffee00-…",
  "data": {
    "enrollmentId": "e1a2b3c4-…",
    "classId": "cl1a2b3c-…",
    "classCode": "ADM-2026-1",
    "className": "Administração — Noite",
    "personId": "p1a2b3c4-…",
    "externalId": "2026001",
    "name": "João Souza",
    "email": "joao@exemplo.com",
    "phone": null,
    "status": "dropped",
    "previousStatus": "enrolled",
    "courseItemId": "ci1a2b3c-…",
    "orgUnitId": "ou1a2b3c-…",
    "changedAt": "2026-08-14T11:02:44.100Z"
  }
}
CampoTipoObservação
classCodestringO código da turma no seu sistema
externalIdstring | nullA matrícula do aluno no seu sistema
previousStatusstring | nullDe onde a matrícula veio (enrolled, completed)
changedAtstringQuando a situação mudou (ISO 8601 UTC)

Só a mudança dispara

Reenviar a mesma situação no roster não gera evento. O payload já traz turma, curso e unidade — você não precisa de uma segunda chamada para saber de onde a evasão veio.

Se o seu servidor ficou fora do ar

GET /v1/enrollments?status=dropped&changedSince=… responde a mesma pergunta por varredura, paginada por cursor. O webhook continua sendo o caminho normal; a varredura é a rede de segurança.


treatment.opened ​

Uma resposta satisfez a condição que a instituição configurou na pesquisa e virou um caso — com responsável, etapa e prazo.

json
{
  "id": "5f2e1a90-…",
  "type": "treatment.opened",
  "version": 1,
  "occurredAt": "2026-08-31T13:20:11.004Z",
  "companyId": "c0ffee00-…",
  "data": {
    "treatmentId": "t1a2b3c4-…",
    "number": 341,
    "formId": "f1a2b3c4-…",
    "formName": "Satisfação — Agosto",
    "responseId": "r1a2b3c4-…",
    "status": "open",
    "stepName": "Tratativa e anotação",
    "stepIndex": 0,
    "dueAt": "2026-09-07T23:59:59.999Z",
    "orgUnitId": "ou1a2b3c-…",
    "orgPath": "/ou0a1b2c/ou1a2b3c/",
    "at": "2026-08-31T13:20:11.000Z"
  }
}
CampoTipoObservação
numberintegerO número do caso dentro da instituição — é por ele que ele é citado
stepNamestring | nullA etapa em que o caso está
dueAtstring | nullVencimento da etapa, contado em dias úteis (seg–sex)
orgPathstring | nullUnidade da resposta, herdada no caso

Um caso por resposta

A entrega é at-least-once, mas a abertura não é: reprocessar o mesmo fato não cria um segundo caso. O payload não traz as respostas — use o responseId com o response.created correspondente se precisar do conteúdo.


treatment.overdue ​

A etapa corrente de um caso passou do prazo. dueAt traz o vencimento ultrapassado.

json
{
  "id": "7c9d0e11-…",
  "type": "treatment.overdue",
  "version": 1,
  "occurredAt": "2026-09-08T09:00:03.552Z",
  "companyId": "c0ffee00-…",
  "data": {
    "treatmentId": "t1a2b3c4-…",
    "number": 341,
    "formId": "f1a2b3c4-…",
    "formName": "Satisfação — Agosto",
    "responseId": "r1a2b3c4-…",
    "status": "open",
    "stepName": "Tratativa e anotação",
    "stepIndex": 0,
    "dueAt": "2026-09-07T23:59:59.999Z",
    "orgUnitId": "ou1a2b3c-…",
    "orgPath": "/ou0a1b2c/ou1a2b3c/",
    "at": "2026-09-08T09:00:03.000Z"
  }
}

Uma vez por etapa

O vencimento sai uma vez por etapa. Se o caso avançar e a etapa seguinte também estourar, chega um novo evento — com stepIndex diferente.


treatment.closed ​

O caso foi encerrado: cumpriu a última etapa do fluxo, ou foi fechado antes (e aí reason traz o motivo registrado). O campo outcome diz como ele terminou.

json
{
  "id": "8e0f1a22-…",
  "type": "treatment.closed",
  "version": 1,
  "occurredAt": "2026-09-10T16:45:00.180Z",
  "companyId": "c0ffee00-…",
  "data": {
    "treatmentId": "t1a2b3c4-…",
    "number": 341,
    "formId": "f1a2b3c4-…",
    "formName": "Satisfação — Agosto",
    "responseId": "r1a2b3c4-…",
    "status": "closed",
    "stepName": "Resposta final e encerramento",
    "stepIndex": 3,
    "dueAt": null,
    "orgUnitId": "ou1a2b3c-…",
    "orgPath": "/ou0a1b2c/ou1a2b3c/",
    "at": "2026-09-10T16:45:00.000Z",
    "outcome": "resolved",
    "reason": null
  }
}
outcomeO que significa
resolvedtratado — a demanda foi resolvida (é o valor de quem cumpriu o fluxo inteiro)
unfoundedimprocedente: a reclamação não se sustentou
no-responsesem retorno de quem reclamou (informação insuficiente para tratar)
duplicateo mesmo caso já estava em tratativa
othero que não coube acima — vem com reason preenchido

Casos encerrados antes da coluna existir chegam com outcome: null; trate-o como "sem desfecho registrado", nunca como resolved.

responseId pode ser nulo

Quando a verificação de eficácia de um caso aponta que a ação corretiva não funcionou, a instituição pode abrir um caso novo encadeado ao anterior. Esse caso trata a mesma queixa e chega com responseId: null — a resposta original está no caso anterior. Se o seu consumidor usa responseId como chave, trate o nulo antes de indexar.

A resposta enviada não vem no evento

Quando o fluxo tem uma etapa de resposta ao respondente, o texto enviado fica no caso — não no data. Ele carrega dado pessoal e a redação do atendimento, e nenhum consumidor externo precisa dele para reagir ao encerramento.

Reabertura não emite evento

Um caso encerrado pode ser reaberto no painel. A reabertura fica no histórico do caso, mas não gera um tipo novo — se você acompanha o estado, releia o caso em vez de assumir que closed é final.


ping ​

Disparado pelo botão Testar em Integrações › Webhooks. Não nasce de nenhum fato do domínio — serve para validar URL, assinatura e conectividade.

json
{
  "id": "0a1b2c3d-…",
  "type": "ping",
  "version": 1,
  "occurredAt": "2026-07-28T09:00:00.000Z",
  "companyId": "c0ffee00-…",
  "data": { "message": "ping" }
}

Seu receptor deve respondê-lo com 2xx como qualquer outro. Se você assinou *, garanta que o ping não quebre o roteamento por tipo.


Tipos novos ​

O catálogo cresce. Se você assinou *, ignore com elegância o que não conhece — responda 200, não 400:

js
const handlers = {
  'response.created': salvarResposta,
  'invite.failed': marcarFalha,
};

const handler = handlers[evento.type];
if (!handler) return res.sendStatus(200);   // não é erro do remetente
await handler(evento.data);

O mesmo vale para campos novos dentro de data: eles aparecem sem aviso e sem bumpar a version. Ver compatibilidade.

Documentação da API pública do Edusati