Gerar PDF a partir de HTML em Python
Guia · atualizado em 12/09/2026 · 8 min de leitura
Quer gerar PDF a partir de HTML em Python? Relatórios, notas, certificados e contratos ficam muito mais fáceis de manter quando o layout é um template HTML com CSS. Este guia compara as bibliotecas mais usadas e mostra o código completo com requests, FastAPI e Django, usando a API do PDF Services.
As opções em Python
| Opção | Como funciona | Pontos de atenção |
|---|---|---|
| WeasyPrint | Renderiza HTML e CSS de impressão em Python, sem navegador. | Ótimo suporte a CSS de impressão, mas depende de bibliotecas nativas (Pango), que complicam a instalação no Windows e em imagens mínimas. |
| xhtml2pdf | Conversor em Python puro. | Suporte limitado a CSS moderno: layouts mais elaborados costumam precisar de ajustes. |
| pdfkit (wkhtmltopdf) | Wrapper do executável wkhtmltopdf. | O wkhtmltopdf foi arquivado em 2023 e usa um WebKit antigo. Evite em projetos novos. |
| Playwright | Controla um Chromium headless e imprime a página. | Precisa instalar e manter o navegador; custo alto de memória em servidores e funções. |
| API HTTP (PDF Services) | Você envia o HTML e recebe o PDF. | Depende de rede e é cobrado por uso, sem dependências nativas no servidor. |
O PDF Services converte com o iText pdfHTML, um motor maduro de conversão de HTML e CSS para PDF, e todo o código do serviço é aberto sob a licença AGPL v3. Você não precisa instalar o iText, Java ou .NET: a conversão acontece na API.
Se você já usa WeasyPrint e ele atende, ótimo. A API faz mais sentido quando você não quer instalar bibliotecas do sistema, quando publica em ambientes serverless (AWS Lambda, Cloud Functions) ou quando prefere não ocupar o seu servidor com a renderização.
Passo a passo com requests
1. Crie a conta e a chave
Cadastre a empresa, confirme o e-mail e crie uma chave em Chaves de API no painel. Guarde-a em variável de ambiente ou em um gerenciador de segredos, nunca no repositório.
2. Envie o HTML e salve o PDF
Instale o requests (pip install requests) e rode:
import os
import requests
api_key = os.environ["PDFSERVICES_API_KEY"]
html = """<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<style>
@page { size: A4; margin: 2cm; }
body { font-family: sans-serif; }
</style>
</head>
<body><h1>Olá, PDF!</h1></body>
</html>"""
resposta = requests.post(
"https://api.pdfservices.sistema.site/v1/html2pdf/html",
headers={"X-Api-Key": api_key, "Content-Type": "text/html; charset=utf-8"},
data=html.encode("utf-8"),
timeout=60,
)
if not resposta.ok:
erro = resposta.json()
raise RuntimeError(f"Falha {resposta.status_code}: {erro['error']} - {erro['message']}")
with open("documento.pdf", "wb") as arquivo:
arquivo.write(resposta.content)
print("Cobrado: R$", resposta.headers["X-Charged-Amount"])
A resposta traz o PDF em application/pdf e os headers X-Charged-Amount (valor cobrado), X-Balance (saldo restante) e X-Transaction-Id. Sempre defina um timeout nas chamadas HTTP.
Codifique o HTML em UTF-8 ao enviar o corpo (html.encode("utf-8")) e declare o charset no Content-Type: assim acentos e cedilhas chegam intactos ao PDF.
Exemplo com FastAPI e Jinja2
Com FastAPI, use o httpx assíncrono para não bloquear o event loop, e o Jinja2 com autoescape para montar o HTML a partir de um template:
import os
import httpx
from fastapi import FastAPI, HTTPException
from fastapi.responses import Response
from jinja2 import Environment, FileSystemLoader, select_autoescape
app = FastAPI()
templates = Environment(loader=FileSystemLoader("templates"), autoescape=select_autoescape())
API_URL = "https://api.pdfservices.sistema.site/v1/html2pdf/json"
@app.get("/faturas/{numero}/pdf")
async def fatura_pdf(numero: str):
# O autoescape do Jinja2 impede que dados do usuário virem HTML no documento.
html = templates.get_template("fatura.html").render(numero=numero)
async with httpx.AsyncClient(timeout=60) as client:
resposta = await client.post(
API_URL,
json={"html": html},
headers={"X-Api-Key": os.environ["PDFSERVICES_API_KEY"]},
)
if resposta.status_code != 200:
raise HTTPException(status_code=502, detail=f"Falha ao gerar o PDF ({resposta.status_code}).")
return Response(
content=resposta.content,
media_type="application/pdf",
headers={"Content-Disposition": 'inline; filename="fatura.pdf"'},
)
Exemplo com Django
No Django, renderize o template normalmente com render_to_string, já com autoescape, e devolva o PDF em um HttpResponse:
import os
import requests
from django.http import HttpResponse
from django.template.loader import render_to_string
def fatura_pdf(request, numero):
html = render_to_string("fatura.html", {"numero": numero})
resposta = requests.post(
"https://api.pdfservices.sistema.site/v1/html2pdf/json",
json={"html": html},
headers={"X-Api-Key": os.environ["PDFSERVICES_API_KEY"]},
timeout=60,
)
resposta.raise_for_status()
return HttpResponse(resposta.content, content_type="application/pdf")
Você pode reaproveitar os templates e o CSS que já usa nas páginas do sistema. Basta um bloco <style> com as regras de @page.
CSS para PDF: tamanho da página, cabeçalho, rodapé e quebras
O motor de conversão entende CSS de impressão (paged media). É com ele que você controla o documento, sem nada específico de Python:
@page {
size: A4;
margin: 2.5cm 2cm;
@top-center { content: "Relatório mensal"; }
@bottom-right { content: "Página " counter(page) " de " counter(pages); }
}
.nova-pagina { page-break-before: always; }
/* Repete o cabeçalho da tabela em todas as páginas */
thead { display: table-header-group; }
- Tamanho e margens:
@page { size: A4; },A4 landscape,Letterou medidas como21cm 29.7cm. - Cabeçalho e rodapé: as caixas de margem (
@top-center,@bottom-rightetc.) comcounter(page)ecounter(pages)geram o "Página 1 de 3". - Quebras:
page-break-before,page-break-afterepage-break-inside: avoidpara não cortar blocos ao meio. - Imagens: embutidas como
data:URI ou por URL https pública, em JPG ou PNG. - Fontes: as padrão já cobrem acentos; para a sua fonte, use
@font-facecomdata:URI. Fontes por URL, como as do Google Fonts, não são baixadas. - JavaScript não é executado: monte o HTML final no servidor. Para gráficos, gere uma imagem PNG antes.
Tratamento de erros
Quando algo dá errado, a API responde com um JSON {"error": "...", "message": "..."}. Os casos que valem tratar no código:
| HTTP | Código | O que fazer |
|---|---|---|
| 401 | invalid_api_key | Confira a chave configurada; ela pode ter sido excluída no painel. |
| 402 | insufficient_balance | Saldo insuficiente: adicione créditos. Não adianta repetir a chamada. |
| 413 | payload_too_large | O corpo passou de 1 MB. Reduza imagens embutidas ou use URLs https. |
| 422 | conversion_failed | O HTML não pôde ser convertido. Revise o documento; a falha não é cobrada. |
| 503 | busy | Muitas chamadas simultâneas na mesma conta: repita com espera crescente. |
Com requests, um HTTPAdapter com Retry(total=3, backoff_factor=1, status_forcelist=[503], allowed_methods=["POST"]) resolve as novas tentativas. Repita só no status 503 e em falhas de rede: repetir 401, 402 ou 422 não muda o resultado.
Custos e privacidade
O PDF Services cobra R$ 0,001 por conversão bem-sucedida, com créditos pré-pagos e sem mensalidade: mil PDFs custam R$ 1,00. Conversões com erro não são cobradas.
O HTML enviado e o PDF gerado são processados só em memória e nunca são armazenados nem registrados em logs. Ficam registrados apenas metadados da transação, como data, tamanho e valor cobrado. Detalhes na Política de Privacidade. Todo o código do serviço é aberto, sob a licença AGPL v3, e pode ser auditado no GitHub.
Gere seu primeiro PDF agora
Crie a conta da sua empresa, gere uma chave no painel e rode o exemplo acima. Pelo painel também dá para testar a conversão antes de escrever uma linha de código.