Pular para o conteúdo

Visão geral

Webhook é o Faturei Hoje avisando o seu sistema quando alguma coisa acontece na empresa: um cliente cadastrado, uma venda paga, uma OS concluída. Em vez de você perguntar à API de tempos em tempos, a API manda um POST com o evento para um endereço seu, o endpoint.

  1. Alguém grava alguma coisa: pelo painel, pela API, pelo WhatsApp, por uma automação ou por uma rotina automática.
  2. Na mesma gravação, o evento é registrado com o objeto do jeito que o GET daquele recurso devolveria naquele instante, com as mesmas regras de campos.
  3. O evento vira uma entrega para cada endpoint ligado e inscrito nele.
  4. A entrega é assinada e enviada. Se falhar, ela é tentada de novo por uma agenda de cerca de 3 dias. Veja Entregas e novas tentativas.

O evento só é registrado quando a empresa tem, naquele momento, ao menos um endpoint ligado e inscrito naquele tipo de evento. O que acontece enquanto nenhum endpoint escuta não vira evento, nem depois.

Pela API, com POST /public/v1/webhook_endpoints:

Terminal window
curl -X POST "https://api.fatureihoje.com/public/v1/webhook_endpoints" \
-H "Authorization: Bearer $FH_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0f5d3c1e-7a2b-4e8f-9c6d-1b2a3c4d5e6f" \
-d '{
"url": "https://erp.suaempresa.com.br/webhooks/faturei-hoje",
"description": "ERP",
"events": ["sale.created", "sale.paid", "client.created"]
}'

A resposta traz o endpoint e o campo secret, que começa com whsec_. Pela API, o segredo só aparece aqui e na rotação: guarde-o no seu servidor, é com ele que você verifica a assinatura. Repetir a mesma chamada com a mesma Idempotency-Key devolve o endpoint sem o segredo. No painel, dono e administradores podem revelar o segredo de novo, confirmando a senha.

  • events é a lista de nomes do Catálogo de eventos, ou ["*"] para todos, inclusive os que entrarem no catálogo depois. * vai sozinho: junto de outro nome, a resposta é 422.
  • Gerir webhooks é de dono e administrador, pelo painel (em Configurações → API → Webhooks) ou por uma chave de API de um membro com esse papel. No painel, mudar a URL ou os eventos pede a senha. As rotas de leitura também exigem o papel, porque a lista de endpoints diz para onde os dados da empresa vão.
  • Receber um evento é ler o objeto dele. Por isso, para se inscrever, a chave precisa de <modulo>:read de cada módulo dos eventos escolhidos (com *, de todos). Sem isso, a resposta é 403.
  • Cada endpoint tem o seu segredo. Dois endpoints nunca dividem segredo.
  • Dono e administradores recebem um e-mail a cada endpoint criado.

Para testar a conexão, POST /public/v1/webhook_endpoints/{id}/ping manda um evento ping de verdade só para aquele endpoint, pelo mesmo caminho de qualquer evento (assinatura, novas tentativas, log de entregas). Não é ambiente de teste: é só uma conferência de que o endereço recebe e verifica. Endpoint desligado responde 409 conflict.

As rotas completas, campo a campo, estão na Referência.

A URL passa por uma conferência no cadastro, e a mesma conferência roda de novo em toda tentativa de envio, porque o dono do domínio pode trocar o DNS depois do cadastro.

  • Só https://. Endereço http:// é recusado.
  • Qualquer porta de 1 a 65535.
  • Sem usuário e senha na URL (https://usuario:senha@...): é recusado, porque ficaria guardado e apareceria na leitura do endpoint.
  • O nome precisa resolver para endereço público na internet. localhost, nome sem ponto, nomes terminados em .local, .internal, .lan e parecidos, IP privado, reservado ou de metadados de nuvem são recusados. Todos os registros A e AAAA do nome são conferidos: basta um interno para recusar.
  • O certificado TLS precisa ser válido. Certificado autoassinado ou vencido faz a entrega falhar.
  • Redirecionamento não é seguido. Um 3xx conta como falha.

No cadastro, a URL recusada responde 422 com param: "url". No envio, a tentativa falha e o motivo aparece no log de entregas.

Cada entrega é um POST com estes cabeçalhos, no padrão Standard Webhooks:

CabeçalhoValor
webhook-idId do evento, evt_ seguido de 32 caracteres hexadecimais. O mesmo id do corpo
webhook-timestampMomento desta tentativa, em segundos Unix
webhook-signatureA assinatura, v1, seguido do HMAC em base64. Veja Verificar a assinatura
content-typeapplication/json
user-agentFatureiHoje-Webhooks/1.0

E este corpo:

{
"id": "evt_9b2f4c7e1a3d4f6b8c0e2a4d6f8b1c3e",
"type": "client.updated",
"api_version": "v1",
"timestamp": "2026-09-16T14:30:00.000Z",
"organization_id": "5c1e8f2a-3b4d-4c6e-8f0a-1b2c3d4e5f60",
"data": {
"object": { "object": "client", "id": "0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d", "...": "..." },
"changed_fields": ["email", "phone"]
}
}
CampoO que é
idId do evento. O mesmo em todas as tentativas e no reenvio manual
typeNome do evento, do Catálogo de eventos
api_versionVersão do contrato do objeto. Hoje, sempre v1
timestampQuando o evento aconteceu, em ISO 8601 UTC. Não muda entre tentativas
organization_idA empresa do evento
data.objectO objeto, igual ao que o GET do recurso devolveria naquele instante
data.changed_fieldsSó nos eventos .updated: quais campos mudaram
data.object_truncatedSó aparece quando o objeto veio resumido, e aí vale true

Repare na diferença entre os dois horários: webhook-timestamp é o horário do envio, e muda a cada tentativa (é o que protege contra repetição maliciosa); timestamp, no corpo, é o horário do fato, e é por ele que você ordena.

O ping tem o mesmo formato, com type: "ping" e um objeto próprio:

{
"id": "evt_4e6a8c0b2d4f4a6c8e0b2d4f6a8c0e2b",
"type": "ping",
"api_version": "v1",
"timestamp": "2026-09-16T14:30:00.000Z",
"organization_id": "5c1e8f2a-3b4d-4c6e-8f0a-1b2c3d4e5f60",
"data": {
"object": { "object": "ping", "webhook_endpoint_id": "7f3a1c5e-9b2d-4f6a-8c0e-2b4d6f8a0c1e" }
}
}

Nos eventos .updated, changed_fields lista os campos do objeto que mudaram naquela gravação, em ordem alfabética. updated_at nunca entra na lista, porque muda em toda gravação e não diz o que mudou.

Uma gravação pode gerar mais de um evento. Se a situação de uma OS mudou junto com a descrição, saem service_order.status_changed e service_order.updated, e o changed_fields do segundo traz os dois campos. As regras de cada área estão no Catálogo de eventos.

Gravação que não muda nada no objeto não gera .updated: salvar um cadastro sem alterar nenhum campo não manda evento.

O corpo da entrega tem teto de 256 KB, contados em bytes. Quando o objeto não cabe, data.object vem só com object e id, e data.object_truncated vem true:

{
"data": {
"object": { "object": "sale", "id": "3c5e7a9b-1d3f-4b5d-8f1a-3c5e7a9b1d3f" },
"object_truncated": true
}
}

Com object_truncated, busque o objeto pela API (GET /public/v1/sales/{id}, no exemplo). Ele chega como está agora, que pode ser diferente de como estava no instante do evento.

Alguns caminhos de gravação não montam o objeto completo. Deles, o evento sai sempre resumido, no mesmo formato do teto acima: data.object = { object, id } e data.object_truncated: true. São eles:

  • o que o assistente do WhatsApp grava;
  • o que as automações gravam;
  • o que vem do Open Finance (importação e conciliação bancária);
  • as rotinas automáticas: a que marca lançamentos e vendas como vencidos e a que gera os meses seguintes das recorrências sem fim;
  • e, raramente, qualquer outra gravação em que o objeto não pôde ser montado na hora.

O tipo do evento é o certo (task.created, financial_transaction.updated…), mas o objeto não vem. Busque-o pela API. Num .updated resumido, changed_fields vem vazio, porque sem o objeto não dá para saber quais campos mudaram.

Trate object_truncated: true sempre do mesmo jeito, venha ele do teto de tamanho ou de um desses caminhos: busque o objeto pela API.

Algumas mudanças no objeto acontecem sem evento próprio. Elas são consequência de outra gravação, e o evento dessa outra gravação é o que você recebe.

  • Vínculo apagado junto com o pai. Quando um registro é excluído, o que apontava para ele perde o vínculo (o campo vira null) sem .updated. Por exemplo: as tarefas e os compromissos de um cliente, de uma OS, de um lançamento ou de um membro que foi excluído, e a OS de um compromisso excluído. Você recebe o .deleted do pai.
  • Exclusão de produto. Os itens de venda perdem o product_id e os movimentos de estoque do produto são apagados, sem sale.updated nem stock.changed. Você recebe o product.deleted.
  • Renomear uma categoria não gera product.updated nem service.updated nos itens dela.
  • Número do recibo. O receipt_number de um recebimento da venda (payments[].receipt_number) sai de null para um número (R-0001, por exemplo) quando alguém gera no painel, pela primeira vez, o recibo do lançamento desse recebimento. Isso acontece sem sale.updated.
  • Nota fiscal. invoice_locked da venda e has_active_invoice dos recebimentos mudam quando uma nota fiscal é emitida ou cancelada, sem sale.updated.
  • is_overdue da tarefa mudando sozinho, com a passagem do tempo, não gera task.updated.
  • Expiração de orçamento. O orçamento vencido só muda de situação quando alguém abre a lista de orçamentos. O quote.status_changed da expiração sai nessa hora, e não no dia do vencimento.

Quando o dado precisa estar exato, releia o recurso pela API em vez de confiar só nos eventos. Veja Boas práticas.

Cada endpoint guarda quem o inscreveu: o membro, e a chave de API quando a inscrição veio pela API. Quem inscreveu é quem criou o endpoint ou, depois, quem mudou a URL ou os eventos, ou religou o endpoint.

Antes de cada envio, o Faturei Hoje confere se quem inscreveu ainda pode ler o módulo daquele evento: a chave continua ativa e com <modulo>:read, e o membro continua ativo e com acesso ao módulo, pelas mesmas regras do painel. Se não pode mais:

  • a entrega daquele evento é fechada sem ser enviada, com situação failed e erro subscriber_access_lost no log;
  • isso não gasta tentativa e não conta como falha do endpoint, que continua ligado;
  • os eventos dos outros módulos, que quem inscreveu ainda lê, continuam chegando;
  • dono e administradores recebem um e-mail avisando, e o aviso só volta a sair depois que uma entrega der certo e o acesso for perdido de novo.

Para voltar a receber, devolva o acesso a quem inscreveu, ou edite o endpoint com um membro (ou chave) que tenha o acesso: quem edita a URL ou os eventos passa a ser quem inscreveu.

Os eventos de venda (sale.*) nunca levam account_id nem finance_transactions, mesmo que quem inscreveu leia o financeiro: se inscrever em vendas exige ler só vendas. Os lançamentos têm os eventos deles, financial_transaction.*, que exigem ler o financeiro.

A empresa que muda para um plano sem a API para de receber entregas, inclusive o ping: elas são fechadas sem envio, com o erro plan_without_api.

  • Rotação com convivência (carência maior que zero, como as opções de 1 hora e de 24 horas do painel): os endpoints inscritos pela chave antiga passam para a chave nova na mesma ação, e nada para de chegar.
  • Rotação “Parar agora”, o gesto de quem suspeita que a chave vazou: os endpoints inscritos pela chave antiga são pausados, porque quem pegou a chave pode ter cadastrado um endpoint para receber os seus dados. Eles aparecem com status: "disabled" e disabled_reason: "emergency_key_rotation", e dono e administradores recebem um e-mail com a lista (só o domínio de cada URL). Revise a lista e religue o que for seu.
  • Chave achada pelo GitHub em repositório público, quando a inscrição no programa de varredura de segredo do GitHub estiver ativa: a chave é revogada automaticamente e os endpoints inscritos por ela são pausados do mesmo jeito, com o mesmo disabled_reason: "emergency_key_rotation", e dono e administradores recebem um e-mail. A inscrição ainda não está ativa.
  • Chave revogada ou vencida (fora o caso acima): os endpoints inscritos por ela deixam de receber, pela regra de acesso acima.

Veja Rotação.

Quantos endpoints a empresa pode ter cadastrados, ligados ou não:

PlanoEndpoints de webhook
Grátis0
Start3
Pleno10
Supra25

No envio:

  • até 5 entregas simultâneas por endpoint;
  • 1 entrega por vez para o endpoint que está falhando, até uma entrega para ele dar certo;
  • até 10 entregas simultâneas por empresa, somando todos os endpoints. Um endpoint lento não segura a fila das outras empresas;
  • o endpoint que só falha por 3 dias seguidos é desligado, e dono e administradores recebem um e-mail. Veja Desligamento automático.

Os dados enviados passam a ser responsabilidade de quem configurou

Seção intitulada “Os dados enviados passam a ser responsabilidade de quem configurou”

Cada entrega leva dados da empresa para fora do Faturei Hoje, para o sistema que o endpoint aponta. A partir do momento em que o dado chega a esse sistema, ele passa a ser responsabilidade da empresa que configurou a integração, inclusive perante a LGPD: quem guarda, por quanto tempo, quem acessa e como descarta.

Do nosso lado, os eventos e o log de entregas ficam guardados por 30 dias e depois são apagados. O envio roda na nossa própria infraestrutura, sem nenhum serviço de terceiro no meio.