Como funciona
- O provedor cadastra o endereço do seu sistema em Configurações → API pública → Webhooks e escolhe os eventos.
- O sistema mostra um segredo (
whsec_…), que o provedor entrega a você para validar a origem dos avisos. - Quando um evento acontece, o seu endereço recebe um
POSTcom JSON, em até cerca de 30 segundos. - Responda com qualquer código
2xxem até 10 segundos. Sem isso, o aviso é enviado de novo mais tarde.
Requisitos do endereço: HTTPS, público na internet, sem usuário e senha na URL. Endereços de rede interna são recusados.
O provedor pode enviar um teste a qualquer momento: ele chega como evento teste.ping.
Eventos
| Evento | Quando | Em dados |
|---|---|---|
fatura.criada | Uma fatura foi gerada. | fatura |
fatura.paga | Uma fatura foi paga (banco, PIX ou baixa manual). | fatura |
fatura.vencida | Uma fatura passou do vencimento sem pagamento. | fatura |
fatura.cancelada | Uma fatura foi cancelada. | fatura |
fatura.estornada | Um pagamento foi estornado. | fatura |
contrato.ativado | Contrato ativado. | contrato, registro |
contrato.bloqueado | Contrato bloqueado. | contrato, registro |
contrato.desbloqueado | Contrato desbloqueado. | contrato, registro |
contrato.desbloqueio_confianca | Desbloqueio de confiança aplicado. | contrato, registro |
contrato.inativado | Contrato inativado. | contrato, registro |
contrato.cancelado | Contrato cancelado. | contrato, registro |
conexao.online | O cliente voltou a ficar online. | contrato, registro |
conexao.offline | O cliente ficou offline. | contrato, registro |
chamado.aberto | Um chamado foi aberto (por qualquer canal). | chamado |
chamado.atualizado | Um chamado foi alterado. | chamado |
fatura, contrato e chamado têm exatamente o mesmo formato das rotas
/faturas/{id}, /contratos/{id}
e /chamados/{id}. Os eventos de conexão podem ser muitos em redes instáveis:
assine só se for usar.
Formato do aviso
POST /webhooks/websystem HTTP/1.1
Content-Type: application/json
User-Agent: WebSystemProvider-Webhooks/1
X-WSP-Evento: fatura.paga
X-WSP-Entrega: 4182
X-WSP-Assinatura: t=1760367602,v1=5f1c0e...a9
{
"id": "evt_9120",
"tipo": "fatura.paga",
"criado_em": "2026-10-13T15:00:02.000Z",
"dados": {
"fatura": {
"id": 77, "cliente_id": 5, "contrato_id": 10, "situacao": "paga", "tipo": "pix",
"valor": 99.9, "valor_pago": 99.9, "vencimento": "2026-10-10",
"pago_em": "2026-10-13T14:59:40.000Z", "segunda_via": null
}
}
}
Nos eventos de contrato e de conexão, registro traz o que aconteceu:
"registro": { "acao": "bloqueio", "descricao": "Bloqueio automático por inadimplência", "autor": "Sistema", "ocorrido_em": "2026-10-13T10:00:00.000Z" }
Validar a assinatura
Cada aviso vem assinado. O cabeçalho X-WSP-Assinatura tem o horário (t) e a assinatura
(v1): um HMAC-SHA256, com o segredo do webhook, de <t>.<corpo bruto>. Compare
a assinatura calculada com a recebida e recuse avisos com mais de 5 minutos, para evitar reenvio malicioso.
Calcule sobre o corpo bruto, exatamente como chegou. Reformatar o JSON antes muda a assinatura.
Node.js (Express)
const crypto = require('crypto');
app.post('/webhooks/websystem', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-WSP-Assinatura') || '';
const t = (header.match(/t=(\d+)/) || [])[1];
const v1 = (header.match(/v1=([a-f0-9]+)/) || [])[1];
const esperado = crypto.createHmac('sha256', process.env.WSP_WEBHOOK_SECRET)
.update(`${t}.${req.body}`).digest('hex');
const valido = v1 && v1.length === esperado.length
&& crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(esperado));
if (!valido || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(401);
const evento = JSON.parse(req.body);
// trate evento.tipo e evento.dados — e responda rápido:
res.sendStatus(200);
});
PHP
$corpo = file_get_contents('php://input');
preg_match('/t=(\d+),v1=([a-f0-9]+)/', $_SERVER['HTTP_X_WSP_ASSINATURA'] ?? '', $m);
$esperado = hash_hmac('sha256', ($m[1] ?? '') . '.' . $corpo, getenv('WSP_WEBHOOK_SECRET'));
if (empty($m) || !hash_equals($esperado, $m[2]) || abs(time() - (int)$m[1]) > 300) {
http_response_code(401);
exit;
}
$evento = json_decode($corpo, true);
http_response_code(200);
Python (Flask)
import hmac, hashlib, os, re, time
from flask import request, abort
@app.post('/webhooks/websystem')
def webhook():
corpo = request.get_data()
m = re.search(r't=(\d+),v1=([a-f0-9]+)', request.headers.get('X-WSP-Assinatura', ''))
if not m:
abort(401)
esperado = hmac.new(os.environ['WSP_WEBHOOK_SECRET'].encode(), f'{m[1]}.'.encode() + corpo, hashlib.sha256).hexdigest()
if not hmac.compare_digest(esperado, m[2]) or abs(time.time() - int(m[1])) > 300:
abort(401)
evento = request.get_json()
return '', 200
Novas tentativas
Se o seu sistema não responder 2xx em 10 segundos, o aviso é enviado de novo, com espera crescente:
| Tentativa | Espera depois da anterior |
|---|---|
| 2ª | 1 minuto |
| 3ª | 5 minutos |
| 4ª | 15 minutos |
| 5ª | 1 hora |
| 6ª | 3 horas |
| 7ª | 6 horas |
| 8ª | 12 horas |
| 9ª (última) | 24 horas |
- Depois da última, a entrega fica como falhou. O provedor vê cada entrega na tela e pode reenviar.
- Um endereço com 100 falhas seguidas é pausado automaticamente; o provedor retoma quando o seu sistema voltar.
- Os avisos e o histórico de entregas ficam guardados por 30 dias.
Boas práticas
- Responda rápido e processe depois (fila): o limite é de 10 segundos.
- Evite processar duas vezes: um aviso pode chegar mais de uma vez (reenvio). Use o
iddo evento (evt_…) para ignorar repetidos. - A ordem não é garantida: use
criado_eme, se precisar do estado atual, consulte a rota correspondente. - Os avisos começam a partir do cadastro do primeiro endereço: eventos anteriores não são enviados.