<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gerardo Barrera</title>
    <description>The latest articles on DEV Community by Gerardo Barrera (@gerardo_barrera_538fc60b7).</description>
    <link>https://dev.to/gerardo_barrera_538fc60b7</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3993280%2F0297a7a0-d323-44a0-82c4-8a664f7f21e6.png</url>
      <title>DEV Community: Gerardo Barrera</title>
      <link>https://dev.to/gerardo_barrera_538fc60b7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gerardo_barrera_538fc60b7"/>
    <language>en</language>
    <item>
      <title>The 4 Best Free Resume Builders in 2026 (Tested &amp; Compared)</title>
      <dc:creator>Gerardo Barrera</dc:creator>
      <pubDate>Mon, 06 Jul 2026 20:01:56 +0000</pubDate>
      <link>https://dev.to/gerardo_barrera_538fc60b7/the-4-best-free-resume-builders-in-2026-tested-compared-7jk</link>
      <guid>https://dev.to/gerardo_barrera_538fc60b7/the-4-best-free-resume-builders-in-2026-tested-compared-7jk</guid>
      <description>&lt;p&gt;Search "free resume builder" and you'll get a wall of tools that all claim to be free. Then you spend an hour building a resume, hit &lt;strong&gt;Download&lt;/strong&gt;, and — surprise — it's $2.95 for a "3-day trial" or your PDF comes out plastered with a watermark.&lt;/p&gt;

&lt;p&gt;So I ran the test properly. For each tool I: created a resume from scratch, tried the templates, checked whether the output was ATS-friendly, and attempted to &lt;strong&gt;download a PDF without entering a credit card&lt;/strong&gt;. Below are the four that were &lt;em&gt;actually&lt;/em&gt; free, ranked by how little they made me fight for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested
&lt;/h2&gt;

&lt;p&gt;Same process for every tool, so the comparison is apples-to-apples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free PDF export&lt;/strong&gt; — can you download without paying?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watermark&lt;/strong&gt; — is the free output clean?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credit card&lt;/strong&gt; — required to finish or not?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ATS-friendly&lt;/strong&gt; — single-column, text-based, parseable output?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI help&lt;/strong&gt; — any writing/keyword assistance on the free tier?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the summary, then the details.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Free PDF&lt;/th&gt;
&lt;th&gt;Watermark&lt;/th&gt;
&lt;th&gt;Credit card&lt;/th&gt;
&lt;th&gt;ATS-friendly&lt;/th&gt;
&lt;th&gt;Free AI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FlowCV&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ none&lt;/td&gt;
&lt;td&gt;❌ no&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JobScoutly&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ none&lt;/td&gt;
&lt;td&gt;❌ no&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canva&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ none&lt;/td&gt;
&lt;td&gt;❌ no&lt;/td&gt;
&lt;td&gt;⚠️ design-heavy&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Docs&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ none&lt;/td&gt;
&lt;td&gt;❌ no&lt;/td&gt;
&lt;td&gt;✅ (with a plain template)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. FlowCV — best free plan overall
&lt;/h2&gt;

&lt;p&gt;FlowCV is the one I'd hand to a friend who just wants a clean resume with zero friction. Unlimited resumes, real PDF export, no watermark, no card. The templates are tasteful and default to single-column layouts that parse well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Genuinely free core, clean ATS-friendly output, no account gymnastics.&lt;br&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Some newer designs and extras sit behind a paid tier; AI help is limited.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. JobScoutly — best free AI + ATS matching
&lt;/h2&gt;

&lt;p&gt;JobScoutly earns this spot because the AI writing help and resume-to-job-description matching are &lt;strong&gt;free with no premium tier&lt;/strong&gt;, which is unusual. You paste a job description and it flags missing keywords and skills, and the templates are single-column and ATS-friendly by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; All features free (AI writing, ATS templates, keyword matching, unlimited downloads), no watermark, no card.&lt;br&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Newer product with a smaller template library than the design-first tools; requires a free account to save your work.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Canva — best for creative roles
&lt;/h2&gt;

&lt;p&gt;If you're a designer, a resume that &lt;em&gt;looks&lt;/em&gt; like you can design is a feature. Canva's free tier exports clean PDFs with no watermark, and the template variety is unmatched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Beautiful templates, free PDF export, great for portfolios and creative fields.&lt;br&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Most templates are multi-column and graphics-heavy — &lt;strong&gt;the opposite of ATS-friendly&lt;/strong&gt;. Great for a human, risky for a parser. Use it only when you know a person reviews first, or pick one of the plain single-column templates.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Google Docs — best for full control
&lt;/h2&gt;

&lt;p&gt;Not flashy, but honest: free forever, no watermark, and you control every pixel. Start from a plain template (skip the multi-column ones) and you get a text-based, parseable resume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Totally free, reliable export, full control, nothing hidden.&lt;br&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; No guidance, no AI, no ATS checks — you're on your own for content and formatting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "looks free but isn't" list
&lt;/h2&gt;

&lt;p&gt;Worth naming, because these eat the most hours: several popular builders (you know the ones — the ones advertising everywhere) let you build the whole resume for free, then gate the &lt;strong&gt;download&lt;/strong&gt; behind a subscription, sometimes via a cheap "trial" that auto-renews. That's not free; that's a funnel. If a tool won't let you export a clean PDF without a card, assume it's paid.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to pick
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Just want it done, clean, fast:&lt;/strong&gt; FlowCV.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want AI help + job-description matching for free:&lt;/strong&gt; JobScoutly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creative field, human reviewer:&lt;/strong&gt; Canva (single-column template).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want total control, no accounts:&lt;/strong&gt; Google Docs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whatever you choose, run the five-second test before you apply: &lt;strong&gt;copy-paste your exported PDF into a plain text file.&lt;/strong&gt; If it comes out scrambled or missing sections, an ATS will choke on it too — pick a more parseable template regardless of which tool made it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Which free builder have you actually shipped a resume from? Curious whether anyone's found one I missed that clears the "no card, no watermark" bar.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>productivity</category>
      <category>job</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Why HTML-to-PDF Breaks in Production (and What to Use Instead)</title>
      <dc:creator>Gerardo Barrera</dc:creator>
      <pubDate>Fri, 26 Jun 2026 02:55:12 +0000</pubDate>
      <link>https://dev.to/gerardo_barrera_538fc60b7/why-html-to-pdf-breaks-in-production-and-what-to-use-instead-3ip8</link>
      <guid>https://dev.to/gerardo_barrera_538fc60b7/why-html-to-pdf-breaks-in-production-and-what-to-use-instead-3ip8</guid>
      <description>&lt;p&gt;Almost every "generate a PDF" feature starts the same way.&lt;/p&gt;

&lt;p&gt;You already have HTML. You already have CSS. So you reach for the obvious move: render the page, screenshot it to PDF, ship it. Puppeteer, Playwright, wkhtmltopdf, a hosted "HTML to PDF API" — pick your flavor. In an afternoon you have an invoice coming out the other end and it looks fine.&lt;/p&gt;

&lt;p&gt;Then it goes to production. And "fine" slowly turns into a backlog of weird, hard-to-reproduce bugs.&lt;/p&gt;

&lt;p&gt;This is not an argument that HTML-to-PDF is useless. For a one-off export or an internal report, it's great. The argument is narrower: &lt;strong&gt;the moment PDF generation becomes a real, automated, customer-facing part of your product, "screenshot a web page" is the wrong abstraction&lt;/strong&gt; — and the failure modes are predictable enough to list in advance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core problem: a PDF is not a web page
&lt;/h2&gt;

&lt;p&gt;A browser renders for an infinite, scrollable, single-width viewport. A PDF is a stack of fixed, finite, printable pages.&lt;/p&gt;

&lt;p&gt;Those are different physics.&lt;/p&gt;

&lt;p&gt;HTML-to-PDF works by rendering your page in a headless browser and then slicing that continuous render into page-sized pieces. Everything that's hard about it comes from that one mismatch: you designed for a stream, and now you're forcing it into pages.&lt;/p&gt;

&lt;p&gt;Most of the bugs below are just that mismatch showing up in different costumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 1: pagination
&lt;/h2&gt;

&lt;p&gt;This is the big one.&lt;/p&gt;

&lt;p&gt;A browser has no concept of "page 2." So when your content is taller than one page, the engine has to guess where to cut — and it cuts wherever the pixel ruler lands.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a table row sliced in half across the page break&lt;/li&gt;
&lt;li&gt;a heading stranded alone at the bottom of a page, its content on the next&lt;/li&gt;
&lt;li&gt;a total row that floats away from the table it belongs to&lt;/li&gt;
&lt;li&gt;a signature block split from the line above it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CSS has &lt;code&gt;break-inside: avoid&lt;/code&gt;, &lt;code&gt;break-before&lt;/code&gt;, and friends — and they help. But support is uneven across engines, they interact badly with flex/grid, and you end up hand-tuning rules per document until it looks right &lt;em&gt;for the data you tested with&lt;/em&gt;. Which leads directly to the next problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 2: it passes review, then real data breaks it
&lt;/h2&gt;

&lt;p&gt;You test with "Acme Inc." and a two-line invoice. It's perfect. You ship.&lt;/p&gt;

&lt;p&gt;Then production sends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a customer name that's 90 characters long&lt;/li&gt;
&lt;li&gt;a line-item description that wraps to three lines&lt;/li&gt;
&lt;li&gt;an invoice with 47 items instead of 2&lt;/li&gt;
&lt;li&gt;a localized currency string twice as wide as &lt;code&gt;$&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;an address with a missing field, collapsing the layout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the logo overlaps the header. The table pushes the total onto a second page by itself. The footer rides up into the body. None of it reproduced locally, because the layout was implicitly tuned to your sample data, not to the full range of real data.&lt;/p&gt;

&lt;p&gt;A layout that only holds for the data you happened to test with isn't a layout. It's a coincidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 3: fonts and rendering drift
&lt;/h2&gt;

&lt;p&gt;The PDF looked right on your machine. On the server it's Times New Roman, because the font you used isn't installed in the container.&lt;/p&gt;

&lt;p&gt;Headless rendering inherits every environment-specific quirk of a real browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;missing fonts silently fall back to something ugly&lt;/li&gt;
&lt;li&gt;emoji and non-Latin scripts render as tofu boxes (□□□)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;print&lt;/code&gt; media queries behave differently than what you previewed on screen&lt;/li&gt;
&lt;li&gt;a Chrome version bump subtly shifts spacing, and last month's pixel-perfect invoice now wraps differently&lt;/li&gt;
&lt;li&gt;high-resolution images blow up the file size or come out blurry depending on the device scale factor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't control the renderer. You control a &lt;em&gt;configuration&lt;/em&gt; of a renderer that changes underneath you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 4: the operational tax
&lt;/h2&gt;

&lt;p&gt;Even when the output is correct, running a browser to make a PDF is a heavy way to make a PDF.&lt;/p&gt;

&lt;p&gt;Each render spins up a headless Chrome process. In production that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hundreds of MB of RAM per render, and CPU spikes under load&lt;/li&gt;
&lt;li&gt;a process that can hang, leak, or zombie — so now you need timeouts, restarts, and a watchdog&lt;/li&gt;
&lt;li&gt;slow cold starts that make this miserable in serverless/Lambda&lt;/li&gt;
&lt;li&gt;a fat, fragile dependency (the whole browser) baked into your container&lt;/li&gt;
&lt;li&gt;a brand-new scaling bottleneck: concurrent PDF requests now compete for browser processes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You set out to render a document and accidentally adopted a browser fleet to operate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 5: security surface
&lt;/h2&gt;

&lt;p&gt;Headless browsers execute whatever they're told to render. The instant any user-controlled content reaches the template — a name, a note, a logo URL — you've opened the door to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSRF via &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt;/&lt;code&gt;@import&lt;/code&gt; pointing at internal services or cloud metadata endpoints&lt;/li&gt;
&lt;li&gt;local file reads through &lt;code&gt;file://&lt;/code&gt; URLs&lt;/li&gt;
&lt;li&gt;JavaScript execution and resource exfiltration unless you've carefully locked the sandbox down&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's all manageable. But "I just wanted an invoice" turned into running a content-injection-capable browser engine on your infrastructure, and that's a real surface you now own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why these keep happening
&lt;/h2&gt;

&lt;p&gt;Step back and the pattern is obvious: &lt;strong&gt;every one of these bugs comes from making the layout engine re-derive the document on every render.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The browser doesn't know it's making an invoice. It knows it's painting a box model. So it re-flows, re-paginates, and re-decides everything, every time, from whatever HTML and data you handed it — and any of those decisions can go sideways when the inputs change or the environment shifts.&lt;/p&gt;

&lt;p&gt;The data changes every render. The layout almost never does. But HTML-to-PDF couples them, so a change in data can corrupt the layout.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to use instead: structured templates
&lt;/h2&gt;

&lt;p&gt;The alternative is to stop screenshotting a web page and instead render from a &lt;strong&gt;structured template&lt;/strong&gt; — a real document definition (pages, blocks, tables, text frames) that's built to paginate and reflow as a document, not as a viewport.&lt;/p&gt;

&lt;p&gt;The split looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTML-to-PDF:   data + layout-as-HTML  →  browser  →  screenshot  →  slice into pages
Structured:    data  →  reusable template  →  document renderer  →  PDF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Concretely, you separate the two things that were tangled together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The template&lt;/strong&gt; is designed once. It owns the layout: page size, margins, typography, where the logo goes, how the table behaves when it has 2 rows or 200.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The data&lt;/strong&gt; is all that changes per render. You send JSON; the template absorbs it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the renderer understands documents instead of viewports, the failure modes above stop being your problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pagination&lt;/strong&gt; is a first-class concept — tables break across pages with headers repeating, rows stay intact, totals stay attached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long/variable data reflows&lt;/strong&gt; inside the structure instead of overflowing it — a 90-character name wraps, a 200-row table paginates, the layout holds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fonts are part of the template&lt;/strong&gt;, embedded and deterministic — no server-environment roulette.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No browser&lt;/strong&gt; means no per-render Chrome process, no cold-start tax, no browser CVEs to babysit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No arbitrary HTML/JS execution&lt;/strong&gt;, so the content-injection surface mostly evaporates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same template, every time. Same spacing, same fonts, same brand. Only the data is different.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in practice
&lt;/h2&gt;

&lt;p&gt;Instead of building an HTML string and handing it to a browser, you design the template once, then generate from data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customer_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Inc."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"invoice_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INV-1048"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"due_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"line_items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"API usage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$480.00"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Support plan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$199.00"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$679.00"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire per-render payload. No markup, no CSS, no print media queries, no &lt;code&gt;break-inside&lt;/code&gt; incantations. Send the data, get back a finished PDF. Whether it's one document or ten thousand, the template already solved the layout — you're not re-solving it on every request.&lt;/p&gt;

&lt;p&gt;This is the idea behind &lt;a href="https://pdfmakerapi.com" rel="noopener noreferrer"&gt;PDFMakerAPI&lt;/a&gt;: design a reusable PDF template in a visual editor, then generate PDFs from your data — by REST API, no-code workflows (Zapier/Make/n8n), or an AI agent over MCP. A PDF generation API for document automation, specifically so the layout stops being something every render has to rediscover.&lt;/p&gt;

&lt;h2&gt;
  
  
  When HTML-to-PDF is still the right call
&lt;/h2&gt;

&lt;p&gt;To keep this honest: reach for headless Chrome when&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it's a genuine one-off or low-volume internal export,&lt;/li&gt;
&lt;li&gt;you're literally trying to archive a &lt;em&gt;web page&lt;/em&gt; as it appears (that's the one case where "screenshot the page" is the actual requirement), or&lt;/li&gt;
&lt;li&gt;the layout is throwaway and consistency doesn't matter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problems start when the document is &lt;strong&gt;reused, branded, automated, generated in bulk, or sent to customers&lt;/strong&gt; — because that's when consistency, determinism, and operational cost stop being optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;HTML-to-PDF feels free because you already have the HTML. The cost shows up later, in production, as pagination bugs, layout breakage on real data, font drift, a browser fleet to operate, and a security surface you didn't mean to adopt.&lt;/p&gt;

&lt;p&gt;If you're exporting a web page, screenshot the web page.&lt;/p&gt;

&lt;p&gt;If you're generating a document — the same document, over and over, with different data — design the template once and render from structured data. Let the data change. Keep the layout fixed.&lt;/p&gt;

&lt;p&gt;That's the difference between a PDF feature that works in the demo and one that survives production.&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>api</category>
      <category>automation</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stop Letting AI Generate Your PDFs From Scratch</title>
      <dc:creator>Gerardo Barrera</dc:creator>
      <pubDate>Thu, 25 Jun 2026 02:01:00 +0000</pubDate>
      <link>https://dev.to/gerardo_barrera_538fc60b7/stop-letting-ai-generate-your-pdfs-from-scratch-59o4</link>
      <guid>https://dev.to/gerardo_barrera_538fc60b7/stop-letting-ai-generate-your-pdfs-from-scratch-59o4</guid>
      <description>&lt;p&gt;AI agents are getting weirdly good at doing work.&lt;/p&gt;

&lt;p&gt;Ask Claude, ChatGPT, Cursor, or any coding agent to “make me an invoice PDF,” and it will usually try. It might write Python with ReportLab. It might generate HTML and CSS. It might spin up Playwright. It might patch a few layout bugs, retry the render, and eventually hand you a PDF.&lt;/p&gt;

&lt;p&gt;At first, that feels magical.&lt;/p&gt;

&lt;p&gt;Then you ask it to do the same thing again tomorrow.&lt;/p&gt;

&lt;p&gt;And it rebuilds the whole thing from scratch.&lt;/p&gt;

&lt;p&gt;Different spacing. Different column widths. Different font choices. Maybe the address block moved. Maybe the footer disappeared. Maybe it handled long line items differently this time.&lt;/p&gt;

&lt;p&gt;That is not automation.&lt;/p&gt;

&lt;p&gt;That is a very expensive intern redrawing your invoice every time you need one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem is not AI-generated documents
&lt;/h2&gt;

&lt;p&gt;The problem is using an LLM as a layout engine.&lt;/p&gt;

&lt;p&gt;LLMs are great at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;understanding what kind of document you need&lt;/li&gt;
&lt;li&gt;extracting the right fields&lt;/li&gt;
&lt;li&gt;transforming messy input into clean structured data&lt;/li&gt;
&lt;li&gt;deciding which template should be used&lt;/li&gt;
&lt;li&gt;writing customer-specific language&lt;/li&gt;
&lt;li&gt;filling missing-but-obvious values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are bad at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pixel-perfect layout&lt;/li&gt;
&lt;li&gt;consistent spacing&lt;/li&gt;
&lt;li&gt;deterministic rendering&lt;/li&gt;
&lt;li&gt;pagination&lt;/li&gt;
&lt;li&gt;table overflow&lt;/li&gt;
&lt;li&gt;preserving brand rules&lt;/li&gt;
&lt;li&gt;generating the exact same output every time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That matters because most business documents are not creative writing exercises.&lt;/p&gt;

&lt;p&gt;Invoices, receipts, certificates, contracts, reports, order forms, proposals, and onboarding packets usually follow the same structure every time.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;data&lt;/strong&gt; changes.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;layout&lt;/strong&gt; does not.&lt;/p&gt;

&lt;p&gt;So why are we asking the model to regenerate the layout on every request?&lt;/p&gt;

&lt;h2&gt;
  
  
  You are paying the model to re-solve a solved problem
&lt;/h2&gt;

&lt;p&gt;Imagine this invoice data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Inc."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"invoice_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INV-1048"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"due_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"line_items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"API usage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$480.00"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Support plan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$199.00"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$679.00"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That data is tiny.&lt;/p&gt;

&lt;p&gt;But when you ask an AI agent to generate the PDF from scratch, it does not just output that data. It generates layout code too.&lt;/p&gt;

&lt;p&gt;It has to decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;page size&lt;/li&gt;
&lt;li&gt;margins&lt;/li&gt;
&lt;li&gt;typography&lt;/li&gt;
&lt;li&gt;table structure&lt;/li&gt;
&lt;li&gt;column widths&lt;/li&gt;
&lt;li&gt;header placement&lt;/li&gt;
&lt;li&gt;footer placement&lt;/li&gt;
&lt;li&gt;colors&lt;/li&gt;
&lt;li&gt;spacing&lt;/li&gt;
&lt;li&gt;overflow behavior&lt;/li&gt;
&lt;li&gt;PDF rendering library&lt;/li&gt;
&lt;li&gt;error handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then it has to run the code.&lt;/p&gt;

&lt;p&gt;Then it has to fix the code.&lt;/p&gt;

&lt;p&gt;Then it has to hope the next invoice still fits.&lt;/p&gt;

&lt;p&gt;Now multiply that by 1,000 invoices.&lt;/p&gt;

&lt;p&gt;You do not have one reusable document workflow.&lt;/p&gt;

&lt;p&gt;You have 1,000 tiny code generation projects.&lt;/p&gt;

&lt;p&gt;That is the hidden tax of AI-generated PDFs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The output drifts because the model is doing too much
&lt;/h2&gt;

&lt;p&gt;This is the part that makes AI document generation dangerous for real workflows.&lt;/p&gt;

&lt;p&gt;A human sees this prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make an invoice for Acme Inc. with these line items.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An LLM sees an open-ended generation task.&lt;/p&gt;

&lt;p&gt;It might produce a good invoice. But unless you force the layout into a deterministic system, the model has room to improvise.&lt;/p&gt;

&lt;p&gt;And improvisation is exactly what you do &lt;strong&gt;not&lt;/strong&gt; want in business documents.&lt;/p&gt;

&lt;p&gt;You do not want invoice #1048 to use a slightly different layout than invoice #1049.&lt;/p&gt;

&lt;p&gt;You do not want your certificate title centered one day and slightly off-center the next.&lt;/p&gt;

&lt;p&gt;You do not want a contract clause to wrap differently because the model changed the font size.&lt;/p&gt;

&lt;p&gt;You do not want a customer report to break because one field was longer than expected.&lt;/p&gt;

&lt;p&gt;For one-off documents, this is fine.&lt;/p&gt;

&lt;p&gt;For automated document generation, it is a bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The better pattern: separate layout from data
&lt;/h2&gt;

&lt;p&gt;The fix is boring.&lt;/p&gt;

&lt;p&gt;That is why it works.&lt;/p&gt;

&lt;p&gt;Design the document once.&lt;/p&gt;

&lt;p&gt;Then generate it forever with data.&lt;/p&gt;

&lt;p&gt;Instead of asking the LLM to generate the entire PDF every time, give it a much smaller job:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Understand the request and produce structured JSON.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then let a deterministic renderer handle the layout.&lt;/p&gt;

&lt;p&gt;The workflow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User request
   ↓
AI agent extracts structured data
   ↓
Reusable PDF template receives data
   ↓
PDF renderer generates consistent output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The LLM is still useful.&lt;/p&gt;

&lt;p&gt;It is just not pretending to be a PDF layout engine anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;Instead of asking an agent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Create an invoice PDF from scratch.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use the invoice template and fill it with this customer data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The model should only need to produce something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"template_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invoice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customer_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Inc."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"invoice_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INV-1048"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"due_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"line_items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"API usage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$480.00"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Support plan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$199.00"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$679.00"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the right job for an LLM.&lt;/p&gt;

&lt;p&gt;Small output.&lt;/p&gt;

&lt;p&gt;Structured data.&lt;/p&gt;

&lt;p&gt;Easy to validate.&lt;/p&gt;

&lt;p&gt;Cheap to retry.&lt;/p&gt;

&lt;p&gt;No layout drift.&lt;/p&gt;

&lt;p&gt;No surprise CSS.&lt;/p&gt;

&lt;p&gt;No headless browser roulette.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is why I built PDFMakerAPI
&lt;/h2&gt;

&lt;p&gt;PDFMakerAPI is built around this exact idea:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design the PDF template once. Generate finished PDFs by sending data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can create reusable templates visually, then fill them through an API, no-code workflows, or AI agents.&lt;/p&gt;

&lt;p&gt;The important part is that the template is reusable.&lt;/p&gt;

&lt;p&gt;The AI does not need to redraw the invoice.&lt;/p&gt;

&lt;p&gt;It does not need to re-invent the table.&lt;/p&gt;

&lt;p&gt;It does not need to decide where the logo goes.&lt;/p&gt;

&lt;p&gt;It just sends data.&lt;/p&gt;

&lt;p&gt;The layout engine does the boring deterministic work.&lt;/p&gt;

&lt;p&gt;That is how document automation should work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it from an AI agent with MCP
&lt;/h2&gt;

&lt;p&gt;With MCP, an agent like Claude, Cursor, or ChatGPT can call a document generation tool instead of writing PDF code.&lt;/p&gt;

&lt;p&gt;The agent receives a request like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make an invoice for Acme Inc. for API usage and support, due net 30.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of generating layout code, the agent calls a tool with structured data.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"template_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invoice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customer_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Inc."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"payment_terms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Net 30"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"line_items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"API usage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$480.00"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Support plan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$199.00"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$679.00"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a finished PDF using the same template every time.&lt;/p&gt;

&lt;p&gt;Same spacing.&lt;/p&gt;

&lt;p&gt;Same fonts.&lt;/p&gt;

&lt;p&gt;Same table.&lt;/p&gt;

&lt;p&gt;Same brand.&lt;/p&gt;

&lt;p&gt;Different data.&lt;/p&gt;

&lt;p&gt;That is the entire point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it from an API
&lt;/h2&gt;

&lt;p&gt;The same pattern works without an agent.&lt;/p&gt;

&lt;p&gt;Your app sends JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.pdfmakerapi.com/v1/render &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "template_id": "invoice",
    "data": {
      "customer_name": "Acme Inc.",
      "invoice_number": "INV-1048",
      "total": "$679.00"
    }
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; invoice.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your document generation is just part of your application.&lt;/p&gt;

&lt;p&gt;No prompt loops.&lt;/p&gt;

&lt;p&gt;No generated layout code.&lt;/p&gt;

&lt;p&gt;No “let me fix this PDF script” step.&lt;/p&gt;

&lt;p&gt;No inconsistent output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bulk generation should not mean bulk prompting
&lt;/h2&gt;

&lt;p&gt;This is where the difference becomes obvious.&lt;/p&gt;

&lt;p&gt;Bad pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prompt the AI 1,000 times.
Generate 1,000 different pieces of layout code.
Render 1,000 PDFs.
Hope they match.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pdfmakerapi.com/v1/render&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;invoice_number&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is real automation.&lt;/p&gt;

&lt;p&gt;The template is already designed.&lt;/p&gt;

&lt;p&gt;The layout is already solved.&lt;/p&gt;

&lt;p&gt;The only thing changing is the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  When AI-generated PDFs are still fine
&lt;/h2&gt;

&lt;p&gt;To be fair, there are cases where generating a PDF from scratch is totally reasonable.&lt;/p&gt;

&lt;p&gt;If you need a quick one-off mockup, a temporary chart, or a throwaway internal document, let the model improvise.&lt;/p&gt;

&lt;p&gt;That is fine.&lt;/p&gt;

&lt;p&gt;The problem starts when the document matters.&lt;/p&gt;

&lt;p&gt;If it needs to be reused, branded, audited, emailed to customers, generated in bulk, or produced consistently, then “AI, please make me a PDF” is the wrong abstraction.&lt;/p&gt;

&lt;p&gt;The better abstraction is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AI, understand the request and fill the right template.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The future is not AI drawing boxes forever
&lt;/h2&gt;

&lt;p&gt;AI agents should not be spending tokens deciding where an invoice table goes.&lt;/p&gt;

&lt;p&gt;They should not be writing and rewriting PDF layout code for documents your company already standardized months ago.&lt;/p&gt;

&lt;p&gt;They should understand intent, prepare data, call tools, and let deterministic systems do deterministic work.&lt;/p&gt;

&lt;p&gt;That is the pattern that scales.&lt;/p&gt;

&lt;p&gt;Use AI for the messy human part.&lt;/p&gt;

&lt;p&gt;Use templates for the boring layout part.&lt;/p&gt;

&lt;p&gt;Design once.&lt;/p&gt;

&lt;p&gt;Generate forever.&lt;/p&gt;

&lt;p&gt;You can try the workflow with &lt;a href="https://pdfmakerapi.com" rel="noopener noreferrer"&gt;PDFMakerAPI&lt;/a&gt; — reusable PDF templates, visual editing, API generation, and MCP support for AI agents.&lt;/p&gt;

&lt;p&gt;The next time an AI agent starts writing 200 lines of PDF layout code, ask yourself:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did I need a smarter model, or did I just need a reusable template?&lt;/strong&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Generate PDFs from JSON in Any Language — One REST API (Python, Node, PHP, Go)</title>
      <dc:creator>Gerardo Barrera</dc:creator>
      <pubDate>Wed, 24 Jun 2026 05:10:18 +0000</pubDate>
      <link>https://dev.to/gerardo_barrera_538fc60b7/generate-pdfs-from-json-in-any-language-one-rest-api-python-node-php-go-564f</link>
      <guid>https://dev.to/gerardo_barrera_538fc60b7/generate-pdfs-from-json-in-any-language-one-rest-api-python-node-php-go-564f</guid>
      <description>&lt;p&gt;Every backend eventually has to spit out a PDF — an invoice, a receipt, a report, a certificate. And in every language, the options are bad:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Headless Chrome / Puppeteer&lt;/strong&gt; — you render HTML to PDF. Works until your data gets long: tables split across pages, fonts go missing, and you're babysitting a browser farm in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native libraries&lt;/strong&gt; (ReportLab in Python, PDFKit in Node, FPDF in PHP, gofpdf in Go) — you position everything by hand in code. Generic fonts, fragile layouts, and the design lives in your codebase forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a cleaner separation: &lt;strong&gt;design the document once, then feed it data over HTTP.&lt;/strong&gt; Because it's just a REST call, the exact same approach works in any language.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: template + data → PDF
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Design a reusable template once — visually, or by describing it to an AI — with placeholders like &lt;code&gt;{{customer}}&lt;/code&gt; and &lt;code&gt;{{total}}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST&lt;/code&gt; your JSON to one endpoint with the template ID.&lt;/li&gt;
&lt;li&gt;Get back a finished, &lt;strong&gt;editable&lt;/strong&gt; PDF.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No HTML/CSS, no headless browser, no fonts to ship. Here's the same request in five languages.&lt;/p&gt;

&lt;h3&gt;
  
  
  curl
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.pdfmakerapi.com/v1/render &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ "template_id": "invoice_default",
        "data": { "number": "INV-1042", "customer": "Acme Inc.", "total": 49.00 } }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; invoice.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Node.js
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.pdfmakerapi.com/v1/render&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PDF_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;template_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice_default&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INV-1042&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Acme Inc.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;49.0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Python
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pdfmakerapi.com/v1/render&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PDF_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice_default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INV-1042&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Acme Inc.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;49.00&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  PHP
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="nv"&gt;$res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://api.pdfmakerapi.com/v1/render"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;stream_context_create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="s2"&gt;"http"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"method"&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"header"&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$_ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'PDF_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s2"&gt;Content-Type: application/json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"content"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="s2"&gt;"template_id"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"invoice_default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"data"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"INV-1042"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"customer"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Acme Inc."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"total"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;49.00&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;span class="nb"&gt;file_put_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"invoice.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Go
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"template_id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"invoice_default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"data"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"INV-1042"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"customer"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Acme Inc."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"total"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;49.00&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://api.pdfmakerapi.com/v1/render"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PDF_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"invoice.pdf"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same JSON, same endpoint, five languages. The PDF comes back in the response body.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the template comes from
&lt;/h2&gt;

&lt;p&gt;You don't write HTML. You design the template in a &lt;strong&gt;visual, drag-and-drop editor&lt;/strong&gt; — drop in text, tables, images, and &lt;code&gt;{{variables}}&lt;/code&gt; — or describe it in plain English and let the AI build it. Change the design later without touching your code; your request body stays the same.&lt;/p&gt;

&lt;p&gt;And because it renders from a &lt;strong&gt;structured template&lt;/strong&gt; (not a screenshot of a webpage), layouts don't break when the data changes — and there's no headless browser, so it generates thousands of PDFs in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating in bulk
&lt;/h2&gt;

&lt;p&gt;It's just HTTP, so loop your data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice_default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
    &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pdfmakerapi.com" rel="noopener noreferrer"&gt;PDFMakerAPI&lt;/a&gt; is free to start — 100 PDFs/month, no card. Design a template, grab an API key, and you're rendering PDFs from any language in a few minutes. You can also generate from no-code tools (Zapier, Make, n8n) or straight from AI agents (Claude, ChatGPT) — same templates, same output.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What's the worst PDF-generation setup you've had to maintain? Curious what people are running.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>api</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Let an AI Agent Generate Real PDFs (with an MCP server)</title>
      <dc:creator>Gerardo Barrera</dc:creator>
      <pubDate>Sat, 20 Jun 2026 04:37:27 +0000</pubDate>
      <link>https://dev.to/gerardo_barrera_538fc60b7/how-to-let-an-ai-agent-generate-real-pdfs-with-an-mcp-server-28e3</link>
      <guid>https://dev.to/gerardo_barrera_538fc60b7/how-to-let-an-ai-agent-generate-real-pdfs-with-an-mcp-server-28e3</guid>
      <description>&lt;p&gt;AI agents are great at producing &lt;em&gt;text&lt;/em&gt;. But the moment you need an actual &lt;strong&gt;document&lt;/strong&gt; — an invoice, a report, a certificate — they fall apart. You get markdown you have to format yourself, or (with Code Interpreter) a rough PDF from a Python lib with generic fonts and tables that never quite look right. There's no clean way to hand an agent the job of "produce a polished PDF."&lt;/p&gt;

&lt;p&gt;So I gave my agents one tool that does exactly that, over &lt;strong&gt;MCP&lt;/strong&gt; (Model Context Protocol). The agent describes the document, and gets back a link to a finished, &lt;strong&gt;editable&lt;/strong&gt; PDF. Here's how to wire it up in about 5 lines of config.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disclosure: I work on &lt;a href="https://pdfmakerapi.com" rel="noopener noreferrer"&gt;PDFMakerAPI&lt;/a&gt; — but the pattern here (give an agent a single, well-scoped tool that returns a reviewable artifact) applies to any MCP server. The config below just happens to use mine.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  30-second MCP refresher
&lt;/h2&gt;

&lt;p&gt;MCP is the open standard for giving an AI agent &lt;em&gt;tools&lt;/em&gt;. You point your client (Claude Desktop, Cursor, Windsurf, Cline, VS Code, ChatGPT…) at an MCP server, and the agent can now call whatever functions that server exposes. No glue code, no custom integration per client — that's the whole appeal.&lt;/p&gt;

&lt;p&gt;The server we're adding exposes exactly &lt;strong&gt;one&lt;/strong&gt; tool: &lt;code&gt;create_document&lt;/code&gt;. (One tool on purpose — a small, predictable surface area is easier for an agent to use correctly than a grab-bag of twenty.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Add the server
&lt;/h2&gt;

&lt;p&gt;Drop this into your client's MCP config (Claude Desktop: &lt;em&gt;Settings → Developer → Edit Config&lt;/em&gt;; Cursor: &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt;; same idea elsewhere):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pdfmakerapi"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@pdfmakerapi/mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart the client. That's the entire setup — no API key, no account.&lt;/p&gt;

&lt;p&gt;Using a web client like &lt;strong&gt;Claude.ai&lt;/strong&gt; or &lt;strong&gt;ChatGPT&lt;/strong&gt; that can't run &lt;code&gt;npx&lt;/code&gt;? Add the hosted endpoint as a custom connector instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.pdfmakerapi.com/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2 — Ask for a document
&lt;/h2&gt;

&lt;p&gt;Now just ask the agent in plain English:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Make a professional invoice for Acme Corp — 3 line items, net 30."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent calls &lt;code&gt;create_document&lt;/code&gt;, and you get back a link like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://app.pdfmakerapi.com/d/019ee2fe-c503-71c0-aaf6-68cf33ca096c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open it and you'll see the finished invoice — and you can &lt;strong&gt;edit any field&lt;/strong&gt; before downloading the PDF. Here's a real one to click:&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://app.pdfmakerapi.com/d/019ee2fe-c503-71c0-aaf6-68cf33ca096c" rel="noopener noreferrer"&gt;Live invoice example&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It works for invoices, receipts, certificates, reports, resumes, letters — if the agent can describe it, it can lay it out with headings, tables, and your data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the "editable link" matters for agents
&lt;/h2&gt;

&lt;p&gt;This is the part I actually care about, and it's why I didn't make the tool just spit out a final PDF.&lt;/p&gt;

&lt;p&gt;If you've spent any time running agents in production, you know the failure mode isn't "bad answer" — it's &lt;strong&gt;a wrong action that's already done&lt;/strong&gt;. An agent that emails the wrong customer or files the wrong number is worse than one that just says something dumb.&lt;/p&gt;

&lt;p&gt;Returning an &lt;strong&gt;editable document&lt;/strong&gt; instead of a finalized file builds a natural &lt;strong&gt;human-in-the-loop checkpoint&lt;/strong&gt; into the workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent &lt;strong&gt;drafts&lt;/strong&gt; the document from the request.&lt;/li&gt;
&lt;li&gt;A human &lt;strong&gt;opens the link, checks it, fixes anything&lt;/strong&gt;, and downloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the agent does the tedious 90% (layout, structure, pulling the data together), but a person stays in control of what actually ships. For anything that leaves the building — an invoice to a client, a certificate with someone's name on it — that checkpoint is worth a lot more than full autonomy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does &lt;em&gt;not&lt;/em&gt; do (so you scope it right)
&lt;/h2&gt;

&lt;p&gt;Being straight about the edges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It &lt;strong&gt;generates&lt;/strong&gt; documents — it does &lt;strong&gt;not&lt;/strong&gt; read or edit a PDF you upload. Different problem.&lt;/li&gt;
&lt;li&gt;It returns an &lt;strong&gt;editable web document + a downloadable PDF&lt;/strong&gt;, not a blank fillable form to hand out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your agent needs to &lt;em&gt;produce&lt;/em&gt; structured documents from data or a prompt, this fits. If it needs to &lt;em&gt;parse&lt;/em&gt; existing PDFs, you want a different tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood (for the curious)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;create_document&lt;/code&gt; takes a document model — a small JSON tree of nodes (text, containers, tables) with optional &lt;code&gt;{{variables}}&lt;/code&gt; — and stores it, returning the share link. The agent can build that structure from a prompt, or you can POST it yourself to the same REST API if you'd rather skip the agent entirely. It's open source (MIT) if you want to read the tool definition: &lt;a href="https://github.com/GerardoBarrera/pdfmakerapi-mcp" rel="noopener noreferrer"&gt;github.com/GerardoBarrera/pdfmakerapi-mcp&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Giving an agent the ability to produce a real, reviewable document turned out to be a small change with a big payoff:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add one MCP server (~5 lines).&lt;/li&gt;
&lt;li&gt;Ask for the document in plain English.&lt;/li&gt;
&lt;li&gt;Get an editable link a human can sign off on before it ships.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No markdown-to-PDF wrangling, no headless browser, and a built-in review step instead of blind autonomy.&lt;/p&gt;

&lt;p&gt;If you're building agents that produce deliverables (not just text): &lt;strong&gt;do you let the agent finalize, or always hand off to a human to review first?&lt;/strong&gt; Genuinely curious how others are drawing that line — drop it in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>claude</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Generate a PDF from JSON in Node.js (without a headless browser)</title>
      <dc:creator>Gerardo Barrera</dc:creator>
      <pubDate>Sat, 20 Jun 2026 04:29:56 +0000</pubDate>
      <link>https://dev.to/gerardo_barrera_538fc60b7/how-to-generate-a-pdf-from-json-in-nodejs-without-a-headless-browser-3i9h</link>
      <guid>https://dev.to/gerardo_barrera_538fc60b7/how-to-generate-a-pdf-from-json-in-nodejs-without-a-headless-browser-3i9h</guid>
      <description>&lt;p&gt;If you've ever had to generate PDFs from a Node app — invoices, receipts, reports, certificates — you've probably reached for &lt;br&gt;
  &lt;strong&gt;Puppeteer&lt;/strong&gt; or some headless-Chrome setup. It works… until you have to run it in production. Now you're shipping a 300MB Chromium &lt;br&gt;
  binary, babysitting a browser process, fighting memory leaks, and your "render a PDF" endpoint times out under load.&lt;/p&gt;

&lt;p&gt;There's a simpler way: &lt;strong&gt;describe the document as JSON, POST it to an API, get a PDF back.&lt;/strong&gt; No browser. In this post I'll show how &lt;br&gt;
  to go from a JSON payload to a finished, editable PDF in about 15 lines of Node.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Full disclosure: I built &lt;a href="https://pdfmakerapi.com" rel="noopener noreferrer"&gt;PDFMakerAPI&lt;/a&gt;, the tool I'm using here. There's a free tier, no API key needed to&lt;br&gt;
  try it, and the code below runs as-is.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;## The idea: separate the data from the design&lt;/p&gt;

&lt;p&gt;The trick to sane PDF generation is to stop gluing strings of HTML together. Instead you keep two things separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The design&lt;/strong&gt; — a layout of text, tables, and containers (you can design this visually, or describe it as JSON).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The data&lt;/strong&gt; — the actual values that change per document.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You merge them with one request and get back a link to the finished PDF. Let's do it.&lt;/p&gt;

&lt;p&gt;## Step 1 — Describe the document as JSON&lt;/p&gt;

&lt;p&gt;A document is just a tree of nodes. Here's a minimal one — a short welcome letter with a &lt;code&gt;{{customer_name}}&lt;/code&gt; placeholder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Welcome Letter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;letter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;var_name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;customer_name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Customer Name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alex Morgan&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;full&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Welcome aboard 🎉&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;xl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#111827&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;full&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hi {{customer_name}}, thanks for joining. This whole PDF was generated from JSON in a single API call — no headless browser
  involved.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;md&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#374151&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything in &lt;code&gt;{{double_braces}}&lt;/code&gt; is a variable, so you can reuse the same layout with different data. The full schema (containers,&lt;br&gt;
  tables, images, fonts, page settings) is in the &lt;a href="https://github.com/GerardoBarrera/pdfmakerapi-mcp" rel="noopener noreferrer"&gt;docs and repo&lt;/a&gt; — but the shape&lt;br&gt;
  above is all you need to start.&lt;/p&gt;

&lt;p&gt;## Step 2 — POST it and get a link back&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.pdfmakerapi.com/api/v1/documents&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// → https://app.pdfmakerapi.com/d/&amp;lt;id&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The response is &lt;code&gt;{ id, url }&lt;/code&gt;, where &lt;code&gt;url&lt;/code&gt; opens the finished document.&lt;/p&gt;

&lt;p&gt;Prefer &lt;code&gt;curl&lt;/code&gt;?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.pdfmakerapi.com/api/v1/documents &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ "document": { "name": "Welcome Letter", "pageSize": "letter", "children": [ { "id": "t", "type": "text", "order": 1, "width":
  "full", "content": "Generated from JSON ✅", "fontSize": "xl", "fontWeight": "bold" } ] } }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;## Step 3 — The part Puppeteer can't do: an &lt;em&gt;editable&lt;/em&gt; result&lt;/p&gt;

&lt;p&gt;Here's the difference that matters. The link you get back isn't a flat, one-shot render — it opens an &lt;strong&gt;editable document&lt;/strong&gt;. You (or&lt;br&gt;
  anyone you send the link to) can change any field in the browser and the preview updates live, then download the PDF.&lt;/p&gt;

&lt;p&gt;Here's a real one generated this way — open it and edit a field:&lt;br&gt;
  👉 &lt;strong&gt;&lt;a href="https://app.pdfmakerapi.com/d/019ee2fe-c503-71c0-aaf6-68cf33ca096c" rel="noopener noreferrer"&gt;Live invoice example&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That turns out to be a big deal in practice: your app drafts the document, but a human can review and fix it before it goes out —&lt;br&gt;
  instead of trusting whatever got rendered.&lt;/p&gt;

&lt;p&gt;## Bonus: you don't even have to write the JSON&lt;/p&gt;

&lt;p&gt;If hand-building the node tree feels tedious, you can skip it entirely. PDFMakerAPI ships an &lt;strong&gt;MCP server&lt;/strong&gt;, so an AI agent (Claude,&lt;br&gt;
  Cursor, ChatGPT, etc.) can generate the document structure from a plain-English prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"pdfmakerapi"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@pdfmakerapi/mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then just ask: &lt;em&gt;"make an invoice for Acme with 3 line items"&lt;/em&gt; and it returns the same kind of editable link. Same engine, no JSON by&lt;br&gt;
  hand.&lt;/p&gt;

&lt;p&gt;## When you should NOT use this&lt;/p&gt;

&lt;p&gt;Being honest: an API isn't always the right call.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you need to &lt;strong&gt;screenshot an existing web page&lt;/strong&gt; to PDF, a headless browser is the right tool — that's literally what it's for.&lt;/li&gt;
&lt;li&gt;If you're generating &lt;strong&gt;one PDF, once, locally&lt;/strong&gt;, a library like &lt;code&gt;pdfkit&lt;/code&gt; is fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where this approach wins is &lt;strong&gt;generating structured documents from data, repeatedly, in production&lt;/strong&gt; — invoices, receipts, reports,&lt;br&gt;
  certificates — without running a browser farm.&lt;/p&gt;

&lt;p&gt;## Wrap-up&lt;/p&gt;

&lt;p&gt;To generate a PDF from JSON in Node:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Describe the document (or let an AI agent describe it).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST&lt;/code&gt; it to &lt;code&gt;/api/v1/documents&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Get back a link to an editable, downloadable PDF.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No Chromium, no rendering servers, no timeouts. If you want to try it, the &lt;a href="https://pdfmakerapi.com" rel="noopener noreferrer"&gt;free tier&lt;/a&gt; gives you 100 PDFs/month with no card, and the MCP server is open source (MIT) on &lt;a href="https://github.com/GerardoBarrera/pdfmakerapi-mcp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're generating PDFs in production today — what are you using, and what's been the most painful part? Curious to hear in the&lt;br&gt;
  comments.&lt;/p&gt;

</description>
      <category>node</category>
      <category>tutorial</category>
      <category>pdf</category>
      <category>api</category>
    </item>
  </channel>
</rss>
