Api reference

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

  1. Requisição inicial com webhook_url
  2. Resposta imediata com job_id e status QUEUED
  3. Processamento assíncrono em background
  4. 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âmetroTipoObrigatórioDescrição
webhook_urlstring✅ SimURL para receber callback
webhook_methodstring❌ NãoMétodo HTTP: POST (padrão), PUT
webhook_headersobject❌ NãoHeaders 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

CampoTipoDescrição
idstringID único da renderização
statusstringSempre QUEUED inicialmente
job_idstringID do job para rastreamento
webhook_urlstringURL que receberá o callback
estimated_time_msnumberTempo estimado de processamento
created_atstringTimestamp 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:

TentativaDelayStatus HTTP que causam retry
Imediato5xx, timeout
5 segundos5xx, timeout
30 segundos5xx, 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

StatusDescrição
pendingAguardando processamento
sentEnviado, aguardando resposta
deliveredEntregue com sucesso (200 OK)
failedFalhou após 3 tentativas
timeoutEndpoint 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

  1. Retorne 200 OK rapidamente

    res.status(200).json({ received: true });
    processInBackground(data);
  2. Valide assinatura HMAC

    if (!validateSignature(req)) {
      return res.status(401).send('Invalid');
    }
  3. Implemente idempotência

    // Processar apenas uma vez
    if (await db.webhooks.exists(job_id)) {
      return res.status(200).json({ already_processed: true });
    }
  4. Baixe PDF imediatamente (expira em 1 hora)

    const pdf = await axios.get(pdf_url);
    await uploadToS3(pdf.data);
  5. Logue tudo

    logger.info('Webhook received', { job_id, status });

❌ Não Faça

  1. Não bloqueie a resposta

    // ❌ Ruim
    const pdf = await downloadLargePDF(); // Demora 30s
    res.status(200).send();
  2. Não ignore erros de retry

    // ❌ Ruim
    res.status(500).send(); // Causa retry desnecessário
  3. Não processe duas vezes

    // ❌ Ruim - pode duplicar
    await savePDF(job_id);
  4. 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

On this page