Webhooks
Processamento assíncrono de PDFs com callbacks automáticos
Webhooks
Webhooks permitem processamento assíncrono de PDFs. Quando o PDF estiver pronto, o RenderHub envia um callback HTTP para sua aplicação.
Quando Usar Webhooks
✅ Use webhooks quando:
- PDF leva mais de 5 segundos para gerar
- Processa muitos PDFs em lote
- Quer melhorar experiência do usuário (não bloquear UI)
- Precisa lidar com timeouts de proxy/gateway
❌ Não use webhooks quando:
- PDF é simples e rápido (<2 segundos)
- Precisa do PDF imediatamente na resposta
- Não pode expor endpoint HTTP público
Como Funciona
- Requisição inicial com
webhook_url - Resposta imediata com
job_ide statusQUEUED - Processamento assíncrono em background
- Callback HTTP POST para sua URL quando pronto
Cliente RenderHub Seu Servidor
| | |
|--POST /render--------->| |
| webhook_url | |
| | |
|<--200 OK---------------| |
| job_id, QUEUED | |
| | |
| |--[processando] |
| | |
| |--POST /webhook-------->|
| | pdf_url, job_id |
| | |
| |<--200 OK---------------|
Requisição com Webhook
Parâmetros Adicionais
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
webhook_url | string | ✅ Sim | URL para receber callback |
webhook_method | string | ❌ Não | Método HTTP: POST (padrão), PUT |
webhook_headers | object | ❌ Não | Headers customizados para o callback |
Exemplo: Node.js
const response = await axios.post('https://renderhub.com/api/v1/render', {
mode: 'CONVERT',
input_type: 'html',
data: largeHtmlContent,
// Configuração do webhook
webhook_url: 'https://meuapp.com/api/webhooks/pdf-ready',
webhook_method: 'POST',
webhook_headers: {
'X-Custom-Header': 'valor',
'Authorization': 'Bearer token_do_meu_app'
}
}, {
headers: {
'Authorization': `Bearer ${RENDERHUB_API_KEY}`
}
});
console.log('Job criado:', response.data.job_id);
// Não bloqueia - continua processando outras coisas
Exemplo: Python
import requests
response = requests.post('https://renderhub.com/api/v1/render',
headers={'Authorization': f'Bearer {API_KEY}'},
json={
'mode': 'CONVERT',
'input_type': 'html',
'data': large_html_content,
'webhook_url': 'https://meuapp.com/api/webhooks/pdf-ready',
'webhook_headers': {
'X-API-Key': 'meu_token_secreto'
}
}
)
job = response.json()
print(f"Job {job['job_id']} criado com sucesso")
Resposta Inicial
Quando você usa webhook, a resposta é imediata com status QUEUED:
{
"id": "render_7kj3h2g1k4j5h6",
"status": "QUEUED",
"job_id": "job_abc123xyz",
"webhook_url": "https://meuapp.com/api/webhooks/pdf-ready",
"estimated_time_ms": 3000,
"created_at": "2024-01-15T10:30:00Z"
}
Campos da Resposta
| Campo | Tipo | Descrição |
|---|---|---|
id | string | ID único da renderização |
status | string | Sempre QUEUED inicialmente |
job_id | string | ID do job para rastreamento |
webhook_url | string | URL que receberá o callback |
estimated_time_ms | number | Tempo estimado de processamento |
created_at | string | Timestamp ISO 8601 |
Payload do Callback
Quando o PDF estiver pronto, RenderHub faz POST para sua webhook_url:
Success (PDF Pronto)
{
"id": "render_7kj3h2g1k4j5h6",
"status": "DONE",
"job_id": "job_abc123xyz",
"pdf_url": "https://renderhub.com/api/v1/download/render_7kj3h2g1k4j5h6?token=xyz",
"pdf_expires_at": "2024-01-15T11:30:00Z",
"pages": 3,
"file_size_kb": 245,
"duration_ms": 2800,
"created_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:30:03Z"
}
Failure (Erro)
{
"id": "render_7kj3h2g1k4j5h6",
"status": "FAILED",
"job_id": "job_abc123xyz",
"error": "conversion_failed",
"message": "Falha ao converter HTML: CSS inválido",
"details": "Unexpected token '}' at line 15",
"created_at": "2024-01-15T10:30:00Z",
"failed_at": "2024-01-15T10:30:02Z"
}
Implementação do Endpoint
Node.js + Express
const express = require('express');
const axios = require('axios');
const fs = require('fs');
const app = express();
app.use(express.json());
app.post('/api/webhooks/pdf-ready', async (req, res) => {
const { id, status, job_id, pdf_url, error } = req.body;
console.log(`Webhook recebido para job ${job_id}: ${status}`);
if (status === 'DONE') {
try {
// Baixar PDF da URL temporária
const pdfResponse = await axios.get(pdf_url, {
responseType: 'arraybuffer'
});
// Salvar em arquivo ou S3/storage
const filename = `pdfs/${id}.pdf`;
fs.writeFileSync(filename, pdfResponse.data);
console.log(`✅ PDF salvo: ${filename}`);
// Atualizar status no banco de dados
await db.jobs.update(job_id, {
status: 'COMPLETED',
pdf_path: filename
});
// Notificar usuário (email, websocket, etc.)
await notifyUser(job_id, filename);
} catch (error) {
console.error('Erro ao baixar PDF:', error.message);
return res.status(500).json({ error: 'Failed to download PDF' });
}
} else if (status === 'FAILED') {
console.error(`❌ Erro ao gerar PDF: ${error}`);
await db.jobs.update(job_id, {
status: 'FAILED',
error_message: error
});
await notifyUser(job_id, null, error);
}
// IMPORTANTE: Retornar 200 OK
res.status(200).json({ received: true });
});
app.listen(3000);
Python + Flask
from flask import Flask, request, jsonify
import requests
import os
app = Flask(__name__)
@app.route('/api/webhooks/pdf-ready', methods=['POST'])
def pdf_ready_webhook():
data = request.json
job_id = data['job_id']
status = data['status']
print(f"Webhook recebido para job {job_id}: {status}")
if status == 'DONE':
pdf_url = data['pdf_url']
# Baixar PDF
response = requests.get(pdf_url)
filename = f"pdfs/{data['id']}.pdf"
with open(filename, 'wb') as f:
f.write(response.content)
print(f"✅ PDF salvo: {filename}")
# Atualizar banco de dados
db.jobs.update(job_id, status='COMPLETED', pdf_path=filename)
# Notificar usuário
notify_user(job_id, filename)
elif status == 'FAILED':
error = data['error']
print(f"❌ Erro ao gerar PDF: {error}")
db.jobs.update(job_id, status='FAILED', error=error)
notify_user(job_id, None, error)
# IMPORTANTE: Retornar 200 OK
return jsonify({'received': True}), 200
if __name__ == '__main__':
app.run(port=3000)
PHP
<?php
// webhook.php
$payload = json_decode(file_get_contents('php://input'), true);
$jobId = $payload['job_id'];
$status = $payload['status'];
error_log("Webhook recebido para job $jobId: $status");
if ($status === 'DONE') {
$pdfUrl = $payload['pdf_url'];
$id = $payload['id'];
// Baixar PDF
$pdfContent = file_get_contents($pdfUrl);
$filename = "pdfs/$id.pdf";
file_put_contents($filename, $pdfContent);
error_log("✅ PDF salvo: $filename");
// Atualizar banco de dados
$db->query("UPDATE jobs SET status='COMPLETED', pdf_path='$filename' WHERE job_id='$jobId'");
// Notificar usuário
notifyUser($jobId, $filename);
} elseif ($status === 'FAILED') {
$error = $payload['error'];
error_log("❌ Erro ao gerar PDF: $error");
$db->query("UPDATE jobs SET status='FAILED', error='$error' WHERE job_id='$jobId'");
notifyUser($jobId, null, $error);
}
// IMPORTANTE: Retornar 200 OK
http_response_code(200);
echo json_encode(['received' => true]);
?>
Segurança do Webhook
1. Validação de Assinatura
RenderHub envia um header X-RenderHub-Signature com HMAC SHA256:
const crypto = require('crypto');
app.post('/webhook', (req, res) => {
const signature = req.headers['x-renderhub-signature'];
const payload = JSON.stringify(req.body);
// Seu webhook secret (disponível no Dashboard)
const secret = process.env.WEBHOOK_SECRET;
// Calcular HMAC
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Validar assinatura
if (signature !== expectedSignature) {
console.error('❌ Assinatura inválida!');
return res.status(401).json({ error: 'Invalid signature' });
}
// Assinatura válida - processar webhook
// ...
});
2. Whitelist de IPs
Configure firewall para aceitar apenas IPs do RenderHub:
# IPs do RenderHub (exemplo)
52.1.2.3
54.5.6.7
3. HTTPS Obrigatório
// ✅ Correto
webhook_url: 'https://meuapp.com/webhook'
// ❌ Incorreto - será rejeitado
webhook_url: 'http://meuapp.com/webhook'
4. Token de Autenticação
Passe seu próprio token nos headers:
{
webhook_url: 'https://meuapp.com/webhook',
webhook_headers: {
'Authorization': 'Bearer meu_token_secreto'
}
}
// No servidor
app.post('/webhook', (req, res) => {
const token = req.headers['authorization'];
if (token !== `Bearer ${process.env.MY_SECRET_TOKEN}`) {
return res.status(401).send('Unauthorized');
}
// Processar...
});
Retry e Timeout
Retry do RenderHub
Se o webhook falhar, RenderHub tenta novamente:
| Tentativa | Delay | Status HTTP que causam retry |
|---|---|---|
| 1ª | Imediato | 5xx, timeout |
| 2ª | 5 segundos | 5xx, timeout |
| 3ª | 30 segundos | 5xx, timeout |
Após 3 tentativas, o webhook é marcado como FAILED.
Timeout
O endpoint deve responder em até 10 segundos. Se demorar mais:
- RenderHub marca como timeout
- Tentará novamente conforme tabela acima
// ✅ Bom: Processar de forma assíncrona
app.post('/webhook', async (req, res) => {
const { job_id, pdf_url } = req.body;
// Retornar 200 OK imediatamente
res.status(200).json({ received: true });
// Processar em background (não bloqueia resposta)
processInBackground(job_id, pdf_url);
});
async function processInBackground(job_id, pdf_url) {
// Baixar PDF
// Salvar no storage
// Notificar usuário
}
Monitoramento de Webhooks
Logs no Dashboard
Veja status de todos os webhooks no Dashboard → Webhooks:
- ✅ Entregues com sucesso
- ⏳ Pendentes/Fila
- 🔄 Retry em andamento
- ❌ Falharam após 3 tentativas
API de Status
Consulte status de um job específico:
GET /api/v1/jobs/{job_id}
Authorization: Bearer SUA_CHAVE_API
Resposta:
{
"job_id": "job_abc123",
"status": "DONE",
"webhook_status": "delivered",
"webhook_attempts": 1,
"webhook_last_attempt": "2024-01-15T10:30:03Z",
"pdf_url": "https://...",
"created_at": "2024-01-15T10:30:00Z"
}
Webhook Status
| Status | Descrição |
|---|---|
pending | Aguardando processamento |
sent | Enviado, aguardando resposta |
delivered | Entregue com sucesso (200 OK) |
failed | Falhou após 3 tentativas |
timeout | Endpoint não respondeu em 10s |
Testando Webhooks Localmente
1. ngrok (Recomendado)
# Iniciar ngrok
ngrok http 3000
# Usar URL pública gerada
Forwarding: https://abc123.ngrok.io -> localhost:3000
// Usar URL do ngrok
webhook_url: 'https://abc123.ngrok.io/webhook'
2. Webhook.site (Para testes)
Use webhook.site para ver payloads sem código:
webhook_url: 'https://webhook.site/sua-url-unica'
3. Servidor de Desenvolvimento
// server.js
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook', (req, res) => {
console.log('Webhook recebido:');
console.log(JSON.stringify(req.body, null, 2));
res.status(200).json({ ok: true });
});
app.listen(3000, () => {
console.log('Servidor rodando em http://localhost:3000');
});
Melhores Práticas
✅ Faça
-
Retorne 200 OK rapidamente
res.status(200).json({ received: true }); processInBackground(data); -
Valide assinatura HMAC
if (!validateSignature(req)) { return res.status(401).send('Invalid'); } -
Implemente idempotência
// Processar apenas uma vez if (await db.webhooks.exists(job_id)) { return res.status(200).json({ already_processed: true }); } -
Baixe PDF imediatamente (expira em 1 hora)
const pdf = await axios.get(pdf_url); await uploadToS3(pdf.data); -
Logue tudo
logger.info('Webhook received', { job_id, status });
❌ Não Faça
-
Não bloqueie a resposta
// ❌ Ruim const pdf = await downloadLargePDF(); // Demora 30s res.status(200).send(); -
Não ignore erros de retry
// ❌ Ruim res.status(500).send(); // Causa retry desnecessário -
Não processe duas vezes
// ❌ Ruim - pode duplicar await savePDF(job_id); -
Não confie apenas no webhook
// ✅ Bom: Também consulte API se webhook não chegar setTimeout(() => checkJobStatus(job_id), 60000);
Exemplo Completo: Sistema de Fila
const Queue = require('bull');
const axios = require('axios');
// Criar fila
const pdfQueue = new Queue('pdf-generation');
// Adicionar job à fila
async function generatePDF(data) {
const job = await pdfQueue.add({
mode: 'CONVERT',
input_type: 'html',
data: data.html
});
return { job_id: job.id, status: 'QUEUED' };
}
// Processar fila
pdfQueue.process(async (job) => {
// Chamar RenderHub com webhook
const response = await axios.post('https://renderhub.com/api/v1/render', {
...job.data,
webhook_url: 'https://meuapp.com/webhook',
webhook_headers: {
'X-Job-ID': job.id
}
});
return response.data;
});
// Receber webhook
app.post('/webhook', async (req, res) => {
const { status, pdf_url } = req.body;
const jobId = req.headers['x-job-id'];
res.status(200).json({ ok: true });
if (status === 'DONE') {
// Atualizar job
const job = await pdfQueue.getJob(jobId);
await job.finished();
// Baixar e armazenar PDF
const pdf = await axios.get(pdf_url, { responseType: 'arraybuffer' });
await saveToStorage(jobId, pdf.data);
}
});
Próximos Passos
- POST /render - Documentação completa da API
- Templates - API de gerenciamento de templates
- Códigos de Erro - Tratamento de erros
- Exemplos - Exemplos completos