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

MintPDF

HTML y Markdown → PDF, como API REST y servidor MCP.

Sin editor de plantillas. Sin IDs de plantilla. Sin panel de control. Sin registro para probar.

mintpdf.dev

Pruébalo · Configuración MCP · API · Autoalojamiento · Seguridad · Limitaciones


Envía HTML o Markdown y recibe un PDF. Por debajo usa Chromium, 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 salga con aspecto de documento en lugar de archivo de texto.

  • La paginación es el punto. break-inside, repetición de thead, líneas huérfanas y viudas, y encabezados y pies de página que de verdad heredan tu estilo. Ver Cómo funciona.
  • MCP nativogenerate_pdf y pdf_from_url sobre HTTP transmisible, para que un agente pueda generar un documento en mitad de una conversación.
  • Los documentos no se guardan — los archivos generados se eliminan después de una hora y su contenido nunca se registra. Las claves, los correos y los contadores de uso sí se almacenan, obviamente. Ver 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

¿Necesitas más de 10 renderizados al día? Una clave gratuita (solo correo, 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

Después 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."

HerramientaEntradaDevuelve
generate_pdfhtml o markdown, más opcionesURL de descarga, válida 1 hora
pdf_from_urlurl (http/https público), más opcionesURL de descarga, válida 1 hora

API

POST /v1/pdf

El cuerpo acepta exactamente una fuente, más opciones:

CampoTipoNotas
htmlstringDocumento completo o fragmento
markdownstringRenderizado con la hoja de estilos predeterminada
urlstringPágina pública a renderizar. Las direcciones privadas/internas están bloqueadas
formatstringA4 (predeterminado), Letter, Legal, A3, A5
landscapebooleanpredeterminado false
marginstringtodos los lados, p. ej. "18mm"
headerText / footerTextstringtexto pequeño en cada página
pageNumbersbooleanañade 3 / 7 al pie de página
output"pdf" | "url"el 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. Las mismas capacidades que la API REST.

Postman

Una colección ya preparada que cubre todos los endpoints y opciones 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 guarda automáticamente en la variable de la colección, así que el resto de la colección funciona de inmediato.

Límites

NivelLímitePrecio
Anónimo10 renderizados/día por IPgratis, sin registro
Clave gratuita100 renderizados/mesgratis, solo correo
Solo3.000 renderizados/mes19 $/mes
Equipo12.000 renderizados/mes49 $/mes
Escala50.000 renderizados/mes129 $/mes

Autoalojamiento

MintPDF tiene licencia MIT; ejecuta el tuyo propio si lo prefieres.

npm install
npm run build
npm start                # http://localhost:3000
node dist/smoke.js       # end-to-end render check

O descarga 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 compilan y publican mediante CI en cada cambio, y cada una se somete a 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. Compilarlo tú mismo también funciona:

docker build -t mintpdf .
docker run -p 3000:3000 -e BASE_URL=http://localhost:3000 mintpdf

Entorno: BASE_URL (se usa en los enlaces de descarga), DATA_DIR (predeterminado /tmp/mintpdf; monta un volumen para conservar las 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, así que la memoria limita la concurrencia mucho antes que la CPU o el coste. 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 compuerta dejaron 30 procesos 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 de inmediato con 503 y un Retry-After en lugar de permitir que se acumule. Rechazar a algunos solicitantes en menos de un segundo es estrictamente mejor que servir a todos un tiempo de espera agotado.

Los valores predeterminados del código son conservadores (3 y 20). Medidos en una instancia pequeña de Railway a 10 y 70:

RáfagaAtendidosRechazadosMedianaTiempo totalInstancia después
454501,5 s2,3 ssaludable
12081393,0 s4,0 ssaludable
250801703,4 s4,4 ssaludable, página de inicio 0,24 s

Aproximadamente 18 renderizados por segundo sostenidos, con la parte rechazada respondida en menos de 2,7 s. Sube 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 Office, fusión, división), Gotenberg es excelente y hace más que esto.

Seguridad

Este servicio renderiza HTML y URLs suministradas por cualquiera, así que las preguntas interesantes son sobre qué puede alcanzar ese contenido.

El HTML enviado ejecuta JavaScript. Tiene que hacerlo: los diagramas Mermaid y las matemáticas KaTeX se renderizan en la página. Trata el renderizador como si ejecutara código no confiable, por eso las restricciones de red siguientes importan más de lo que importarían para un conversor estático.

El SSRF está bloqueado en dos capas.

  1. Un url enviado se analiza, se restringe a http/https y se resuelve. Si cualquier dirección resuelta es privada, la solicitud se rechaza con un 400 antes de que intervenga un navegador.
  2. De forma independiente, cada solicitud que Chromium hace 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 comprobación existe para cerrar. Los nombres no resolubles fallan de forma cerrada.

Para ser precisos sobre lo que esto logra y no logra: un intento de DNS-rebinding ya no puede esperar a que caduque una aprobación en caché, así 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. Eliminarla por completo significa fijar la dirección resuelta en la capa de socket, lo cual no está implementado.

Bloqueados: loopback, 0.0.0.0, RFC1918, CGNAT (100.64/10), link-local y metadatos de nube (169.254.169.254); loopback IPv6, no especificada, link-local, site-local, unique-local, multicast, NAT64 y Teredo; formas IPv4-mapped e IPv4-compatible en cualquiera de las dos grafías, así 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 todos los esquemas excepto http, https, data y blob.

Las direcciones se juzgan por sus bytes en lugar de por coincidencia de texto, porque la misma dirección tiene muchas grafías y una coincidencia de texto atrapa una y se pierde el resto.

Hay una suite de pruebas exactamente para esto, y está pensada para ejecutarse en lugar de confiarse en ella:

BASE=https://mintpdf.dev node scratchpad/ssrf_suite.mjs

Comprueba los bypass anteriores y que el renderizado ordinario sigue funcionando, porque una protección que también bloquea las fuentes web es un error diferente, no 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, así que el HTML y el Markdown que envías no se escriben en ningún sitio excepto en el archivo temporal. La tabla de analíticas almacena el tipo de evento, la ruta, el referrer, el país y un hash con sal diaria de la IP. Sin contenido de documentos y sin forma de reconstruir un documento a partir de ello.

Limitaciones

Conviene saberlo antes de construir sobre esto.

  • Los archivos se eliminan después de una hora. No hay biblioteca de documentos ni forma de obtener un renderizado de nuevo más tarde. Genera, usa, listo. Si necesitas permanencia, guarda los bytes en tu lado.
  • Sin formatos Office, fusión ni 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 sola instancia. Las claves y las cuotas viven en SQLite en un volumen montado, así que ejecutar varias réplicas contra un solo volumen no funcionará. El escalado horizontal necesita una base de datos real primero.
  • Los renderizados están sujetos a control de admisión. Por encima de la capacidad, la API devuelve 503 con Retry-After en lugar de poner en cola sin límite. Ver la tabla anterior para el comportamiento medido.
  • Dos días de antigüedad al momento de escribir esto, sin usuarios de pago todavía.

Cómo funciona

TypeScript, Fastify y Puppeteer manejando un Chromium compartido con una página por solicitud. node:sqlite contiene claves, cuotas y eventos, así que no hay dependencias nativas que compilar.

Las partes que requirieron el trabajo real son las poco glamurosas:

  • CSS de impresión. break-inside: avoid en 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: avoid en los 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 nulo a menos que los estilos estén en línea, y quedan fuera de los márgenes del contenido.
  • Control de admisión, porque una pestaña de Chromium por solicitud concurrente es como el contenedor se queda sin memoria.
  • Aislamiento de red para un renderizador que ejecuta JavaScript no confiable. Ver Seguridad.

Licencia

MIT — ver LICENSE.