Webhooks
📚 Índice
- Introdução
- Como Funcionam os Webhooks
- Eventos Disponíveis
- Criar Webhooks
- Listar Webhooks
- Obter Webhook Específico
- Atualizar Webhooks
- Deletar Webhooks
- Formato do Payload
- Sistema de Retry
- Dead Letter Queue
- Segurança e Verificação
- Casos de Uso
- Boas Práticas
- Troubleshooting
Introdução
Webhooks permitem que sua aplicação receba notificações em tempo real sobre eventos de documentos. Quando um documento é criado, atualizado ou deletado, o Document Hub envia uma requisição HTTP POST para a URL que você configurou.
NESTE MOMENTO A FEATURE SO ENVIA DOCUMENTOS EM WEBHOOKS SEM AUTENTICAÇÃO
Escopo Necessário
Para gerenciar webhooks, você precisa de um token com o escopo webhook:manage.
Características
- ✅ Notificações em tempo real
- ✅ Processamento assíncrono (não afeta a API)
- ✅ Sistema de retry automático com backoff exponencial
- ✅ Dead Letter Queue para falhas persistentes
- ✅ Segurança com HMAC (planejado)
- ✅ Filtros por evento
- ✅ Ativação/desativação simples
Como Funcionam os Webhooks
Fluxo Completo
1. Usuário cria/atualiza/deleta um documento
└─> API processa a requisição
└─> Documento é salvo no banco de dados
└─> API retorna resposta para o usuário
2. Sistema dispara evento interno
└─> DocumentCreated/Updated/Deleted event
3. Listener captura o evento
└─> SendDocumentWebhookNotifications listener
4. Job é despachado para a fila
└─> SendWebhookNotification job
5. Job processa de forma assíncrona
└─> Busca todos os webhooks ativos do cliente
└─> Filtra webhooks que escutam este evento
└─> Envia POST para cada URL
6. Se falhar:
└─> Retry automático com backoff exponencial
└─> Após X tentativas, move para Dead Letter Queue
Vantagens do Processamento Assíncrono
- 🚀 Performance: A API não espera o webhook ser entregue
- 🔄 Confiabilidade: Sistema de retry automático
- 📊 Escalabilidade: Milhares de webhooks podem ser processados em paralelo
- 🛡️ Isolamento: Falhas em webhooks não afetam a API
Eventos Disponíveis
O Document Hub suporta os seguintes eventos:
| Evento | Descrição | Quando é Disparado |
|---|---|---|
document.created | Documento criado | POST /.../{key} cria novo documento |
document.updated | Documento atualizado | PUT /.../{key} atualiza documento |
document.deleted | Documento deletado | DELETE /.../{key} deleta documento |
document.read | Documento lido | GET /.../{key} lê documento |
Observações
- ✅ Você pode escutar um ou mais eventos por webhook
- ✅ Criar múltiplos webhooks para o mesmo evento (URLs diferentes)
- ⚠️
document.readpode gerar muitas notificações (use com cuidado)
Criar Webhooks
Endpoint
POST /api/v1/client/{client}/webhooksHeaders
Authorization: Bearer {SEU_TOKEN}
Content-Type: application/jsonBody da Requisição
{
"url": "https://seu-dominio.com/webhook/documents",
"events": ["document.created", "document.updated"],
"status": "active"
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
url | string | Sim | URL que receberá as notificações (deve ser HTTPS em produção) |
events | array | Sim | Lista de eventos a escutar |
status | string | Não | active (padrão) ou inactive |
Validações
- ✅
urldeve ser uma URL válida - ✅
urldeve ser única por cliente (não pode cadastrar a mesma URL 2 vezes) - ✅
eventsdeve conter pelo menos um evento válido - ✅ Cada evento em
eventsdeve ser um dos valores permitidos
Exemplo 1: Webhook Simples
curl -X POST "https://document-hub-api-xp.wake.tech/api/v1/client/{CLIENT_ID}/webhooks" \
-H "Authorization: Bearer {SEU_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"url": "https://meu-app.com/webhooks/documents",
"events": ["document.created", "document.updated"],
"status": "active"
}'Exemplo 2: Webhook para Todos os Eventos
curl -X POST "https://document-hub-api-xp.wake.tech/api/v1/client/{CLIENT_ID}/webhooks" \
-H "Authorization: Bearer {SEU_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"url": "https://meu-app.com/api/v1/document-events",
"events": [
"document.created",
"document.updated",
"document.deleted",
"document.read"
]
}'Exemplo 3: Webhook Inativo (para configurar antes de ativar)
curl -X POST "https://document-hub-api-xp.wake.tech/api/v1/client/{CLIENT_ID}/webhooks" \
-H "Authorization: Bearer {SEU_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"url": "https://staging.meu-app.com/webhooks/test",
"events": ["document.created"],
"status": "inactive"
}'Resposta (201 Created)
{
"id": "9fc42fe9-1234-5678-9abc-def123456789",
"client_id": "9fc42fe9-a4fd-443c-8d49-b85ece2151b9",
"url": "https://meu-app.com/webhooks/documents",
"events": ["document.created", "document.updated"],
"status": "active",
"created_at": "2024-01-15T10:30:00.000000Z",
"updated_at": "2024-01-15T10:30:00.000000Z"
}Listar Webhooks
Endpoint
GET /api/v1/client/{client}/webhooksExemplo
curl -X GET "https://document-hub-api-xp.wake.tech/api/v1/client/{CLIENT_ID}/webhooks" \
-H "Authorization: Bearer {SEU_TOKEN}"Resposta (200 OK)
{
"current_page": 1,
"data": [
{
"id": "9fc42fe9-1234-5678-9abc-def123456789",
"client_id": "9fc42fe9-a4fd-443c-8d49-b85ece2151b9",
"url": "https://meu-app.com/webhooks/documents",
"events": ["document.created", "document.updated"],
"status": "active",
"created_at": "2024-01-15T10:30:00.000000Z",
"updated_at": "2024-01-15T10:30:00.000000Z"
},
{
"id": "9fc42fe9-5678-1234-9abc-def123456789",
"url": "https://outro-app.com/api/notifications",
"events": ["document.deleted"],
"status": "inactive",
"created_at": "2024-01-10T08:00:00.000000Z",
"updated_at": "2024-01-12T14:30:00.000000Z"
}
],
"per_page": 15,
"total": 2
}Obter Webhook Específico
Endpoint
GET /api/v1/client/{client}/webhooks/{webhook_id}Exemplo
curl -X GET "https://document-hub-api-xp.wake.tech/api/v1/client/{CLIENT_ID}/webhooks/{WEBHOOK_ID}" \
-H "Authorization: Bearer {SEU_TOKEN}"Resposta (200 OK)
{
"id": "9fc42fe9-1234-5678-9abc-def123456789",
"client_id": "9fc42fe9-a4fd-443c-8d49-b85ece2151b9",
"url": "https://meu-app.com/webhooks/documents",
"events": ["document.created", "document.updated"],
"status": "active",
"created_at": "2024-01-15T10:30:00.000000Z",
"updated_at": "2024-01-15T10:30:00.000000Z"
}Atualizar Webhooks
Endpoint
PUT /api/v1/client/{client}/webhooks/{webhook_id}O que pode ser atualizado
- ✅ URL (
url) - ✅ Eventos (
events) - ✅ Status (
status)
Exemplo 1: Mudar URL
curl -X PUT "https://document-hub-api-xp.wake.tech/api/v1/client/{CLIENT_ID}/webhooks/{WEBHOOK_ID}" \
-H "Authorization: Bearer {SEU_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"url": "https://nova-url.com/webhooks/documents"
}'Exemplo 2: Adicionar Eventos
curl -X PUT "https://document-hub-api-xp.wake.tech/api/v1/client/{CLIENT_ID}/webhooks/{WEBHOOK_ID}" \
-H "Authorization: Bearer {SEU_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"events": ["document.created", "document.updated", "document.deleted"]
}'Exemplo 3: Desativar Temporariamente
curl -X PUT "https://document-hub-api-xp.wake.tech/api/v1/client/{CLIENT_ID}/webhooks/{WEBHOOK_ID}" \
-H "Authorization: Bearer {SEU_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"status": "inactive"
}'Exemplo 4: Atualização Completa
curl -X PUT "https://document-hub-api-xp.wake.tech/api/v1/client/{CLIENT_ID}/webhooks/{WEBHOOK_ID}" \
-H "Authorization: Bearer {SEU_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"url": "https://nova-url.com/api/v2/webhooks",
"events": ["document.created"],
"status": "active"
}'Resposta (200 OK)
{
"id": "9fc42fe9-1234-5678-9abc-def123456789",
"url": "https://nova-url.com/api/v2/webhooks",
"events": ["document.created"],
"status": "active",
"updated_at": "2024-01-20T15:45:00.000000Z"
}Deletar Webhooks
Endpoint
DELETE /api/v1/client/{client}/webhooks/{webhook_id}Exemplo
curl -X DELETE "https://document-hub-api-xp.wake.tech/api/v1/client/{CLIENT_ID}/webhooks/{WEBHOOK_ID}" \
-H "Authorization: Bearer {SEU_TOKEN}"Resposta (204 No Content)
Sem corpo de resposta. Webhook deletado com sucesso.
Comportamento
- ✅ O webhook é permanentemente deletado
- ✅ Notificações pendentes na fila ainda serão processadas
- ✅ Novas notificações não serão mais enviadas para esta URL
Formato do Payload
Quando um evento ocorre, o Document Hub envia uma requisição POST para sua URL com o seguinte formato:
Headers Enviados
Content-Type: application/json
User-Agent: DocumentHub-Webhook/1.0
X-DocumentHub-Event: document.created
X-DocumentHub-Delivery: 9fc42fe9-1234-5678-9abc-def123456789Body do Payload
{
"event": "document.created",
"data": {
"id": "9fc43d2a-8e4f-4a3b-9d2e-1a2b3c4d5e6f",
"client_id": "9fc42fe9-a4fd-443c-8d49-b85ece2151b9",
"environment": "production",
"context": "orders",
"type": "invoice",
"key": "INV-001",
"data": {
"customer_id": "CUST-123",
"amount": 1500.00,
"currency": "BRL",
"status": "pending"
},
"version": 1,
"created_at": "2024-01-15T10:30:00.000000Z",
"updated_at": "2024-01-15T10:30:00.000000Z",
"expires_at": null
},
"metadata": {
"source": "DocumentHub",
"version": "1.0",
"sent_at": "2024-01-15T10:30:01.000000Z",
"attempt": 1
}
}Campos do Payload
| Campo | Tipo | Descrição |
|---|---|---|
event | string | Nome do evento que disparou o webhook |
data | object | Dados completos do documento |
metadata | object | Metadados sobre a notificação |
metadata.source | string | Sempre "DocumentHub" |
metadata.version | string | Versão do formato do payload |
metadata.sent_at | timestamp | Momento do envio |
metadata.attempt | integer | Número da tentativa (1, 2, 3...) |
Exemplos de Payloads por Evento
document.created
{
"event": "document.created",
"data": {
/* documento completo recém-criado */
"version": 1
},
"metadata": { ... }
}document.updated
{
"event": "document.updated",
"data": {
/* documento completo com nova versão */
"version": 2
},
"metadata": { ... }
}document.deleted
{
"event": "document.deleted",
"data": {
/* documento com deleted_at preenchido */
"deleted_at": "2024-01-15T14:30:00.000000Z"
},
"metadata": { ... }
}document.read
{
"event": "document.read",
"data": {
/* documento que foi lido */
},
"metadata": { ... }
}Sistema de Retry
O Document Hub implementa um sistema robusto de retry com backoff exponencial.
Configuração (.env)
.env)# Número máximo de tentativas
WEBHOOK_MAX_ATTEMPTS=5
# Timeout da requisição HTTP (segundos)
WEBHOOK_TIMEOUT=10
# Verificar certificado SSL
WEBHOOK_VERIFY_SSL=true
# Backoff (segundos entre tentativas)
WEBHOOK_BACKOFF_CONFIG="[30,120,300,600,1800]"Funcionamento
-
Tentativa 1: Imediata (quando o evento ocorre)
- Se sucesso (200-299): ✅ Fim
- Se falha: Agenda retry
-
Tentativa 2: Após 30 segundos
- Se sucesso: ✅ Fim
- Se falha: Agenda retry
-
Tentativa 3: Após 120 segundos (2 minutos)
- Se sucesso: ✅ Fim
- Se falha: Agenda retry
-
Tentativa 4: Após 300 segundos (5 minutos)
- Se sucesso: ✅ Fim
- Se falha: Agenda retry
-
Tentativa 5: Após 600 segundos (10 minutos)
- Se sucesso: ✅ Fim
- Se falha: Move para Dead Letter Queue
Códigos de Status Considerados
| Faixa | Comportamento |
|---|---|
| 200-299 | ✅ Sucesso - Não faz retry |
| 300-399 | ⚠️ Redirect - Considera sucesso |
| 400-499 | ❌ Erro do cliente - Não faz retry (exceto 408, 429) |
| 500-599 | 🔄 Erro do servidor - Faz retry |
| Timeout | 🔄 Faz retry |
| Network Error | 🔄 Faz retry |
Exemplo de Implementação do Endpoint
// Node.js / Express
app.post('/webhooks/documents', (req, res) => {
try {
const { event, data, metadata } = req.body;
// Processar o evento
console.log(`Recebido evento: ${event}`);
console.log(`Documento: ${data.key}`);
// Fazer o que precisa (salvar em banco, enviar email, etc.)
processDocument(event, data);
// IMPORTANTE: Retornar 200 rapidamente
res.status(200).json({ received: true });
} catch (error) {
console.error('Erro processando webhook:', error);
// Retornar 500 para o Document Hub tentar novamente
res.status(500).json({ error: 'Internal Server Error' });
}
});# Python / Flask
@app.route('/webhooks/documents', methods=['POST'])
def webhook_handler():
try:
payload = request.get_json()
event = payload['event']
data = payload['data']
# Processar o evento
print(f'Recebido evento: {event}')
print(f'Documento: {data["key"]}')
process_document(event, data)
# Retornar 200 rapidamente
return jsonify({'received': True}), 200
except Exception as e:
print(f'Erro: {e}')
# Retornar 500 para retry
return jsonify({'error': 'Internal Server Error'}), 500// PHP
<?php
// webhook.php
$payload = json_decode(file_get_contents('php://input'), true);
$event = $payload['event'];
$data = $payload['data'];
try {
// Processar o evento
error_log("Recebido evento: $event");
error_log("Documento: " . $data['key']);
processDocument($event, $data);
// Retornar 200
http_response_code(200);
echo json_encode(['received' => true]);
} catch (Exception $e) {
error_log("Erro: " . $e->getMessage());
// Retornar 500 para retry
http_response_code(500);
echo json_encode(['error' => 'Internal Server Error']);
}Dead Letter Queue
Quando um webhook falha após todas as tentativas, ele é movido para a Dead Letter Queue (DLQ).
O que é a DLQ?
A DLQ armazena webhooks que falharam persistentemente, permitindo:
- 📊 Monitoramento de falhas
- 🔄 Reprocessamento manual
- 🐛 Debug de problemas
- 📈 Métricas de confiabilidade
Tabela: failed_webhooks
failed_webhooksCREATE TABLE failed_webhooks (
id UUID PRIMARY KEY,
webhook_id UUID,
client_id UUID,
payload JSON,
last_error TEXT,
attempts INT,
created_at TIMESTAMP,
updated_at TIMESTAMP
);Comandos Artisan
Ver Webhooks Falhados
# Lista todos os webhooks falhados
php artisan queue:failedReprocessar Webhooks
# Reprocessar webhooks falhados (limite de 100)
php artisan webhooks:retry-failed --limit=100
# Reprocessar um webhook específico
php artisan queue:retry {job_id}Limpar DLQ
# Limpar webhooks com mais de 30 dias
php artisan webhooks:prune-dlq --days=30
# Limpar todos os webhooks falhados
php artisan queue:flushMonitoramento da DLQ
É importante monitorar a DLQ regularmente:
# Criar um cron job para alertar sobre falhas
0 */6 * * * php /path/to/artisan webhooks:check-dlq --alert-if-more-than=10Segurança e Verificação
Verificar Origem (Planejado)
O Document Hub planeja implementar assinatura HMAC para verificar que a requisição veio realmente do Document Hub:
# Header que será incluído (futuro)
X-DocumentHub-Signature: sha256=abc123...Implementação de verificação (quando disponível):
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const hash = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return `sha256=${hash}` === signature;
}
// Uso
const signature = req.headers['x-documenthub-signature'];
const isValid = verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}Melhores Práticas de Segurança
-
Use HTTPS
# ✅ Bom https://seu-dominio.com/webhooks # ❌ Ruim (apenas para testes locais) http://seu-dominio.com/webhooks -
Valide o Payload
if (!payload.event || !payload.data) { return res.status(400).json({ error: 'Invalid payload' }); } -
Use um Endpoint Dedicado
# ✅ Bom: Endpoint específico https://api.seu-dominio.com/webhooks/documenthub # ❌ Ruim: Endpoint genérico exposto https://api.seu-dominio.com/api/receive -
Limite Taxa de Processamento
// Implementar rate limiting no seu endpoint const rateLimit = require('express-rate-limit'); const limiter = rateLimit({ windowMs: 1 * 60 * 1000, // 1 minuto max: 100 // máx 100 requisições por minuto }); app.post('/webhooks/documents', limiter, webhookHandler); -
Registre Tudo (Logging)
console.log({ timestamp: new Date(), event: payload.event, document_key: payload.data.key, attempt: payload.metadata.attempt, ip: req.ip });
Casos de Uso
Caso 1: Sincronização com Data Warehouse
app.post('/webhooks/documents', async (req, res) => {
const { event, data } = req.body;
if (event === 'document.created' || event === 'document.updated') {
// Sincronizar com data warehouse
await dataWarehouse.upsert({
table: `${data.context}_${data.type}`,
key: data.key,
values: data.data
});
}
res.status(200).json({ received: true });
});Caso 2: Notificação no Slack
app.post('/webhooks/documents', async (req, res) => {
const { event, data } = req.body;
if (event === 'document.created' && data.type === 'invoice') {
await slack.postMessage({
channel: '#invoices',
text: `Nova fatura criada: ${data.key} - R$ ${data.data.amount}`
});
}
res.status(200).json({ received: true });
});Caso 3: Trigger de Workflow
app.post('/webhooks/documents', async (req, res) => {
const { event, data } = req.body;
if (event === 'document.updated' &&
data.type === 'order' &&
data.data.status === 'paid') {
// Iniciar workflow de processamento de pedido
await workflow.start('process_order', {
order_id: data.key,
customer_id: data.data.customer_id
});
}
res.status(200).json({ received: true });
});Caso 4: Auditoria Centralizada
app.post('/webhooks/documents', async (req, res) => {
const { event, data, metadata } = req.body;
// Salvar em log de auditoria
await auditLog.create({
event_type: event,
resource_type: 'document',
resource_id: data.id,
document_key: data.key,
context: data.context,
type: data.type,
changes: data,
timestamp: metadata.sent_at
});
res.status(200).json({ received: true });
});Boas Práticas
1. Retorne 200 Rapidamente
// ✅ Bom: Processa assincronamente
app.post('/webhooks', (req, res) => {
// Retorna 200 imediatamente
res.status(200).json({ received: true });
// Processa depois
processWebhook(req.body);
});
// ❌ Ruim: Processa sincronamente
app.post('/webhooks', async (req, res) => {
// Demora muito, pode dar timeout
await heavyProcessing(req.body);
res.status(200).json({ received: true });
});2. Seja Idempotente
Webhooks podem ser entregues mais de uma vez. Prepare-se para isso:
app.post('/webhooks', async (req, res) => {
const { event, data } = req.body;
// Verificar se já processou
const exists = await processedWebhooks.find(data.id);
if (exists) {
console.log('Webhook já processado, ignorando');
return res.status(200).json({ received: true });
}
// Processar
await processDocument(data);
// Marcar como processado
await processedWebhooks.create({ id: data.id });
res.status(200).json({ received: true });
});3. Use Filas
// ✅ Bom: Coloca em fila para processar depois
app.post('/webhooks', async (req, res) => {
await queue.enqueue('process_webhook', req.body);
res.status(200).json({ received: true });
});4. Monitore Falhas
app.post('/webhooks', async (req, res) => {
try {
await processWebhook(req.body);
// Registrar sucesso
await metrics.increment('webhook.success');
res.status(200).json({ received: true });
} catch (error) {
// Registrar falha
await metrics.increment('webhook.failure');
await logger.error('Webhook failed', { error, payload: req.body });
res.status(500).json({ error: 'Internal Server Error' });
}
});5. Teste Localmente
Use ferramentas como ngrok para testar webhooks localmente:
# 1. Instale ngrok
npm install -g ngrok
# 2. Exponha sua porta local
ngrok http 3000
# 3. Use a URL gerada como webhook
https://abc123.ngrok.io/webhooks/documentsTroubleshooting
Webhooks Não Estão Sendo Recebidos
Possíveis causas:
-
Webhook está inativo
# Verificar status curl -X GET "https://api.../webhooks/{ID}" \ -H "Authorization: Bearer {TOKEN}" # Ativar curl -X PUT "https://api.../webhooks/{ID}" \ -H "Authorization: Bearer {TOKEN}" \ -d '{"status": "active"}' -
URL incorreta ou inacessível
# Testar manualmente curl -X POST "https://sua-url.com/webhook" \ -H "Content-Type: application/json" \ -d '{"test": true}' -
Firewall bloqueando
- Verifique se seu servidor aceita requisições do Document Hub
-
Workers de fila não estão rodando
# Verificar workers php artisan queue:work --verbose
Webhooks Falhando Constantemente
-
Endpoint retornando erro
- Verifique logs do seu servidor
- Certifique-se de retornar 200
-
Timeout
- Seu endpoint demora mais de 10 segundos?
- Processe assincronamente
-
SSL inválido
# Verificar certificado SSL curl -v https://sua-url.com/webhook
Ver Logs de Webhooks
# Logs do Laravel
tail -f storage/logs/laravel.log | grep webhook
# Ver jobs falhados
php artisan queue:failedUpdated 3 days ago
