Webhooks
Receba avisos da OctoSolve no seu sistema quando algo acontecer, com assinatura e reenvio automático.
Webhooks
Com a API você pergunta para a OctoSolve. Com webhook, a OctoSolve avisa você: toda vez que algo acontece na sua conta, mandamos uma requisição POST para o endereço que você cadastrar.
Serve para coisas como registrar o agendamento no seu sistema, avisar o time num canal interno quando cair um lead, ou disparar uma automação no Make ou no n8n quando um pagamento for confirmado.
Você cadastra os endereços em Configurações, aba Integrações.
Eventos disponíveis
Cada endereço cadastrado escolhe quais eventos quer receber. Não é tudo ou nada.
| Evento | Quando dispara |
|---|---|
contact.created | Um contato novo foi criado |
contact.updated | Um contato foi alterado |
contact.deleted | Um contato foi apagado, pela API ou pelo painel |
appointment.created | Um agendamento foi criado |
appointment.updated | Um agendamento mudou data, duração, profissional, serviço, valor ou status sem cancelamento |
appointment.cancelled | Um agendamento foi cancelado por qualquer fluxo |
payment.confirmed | Um pagamento foi confirmado |
conversation.handoff | Uma conversa passou do agente para uma pessoa do seu time |
Mensagens de conversa não geram evento nesta versão, de propósito: o volume é alto e o conteúdo é sensível.
Formato do envio
Todo evento chega com a mesma casca:
O data muda conforme o tipo. Campos podem vir null.
contact.created e contact.updated
Mesmo formato do contato na API: id, telefone, nome, email, cidade, data_nascimento, observacoes, tags, status, ig_username, telegram_username, updated_at.
O telefone pode vir null: contato que chegou pelo Instagram ou pelo Telegram se identifica por usuário.
O CPF não vai no evento, de propósito. O aviso sai da nossa casa e entra no seu sistema, e no caminho ele passa por lugares que ninguém controla depois: o log do seu servidor, o log do seu gateway e ferramentas de integração no meio (Make, n8n, Zapier). O CPF é do cliente final, não da sua empresa, então quanto menos lugares ele visitar, menor o estrago se um desses lugares vazar.
Se você precisa do CPF, ele está na API. Pegue o id que veio no evento e consulte:
Esse caminho é diferente do aviso: a consulta sai da sua chave, fica registrada, e você escolhe a hora. Vale a mesma ideia para qualquer dado que não esteja no evento.
contact.deleted
| Campo | Tipo |
|---|---|
id | uuid |
deleted_at | datetime |
Payload mínimo, de propósito: quem apagou o próprio dado pessoal não pode receber telefone, nome nem CPF junto do aviso de que ele foi apagado. Use o id para casar com o contato que você já tinha guardado do contact.created.
Este evento dispara tanto quando o contato é apagado pela API (DELETE /v1/contacts/{id}) quanto pelo painel.
Apagar um contato apaga junto os eventos dele que ainda estavam na fila. Se o contato foi apagado logo depois de um agendamento ou cancelamento, esses eventos podem nunca chegar até você: só o contact.deleted chega. É por exigência de privacidade, porque a entrega guarda uma cópia do dado pessoal enquanto espera.
Duas coisas seguem disso. Não conte com o histórico de agendamentos de um contato apagado, e trate o contact.deleted apagando o registro do seu lado também: o que já saiu para o seu servidor continua aí, e a exclusão feita aqui não alcança a sua base.
appointment.created, appointment.updated e appointment.cancelled
| Campo | Tipo |
|---|---|
id | uuid |
contact_id | uuid |
data_hora | datetime |
status | string |
duracao_minutos | int ou null |
servico_id | uuid ou null |
profissional_id | uuid ou null |
valor | número ou null |
pago | booleano ou null |
motivo_cancelamento | string ou null |
created_at | datetime ou null |
appointment.updated é enviado quando muda data_hora, duracao_minutos, profissional_id, servico_id, valor ou status, desde que o novo status não seja um cancelamento.
Alterar somente notas não envia webhook. A nota não faz parte do payload, de propósito: é texto livre do dono e pode conter dado sensível, inclusive clínico. Enviar um evento sem mostrar o que mudou também criaria ruído para a integração.
appointment.cancelled é enviado em qualquer cancelamento, inclusive no cancelamento automático por pagamento não feito. Os estados cancelado, cancelado_por_nao_pagamento, cancelado_estornado e cancelado_lgpd pertencem a essa família.
payment.confirmed
| Campo | Tipo |
|---|---|
id | uuid |
contact_id | uuid ou null |
appointment_id | uuid ou null |
valor_brl | número |
tipo | string |
status | string |
paid_at | datetime ou null |
created_at | datetime |
conversation.handoff
| Campo | Tipo |
|---|---|
conversation_id | uuid |
contact_id | uuid |
canal | string |
motivo | string |
ocorrido_em | datetime |
Nenhum trecho da conversa é enviado.
Cabeçalhos
| Cabeçalho | Para que serve |
|---|---|
X-Octo-Event | Tipo do evento |
X-Octo-Timestamp | Momento do envio, em segundos Unix. Entra na assinatura |
X-Octo-Signature | Assinatura no formato sha256=<hex> |
X-Octo-Delivery | Identificador da entrega. Estável entre as tentativas: use para deduplicar |
X-Octo-Event-Id | Identificador do evento que originou a entrega |
X-Octo-Event-Timestamp | Momento em que o fato aconteceu, ISO 8601 em UTC |
X-Octo-Attempt | Número da tentativa. 1 é a primeira |
Deduplique por X-Octo-Delivery. Se o seu servidor processar o evento e a
resposta se perder, nós reenviamos: a segunda entrega chega com o mesmo
X-Octo-Delivery. Tratar cada chegada como nova duplica registro.
Use X-Octo-Event-Timestamp para decidir se o evento ainda faz sentido. Um
appointment.cancelled represado por duas horas pode não valer mais no seu
sistema, e só esse cabeçalho conta essa diferença: o X-Octo-Timestamp é a hora
da tentativa e sempre parece recente.
Verificar a assinatura
Verifique sempre. Sem isso, qualquer um que descubra a sua URL pode fingir ser a OctoSolve.
A assinatura é um HMAC SHA256 do texto {timestamp}.{corpo}, usando o secret do endereço como chave. O corpo é o texto cru recebido, antes de virar objeto.
Rejeite envios com X-Octo-Timestamp de mais de 5 minutos atrás, para bloquear reenvio malicioso.
Python
Node
No Express, guarde o corpo cru com express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }). Se você assinar em cima do JSON já convertido e convertido de volta, a assinatura não vai bater.
O mesmo evento pode chegar duas vezes
Esta é a parte mais importante desta página.
A entrega é pelo menos uma vez. Em falha de rede ou reenvio, o mesmo evento pode chegar repetido. O seu sistema precisa aguentar isso.
Como resolver: guarde o campo id do evento (o evt_...) e ignore o que você já processou. Esse id é estável, inclusive quando você clica em Reenviar no painel.
A ordem também não é garantida. Uma tentativa que falhou e foi reenviada pode chegar depois de um evento mais novo. Se a ordem importa para você, use o created_at do evento, não a ordem de chegada.
Se o seu endereço não responder
Consideramos entregue quando a resposta é 2xx. Qualquer outra coisa é falha.
Tentamos de novo, com espaço crescente entre as tentativas:
| Tentativa | Depois da anterior |
|---|---|
| 2ª | 1 minuto |
| 3ª | 5 minutos |
| 4ª | 30 minutos |
| 5ª | 2 horas |
| 6ª (última) | 6 horas |
Algumas respostas não são tentadas de novo, porque insistir não adiantaria:
3xx(redirecionamento): não seguimos redirecionamento, por segurança. Cadastre o endereço final.4xx, com exceção de408e429: quer dizer que o endereço existe mas recusou. Corrija e reenvie pelo painel.
Respondemos ao Retry-After quando ele pede um tempo maior que o nosso, com teto de 24 horas por espera nos eventos de pagamento e contato, e de 2 horas nos de agenda e atendimento.
A agenda depende do evento
Nem todo aviso envelhece igual, então nem todo aviso insiste igual:
| Evento | Tentativas | Última tentativa, contando da primeira |
|---|---|---|
payment.confirmed | 6 | cerca de 8 h 36 min |
contact.created, contact.updated, contact.deleted | 6 | cerca de 8 h 36 min |
appointment.created, appointment.updated, appointment.cancelled | 4 | cerca de 36 min |
conversation.handoff | 4 | cerca de 36 min |
Evento de agenda entregue horas depois pode chegar quando o horário já passou: o seu sistema agiria sobre um fato morto. Pagamento é o contrário, e por isso insiste por mais tempo.
Endereço desligado automaticamente
Depois de 20 tentativas seguidas com falha, desligamos o endereço e avisamos você no painel. É proteção contra endereço abandonado gerando fila infinita.
Ele não é apagado: você conserta o seu lado e clica em reativar.
Trocar o endereço não cria um endpoint novo. Em Configurações, aba Integrações, edite o cadastro e salve o endereço novo: o secret e o histórico de entregas continuam os mesmos. As tentativas com falha não zeram ao trocar, então um endereço já desligado continua desligado depois de salvar, e precisa do clique em reativar.
Requisitos do endereço
| Regra | Motivo |
|---|---|
Precisa ser https:// na porta 443 | Evento trafega com dado do seu cliente |
| Não pode apontar para rede interna | Proteção contra uso da OctoSolve para atacar redes privadas |
| Não pode ter usuário e senha na URL | Formato usado para enganar leitor humano |
| Responda rápido, em até 10 segundos | Passou disso, tratamos como falha e tentamos de novo |
Responda 200 assim que receber e processe depois, em segundo plano. Não deixe o processamento inteiro dentro da requisição.
Limites por plano
| Plano | Webhooks | Endereços |
|---|---|---|
| Starter | não | 0 |
| Plus | sim | 5 |
| Pro | sim | 10 |
O limite de requisições por minuto da página de Limites vale para a API. Envio de webhook é nosso, não consome a sua cota.
Retenção do histórico
O histórico de entregas, com tentativas, código de resposta e corpo devolvido pelo seu servidor, fica disponível por 30 dias. Depois disso a linha é apagada.
Isso importa em dois momentos: uma investigação de "por que não chegou" precisa acontecer dentro dessa janela, e qualquer auditoria de prazo maior tem que guardar o evento no seu lado, no momento em que ele chega.
Receita para Make ou n8n
- Crie um cenário (Make) ou workflow (n8n) começando com Webhook ou Webhook Trigger.
- Copie a URL que a ferramenta gerar.
- Na OctoSolve, vá em Configurações, aba Integrações, e cadastre essa URL escolhendo os eventos.
- Copie o secret que aparece uma única vez e guarde.
- No fluxo, adicione um passo que confira a assinatura (as duas ferramentas têm nó de código; use o exemplo em Node acima).
- Adicione um passo que ignore evento com
idjá visto, guardando os ids num Data Store (Make) ou numa tabela (n8n). - Só depois disso, ligue o passo que faz o trabalho de verdade.
Os passos 5 e 6 parecem burocracia, mas são o que separa uma automação que funciona de uma que duplica cobrança no fim de semana.
Testar
Enquanto você monta, aponte para um coletor público (como o webhook.site) para ver o formato real do evento chegando. Depois troque para o seu endereço definitivo e rotacione o secret, porque o antigo passou por um serviço de terceiro.
Veja também
- Integrações, a mesma tela explicada sem jargão, com prints de onde ficam o cadastro do endereço, a troca do segredo e o histórico de entregas.
- Limites de requisições, para o teto da API que a sua integração também usa.