Webhooks

📚 Índice

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:

EventoDescriçãoQuando é Disparado
document.createdDocumento criadoPOST /.../{key} cria novo documento
document.updatedDocumento atualizadoPUT /.../{key} atualiza documento
document.deletedDocumento deletadoDELETE /.../{key} deleta documento
document.readDocumento lidoGET /.../{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.read pode gerar muitas notificações (use com cuidado)

Criar Webhooks

Endpoint

POST /api/v1/client/{client}/webhooks

Headers

Authorization: Bearer {SEU_TOKEN}
Content-Type: application/json

Body da Requisição

{
  "url": "https://seu-dominio.com/webhook/documents",
  "events": ["document.created", "document.updated"],
  "status": "active"
}
CampoTipoObrigatórioDescrição
urlstringSimURL que receberá as notificações (deve ser HTTPS em produção)
eventsarraySimLista de eventos a escutar
statusstringNãoactive (padrão) ou inactive

Validações

  • url deve ser uma URL válida
  • url deve ser única por cliente (não pode cadastrar a mesma URL 2 vezes)
  • events deve conter pelo menos um evento válido
  • ✅ Cada evento em events deve 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}/webhooks

Exemplo

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-def123456789

Body 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

CampoTipoDescrição
eventstringNome do evento que disparou o webhook
dataobjectDados completos do documento
metadataobjectMetadados sobre a notificação
metadata.sourcestringSempre "DocumentHub"
metadata.versionstringVersão do formato do payload
metadata.sent_attimestampMomento do envio
metadata.attemptintegerNú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)

# 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

  1. Tentativa 1: Imediata (quando o evento ocorre)

    • Se sucesso (200-299): ✅ Fim
    • Se falha: Agenda retry
  2. Tentativa 2: Após 30 segundos

    • Se sucesso: ✅ Fim
    • Se falha: Agenda retry
  3. Tentativa 3: Após 120 segundos (2 minutos)

    • Se sucesso: ✅ Fim
    • Se falha: Agenda retry
  4. Tentativa 4: Após 300 segundos (5 minutos)

    • Se sucesso: ✅ Fim
    • Se falha: Agenda retry
  5. Tentativa 5: Após 600 segundos (10 minutos)

    • Se sucesso: ✅ Fim
    • Se falha: Move para Dead Letter Queue

Códigos de Status Considerados

FaixaComportamento
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

CREATE 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:failed

Reprocessar 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:flush

Monitoramento 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=10

Seguranç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

  1. Use HTTPS

    # ✅ Bom
    https://seu-dominio.com/webhooks
    
    # ❌ Ruim (apenas para testes locais)
    http://seu-dominio.com/webhooks
  2. Valide o Payload

    if (!payload.event || !payload.data) {
      return res.status(400).json({ error: 'Invalid payload' });
    }
  3. 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
  4. 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);
  5. 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/documents

Troubleshooting

Webhooks Não Estão Sendo Recebidos

Possíveis causas:

  1. 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"}'
  2. URL incorreta ou inacessível

    # Testar manualmente
    curl -X POST "https://sua-url.com/webhook" \
      -H "Content-Type: application/json" \
      -d '{"test": true}'
  3. Firewall bloqueando

    • Verifique se seu servidor aceita requisições do Document Hub
  4. Workers de fila não estão rodando

    # Verificar workers
    php artisan queue:work --verbose

Webhooks Falhando Constantemente

  1. Endpoint retornando erro

    • Verifique logs do seu servidor
    • Certifique-se de retornar 200
  2. Timeout

    • Seu endpoint demora mais de 10 segundos?
    • Processe assincronamente
  3. 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:failed