MintPDF
Convierte Markdown o HTML en un PDF con estilo, o renderiza cualquier página web pública, y recibe un enlace de descarga.
Documentación
MintPDF
HTML y Markdown → PDF, como API REST y servidor MCP.
Sin editor de plantillas. Sin IDs de plantillas. Sin panel de control. Sin registro para probar.
Pruébalo · Configuración MCP · API · Autoalojamiento · Seguridad · Limitaciones
Envía HTML o Markdown, recibe un PDF. Utiliza Chromium internamente, con el CSS de impresión ya resuelto para que las tablas no se dividan entre páginas, los encabezados de tabla se repitan y el Markdown se vea como un documento en lugar de un archivo de texto.
- La paginación es el punto.
break-inside, repetición dethead, huérfanas y viudas, y encabezados y pies de página que realmente heredan tu estilo. Consulta Cómo funciona. - MCP nativo —
generate_pdfypdf_from_urlsobre HTTP transmisible, para que un agente pueda producir un documento en medio de una conversación. - Los documentos no se conservan — los archivos renderizados se eliminan después de una hora, y su contenido nunca se registra. Las claves, correos electrónicos y contadores de uso sí se almacenan. Consulta Seguridad.
- Ejecútalo tú mismo — MIT, con una imagen publicada. El servicio alojado existe para que no tengas que operar Chromium, no porque el renderizador sea secreto.
Inicio rápido
Sin registro, sin clave:
curl -X POST https://mintpdf.dev/v1/pdf \
-H "Content-Type: application/json" \
-d '{"markdown":"# Invoice #42\n\n| Item | Price |\n|---|---|\n| Widget | $9.00 |","pageNumbers":true}' \
--output invoice.pdf
¿Quieres más de 3 renderizados al día? Una clave gratuita (solo correo electrónico, sin tarjeta) lo eleva a 100 al mes:
curl -X POST https://mintpdf.dev/v1/keys \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com"}'
# → {"key":"pm_…","daily_limit":100} # 100 renders per month
Luego envía Authorization: Bearer pm_… con tus solicitudes.
Úsalo desde Claude (o cualquier cliente MCP)
{
"mcpServers": {
"mintpdf": {
"command": "npx",
"args": ["-y", "mintpdf-mcp"]
}
}
}
¿Prefieres el endpoint alojado directamente? Usa mcp-remote en su lugar:
{
"mcpServers": {
"mintpdf": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://mintpdf.dev/mcp"]
}
}
}
Reinicia tu cliente y simplemente pide:
"Resume este hilo como un informe de una página con números de página y dame un PDF."
| Herramienta | Entrada | Devuelve |
|---|---|---|
generate_pdf | html o markdown, más opciones | URL de descarga, válida 1 hora |
pdf_from_url | url (http/https público), más opciones | URL de descarga, válida 1 hora |
API
POST /v1/pdf
El cuerpo acepta exactamente una fuente, más opciones:
| Campo | Tipo | Notas |
|---|---|---|
html | string | Documento completo o fragmento |
markdown | string | Renderizado con la hoja de estilos predeterminada |
url | string | Página pública para renderizar. Las direcciones privadas/internas están bloqueadas |
format | string | A4 (predeterminado), Letter, Legal, A3, A5 |
landscape | boolean | predeterminado false |
margin | string | todos los lados, p. ej. "18mm" |
headerText / footerText | string | texto pequeño en cada página |
pageNumbers | boolean | añade 3 / 7 al pie de página |
output | "pdf" | "url" | predeterminado devuelve bytes PDF; "url" devuelve JSON con un enlace |
POST /v1/keys
{"email":"you@example.com"} → una clave gratuita. Sin tarjeta, sin bucle de verificación.
POST /mcp
Endpoint MCP de HTTP transmisible, sin estado. Mismas capacidades que la API REST.
Postman
Una colección lista para usar que cubre cada endpoint y opción está en
postman/. Impórtala por enlace:
https://raw.githubusercontent.com/TrendTweekers/mintpdf/main/postman/mintpdf.postman_collection.json
La primera solicitud se ejecuta sin clave alguna, y obtener una clave gratuita la almacena en la variable de la colección automáticamente, por lo que el resto de la colección funciona de inmediato.
Límites
| Nivel | Límite | Precio |
|---|---|---|
| Anónimo | 3 renderizados/día por IP | gratis, sin registro |
| Clave gratuita | 100 renderizados/mes | gratis, solo correo electrónico |
| Solo | 3,000 renderizados/mes | $19/mes |
| Equipo | 12,000 renderizados/mes | $49/mes |
| Escala | 50,000 renderizados/mes | $129/mes |
Autoalojamiento
MintPDF tiene licencia MIT; ejecuta el tuyo propio si prefieres.
npm install
npm run build
npm start # http://localhost:3000
node dist/smoke.js # end-to-end render check
O extrae la imagen publicada, que incluye Chromium y las fuentes integradas:
docker run -p 3000:3000 \
-e BASE_URL=http://localhost:3000 \
-e DATA_DIR=/data -v mintpdf-data:/data \
ghcr.io/trendtweekers/mintpdf:latest
Entonces es la misma API en tu propia máquina, sin límites y sin que nada salga de ella:
curl -X POST http://localhost:3000/v1/pdf \
-H "Content-Type: application/json" \
-d '{"markdown":"# Local","pageNumbers":true}' --output local.pdf
Las imágenes se construyen y publican mediante CI en cada cambio, y cada una se prueba con una prueba de humo iniciando el
contenedor y renderizando un PDF real antes de etiquetarla. Las etiquetas son latest y el SHA corto
del commit. Construirlo tú mismo también funciona:
docker build -t mintpdf .
docker run -p 3000:3000 -e BASE_URL=http://localhost:3000 mintpdf
Entorno: BASE_URL (usado en enlaces de descarga), DATA_DIR (predeterminado a /tmp/mintpdf; monta un
volumen para persistir claves), ANON_DAILY_LIMIT, FREE_MONTHLY_LIMIT, SOLO_MONTHLY_LIMIT,
TEAM_MONTHLY_LIMIT, SCALE_MONTHLY_LIMIT, OVERAGE_FACTOR, RENDER_CONCURRENCY,
RENDER_QUEUE, RENDER_QUEUE_WAIT_MS.
Control de carga y admisión
Cada renderizado es una pestaña de Chromium, por lo que la memoria limita la concurrencia mucho antes que la CPU o el costo. Sin límite, un pico de tráfico abre una pestaña por solicitud hasta que el reaper de OOM mata el contenedor y cada solicitud falla, incluidas las casi terminadas. Medido aquí: 30 renderizados concurrentes sin puerta dejaron 30 procesos de Chrome huérfanos y una máquina inutilizable.
RENDER_CONCURRENCY renderizados se ejecutan a la vez, RENDER_QUEUE más pueden esperar, y cualquier cosa más allá se
rechaza inmediatamente con 503 y un Retry-After en lugar de permitir que se acumulen. Rechazar
a algunos llamadores en menos de un segundo es estrictamente mejor que servir a todos un tiempo de espera.
Los valores predeterminados del código son conservadores (3 y 20). Medido en una instancia pequeña de Railway a 10 y 70:
| Ráfaga | Servidos | Rechazados | Mediana | Tiempo total | Instancia después |
|---|---|---|---|---|---|
| 45 | 45 | 0 | 1.5s | 2.3s | saludable |
| 120 | 81 | 39 | 3.0s | 4.0s | saludable |
| 250 | 80 | 170 | 3.4s | 4.4s | saludable, página de inicio 0.24s |
Aproximadamente 18 renderizados por segundo sostenidos, con la parte rechazada respondida en menos de 2.7s. Aumenta los números solo contra una medición en tu propio tamaño de instancia, nunca por esperanza.
Si quieres una cadena de herramientas PDF autoalojada más completa (formatos de Office, fusión, división), Gotenberg es excelente y hace más que esto.
Seguridad
Este servicio renderiza HTML y URLs suministradas por cualquiera, por lo que las preguntas interesantes son sobre qué puede alcanzar ese contenido.
El HTML enviado ejecuta JavaScript. Tiene que hacerlo: los diagramas de Mermaid y las matemáticas de KaTeX se renderizan en la página. Trata al renderizador como si ejecutara código no confiable, por eso las restricciones de red a continuación importan más de lo que lo harían para un convertidor estático.
El SSRF está bloqueado en dos capas.
- Un
urlenviado se analiza, se restringe ahttp/https, y se resuelve. Si cualquier dirección resuelta es privada, la solicitud se rechaza con un 400 antes de que intervenga un navegador. - Independientemente, cada solicitud que hace Chromium se intercepta y el destino se resuelve de nuevo en el
momento de la solicitud, y luego se bloquea si es privado. Esto cubre imágenes incrustadas, hojas de estilo, fuentes,
redirecciones y
fetch()del JavaScript enviado, no solo la URL que pediste.
La segunda capa resuelve en lugar de confiar en el nombre de host, y almacena en caché solo rechazos, nunca aprobaciones: almacenar en caché "este host es público" reabriría exactamente el agujero que la verificación existe para cerrar. Los nombres no resolubles fallan de manera cerrada.
Ser precisos sobre lo que eso logra y no logra: un intento de rebinding de DNS ya no puede esperar una aprobación en caché, por lo que tiene que ganar una carrera entre esta búsqueda y la propia de Chromium, en cada solicitud. Ese es un objetivo mucho más estrecho que una ventana fija, pero es una carrera estrechada en lugar de una puerta cerrada. Eliminarlo por completo significa fijar la dirección resuelta en la capa de socket, lo cual no está implementado.
Bloqueado: loopback, 0.0.0.0, RFC1918, CGNAT (100.64/10), link-local y metadatos de nube
(169.254.169.254); loopback IPv6, no especificado, link-local, site-local, unique-local, multicast,
NAT64 y Teredo; formas IPv4-mapeadas e IPv4-compatibles en cualquier ortografía, por lo que ::ffff:10.0.0.1
y ::ffff:a00:1 son la misma dirección y ambas se rechazan; nombres localhost/.local/.internal;
cualquier nombre de host público que resuelva a una dirección privada; y cada esquema excepto http, https,
data y blob.
Las direcciones se juzgan por sus bytes en lugar de coincidir texto, porque la misma dirección tiene muchas ortografías y una coincidencia de texto atrapa una y pierde el resto.
Hay una suite de pruebas exactamente para esto, y está pensada para ejecutarse en lugar de confiarse:
BASE=https://mintpdf.dev node scratchpad/ssrf_suite.mjs
Verifica los bypasses anteriores y que el renderizado ordinario aún funcione, porque un guardia que también bloquea fuentes web es un error diferente en lugar de una corrección.
El analizador IPv6 tiene su propia tabla de literales adversariales, ya que una cadena inválida que silenciosamente se convierte en una dirección válida es el fallo que importa en este tipo de código:
node scratchpad/ipv6_table_test.mjs
Los enlaces de descarga usan un identificador crypto.randomUUID() y no están autenticados: cualquiera con
el enlace puede obtener el archivo durante la hora que existe. Eso es deliberado, para que un enlace pueda enviarse por correo o
entregarse a un navegador, pero significa que el enlace es el secreto.
Registro. Los metadatos de solicitud se registran (método, ruta, estado, duración). Los cuerpos de solicitud nunca se registran, por lo que el HTML y Markdown que envías no se escriben en ningún lugar excepto en el archivo temporal. La tabla de análisis almacena tipo de evento, ruta, referente, país y un hash salado diario de la IP. Sin contenido de documento, y sin forma de reconstruir un documento a partir de ello.
Limitaciones
Vale la pena saberlo antes de construir sobre ello.
- Los archivos se eliminan después de una hora. No hay biblioteca de documentos ni forma de obtener un renderizado nuevamente más tarde. Genera, usa, listo. Si necesitas permanencia, guarda los bytes en tu lado.
- Sin formatos de Office, fusión o división. Esto convierte HTML, Markdown y páginas web, y nada más. Gotenberg es más maduro y cubre mucho más terreno si te autoalojas y lo necesitas.
- Una instancia. Las claves y cuotas viven en SQLite en un volumen montado, por lo que ejecutar varias réplicas contra un volumen no funcionará. El escalado horizontal necesita una base de datos real primero.
- Los renderizados están controlados por admisión. Sobre capacidad, la API devuelve
503conRetry-Afteren lugar de poner en cola sin límite. Consulta la tabla anterior para el comportamiento medido. - Dos días de antigüedad al momento de escribir esto, sin usuarios de pago aún.
Cómo funciona
TypeScript, Fastify y Puppeteer manejando un Chromium compartido con una página por solicitud. node:sqlite
contiene claves, cuotas y eventos, por lo que no hay dependencias nativas que construir.
Las partes que tomaron el trabajo real son las poco glamorosas:
- CSS de impresión.
break-inside: avoiden tablas, filas, elementos de lista, bloques de código, blockquotes y figuras;thead { display: table-header-group }para que los encabezados se repitan;orphans/widows;break-after: avoiden encabezados para que ninguno quede varado al pie de una página. - Plantillas de encabezado y pie de página, que son un documento separado de tu página: ignoran el CSS de la página y se renderizan a un tamaño de fuente casi cero a menos que los estilos estén en línea, y están fuera de los márgenes de contenido.
- Control de admisión, porque una pestaña de Chromium por solicitud concurrente es cómo el contenedor se queda sin memoria.
- Aislamiento de red para un renderizador que ejecuta JavaScript no confiable. Consulta Seguridad.
Licencia
MIT — consulta LICENSE.