<?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: PDF4me</title>
    <description>The latest articles on DEV Community by PDF4me (@pdf4me).</description>
    <link>https://dev.to/pdf4me</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%2F4030343%2Fb2bd2942-2a26-4c0d-9fcf-bc47b6b17f04.png</url>
      <title>DEV Community: PDF4me</title>
      <link>https://dev.to/pdf4me</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pdf4me"/>
    <language>en</language>
    <item>
      <title>Page Range, Page Count, or Individual Pages: The Three Ways to Split a PDF</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Tue, 01 Sep 2026 12:52:49 +0000</pubDate>
      <link>https://dev.to/pdf4me/page-range-page-count-or-individual-pages-the-three-ways-to-split-a-pdf-33ai</link>
      <guid>https://dev.to/pdf4me/page-range-page-count-or-individual-pages-the-three-ways-to-split-a-pdf-33ai</guid>
      <description>&lt;p&gt;Say you have a 40 page contract and you need three different things out of it: pages 1 to 10 for the counterparty, every 5 pages as a separate reviewer packet, and pages 3, 12, and 27 pulled out on their own because that is where the signature blocks live. That is not one splitting problem. That is three, and PDF4me's Split PDF endpoint treats them as three separate parameters, not three separate products.&lt;/p&gt;

&lt;p&gt;Here is what is actually going on under the hood, and why the platform you build on changes how many of those three ways you can reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the endpoint actually does
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.pdf4me.com/pdf4me-api/merge-split/split-pdf/" rel="noopener noreferrer"&gt;Split PDF&lt;/a&gt; REST endpoint is a single &lt;code&gt;POST /api/v2/SplitPDF&lt;/code&gt; call. You send it a base64 PDF and one of four &lt;code&gt;splitAction&lt;/code&gt; values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SplitAfterPage&lt;/strong&gt;: cut once, after a page you name. Two output files, always.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RecurringSplitAfterPage&lt;/strong&gt;: cut every N pages. A 21 page PDF with &lt;code&gt;splitActionNumber&lt;/code&gt; 4 comes back as six files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SplitSequence&lt;/strong&gt;: cut at a list of specific pages, like &lt;code&gt;[1, 3, 8]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SplitRanges&lt;/strong&gt;: pull out named ranges, like &lt;code&gt;"1-4"&lt;/code&gt; or &lt;code&gt;"10-21"&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That fourth column, count, range, sequence, plus the simple two way cut, is the whole feature. It is worth reading the docs page's own parameter table closely, because it has a small inconsistency worth knowing about before you write code: the Required Parameters table labels the two file fields "File Content" and "File Name," but the Payload example directly underneath it, and the interactive &lt;a href="https://docs.pdf4me.com/url-api-tester/split-pdf/" rel="noopener noreferrer"&gt;API Tester's Split PDF page&lt;/a&gt;, both confirm the real JSON keys are &lt;code&gt;docContent&lt;/code&gt; and &lt;code&gt;docName&lt;/code&gt;. The table labels are UI-friendly aliases, not the field names your request body actually needs.&lt;/p&gt;

&lt;p&gt;The response has its own small oddity too: the live docs page's own JSON example spells the output array key &lt;code&gt;splited Documents&lt;/code&gt;, not "split." It is not a typo in this article. It is what the page says.&lt;/p&gt;

&lt;p&gt;Here is a minimal request built from the verified field names, using Python's &lt;code&gt;requests&lt;/code&gt; library:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contract.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;rb&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;doc_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&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;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;payload&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;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;doc_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docName&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;contract.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;splitAction&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;SplitRanges&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;splitRanges&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;1-10&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;fileNaming&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;NameAsPerOrder&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;async&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&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.pdf4me.com/api/v2/SplitPDF&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&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;Content-Type&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;application/json&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="n"&gt;payload&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&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="c1"&gt;# result["splited Documents"] holds the array of split PDFs
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap &lt;code&gt;splitAction&lt;/code&gt; to &lt;code&gt;SplitSequence&lt;/code&gt; with a &lt;code&gt;splitSequence&lt;/code&gt; value like &lt;code&gt;[1, 3, 8]&lt;/code&gt; for the individual pages case, or &lt;code&gt;RecurringSplitAfterPage&lt;/code&gt; with &lt;code&gt;splitActionNumber&lt;/code&gt; for equal chunks. Same endpoint, same payload shape, one field changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four platforms, two very different capability sets
&lt;/h2&gt;

&lt;p&gt;This is the part worth building a workflow decision around, because the four platforms do not offer the same three ways to split.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;REST and &lt;a href="https://docs.pdf4me.com/integration/n8n/merge-split/split-pdf/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; match each other exactly.&lt;/strong&gt; n8n's Split PDF node exposes the same four options as the REST call, Split After Page, Recurring Split After Page, Split Sequence, and Split Ranges, through one Split Action dropdown, plus its own richer output (fileSize, success, splitCount, originalFileName on top of the split files themselves). If your workflow needs to pull out pages 3, 12, and 27 in one call, or extract "10 to 21" as a named range, n8n can do it without leaving the node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://docs.pdf4me.com/integration/power-automate/merge-split/split-pdf/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt; and &lt;a href="https://docs.pdf4me.com/integration/zapier/merge-split/split-pdf-by-page-number/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt; both cut that down to two options.&lt;/strong&gt; Simple Split (one cut point, two output files) and Recurring Split (equal N page segments). That is it. Neither platform's Split PDF action exposes a page-range mode or a specific-page-sequence mode in its UI, full stop. If your Power Automate flow or your Zap needs to hand back "pages 3, 12, and 27" as one output, the building block for that is not on this action, at least not as documented today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://docs.pdf4me.com/integration/make/merge-split/split-pdf/" rel="noopener noreferrer"&gt;Make&lt;/a&gt; is the platform to read most carefully&lt;/strong&gt;, because its own documentation page does not agree with itself. The prose at the top of the page and its "Common Questions" section describe a comma-separated, zero-indexed range notation: &lt;code&gt;0, 5, 10-&lt;/code&gt; creating three parts, a trailing hyphen meaning "to the end." That reads exactly like REST's SplitRanges or SplitSequence. But the actual configurable Parameter table further down the same page only offers Simple Split and Recurring Split, with a single one-based Page Number field, the same reduced shape as Power Automate and Zapier. Two different splitting models, described on the same page, and only one of them appears to be an actual configurable field. If you are building in Make, test against the live module before you assume the range notation in the prose is reachable from the UI.&lt;/p&gt;

&lt;p&gt;So if the job is "split into equal chunks" or "cut once at page N," all four platforms handle it. If the job is "pull out this specific list of pages" or "extract this named range," you are choosing between REST, n8n, or writing around the gap on Power Automate and Zapier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this actually gets used
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.pdf4me.com/integration/power-automate/merge-split/split-pdf/" rel="noopener noreferrer"&gt;Power Automate docs&lt;/a&gt; walk through a legal review scenario that is a good stand-in for the general pattern: a 100 page contract comes in, gets Recurring Split into 10 page segments, and each segment routes to a different reviewer through an Apply to Each loop. That is the "page count" way to split, and it is the one every platform here supports.&lt;/p&gt;

&lt;p&gt;The "page range" and "individual pages" ways show up when the output is not evenly sized. A signed contract where the main agreement is pages 1 through 10 and the schedules are pages 11 onward is a SplitRanges job, not a Recurring Split job, because the two halves are different lengths and only one of them (the schedules) needs to go to a narrower distribution list. Zapier's own docs describe exactly this contract-splitting scenario, but built on Simple Split, a single cut point, because that is the only tool the platform's Split PDF action actually gives you for it.&lt;/p&gt;

&lt;p&gt;For jobs where you just need a handful of pages out and do not care about the rest of the document as separate files, PDF4me also has a dedicated &lt;a href="https://docs.pdf4me.com/pdf4me-api/organize/extract-pages/" rel="noopener noreferrer"&gt;Extract Pages&lt;/a&gt; endpoint, which Make's own docs point to explicitly for that narrower case. Split PDF and Extract Pages solve adjacent but different problems: one divides a document into multiple outputs, the other pulls a subset into one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you build, test it live
&lt;/h2&gt;

&lt;p&gt;Every one of the split action types above is easier to confirm than to guess. The &lt;a href="https://docs.pdf4me.com/url-api-tester/split-pdf/" rel="noopener noreferrer"&gt;Split PDF page in the interactive API Tester&lt;/a&gt; has a working, live form for this exact endpoint: paste a base64 PDF, pick a split action, send the request, see the actual response shape come back. It is worth noting that this specific tester page is fully functional right now, even though the API Tester's own general getting-started page still lists Split PDF under "coming soon" in its endpoint status list. The individual page works. The status blurb has not caught up.&lt;/p&gt;

&lt;p&gt;That live check matters more than usual here, because this is a feature where four platforms genuinely diverge on what is configurable, one page contradicts itself, and a parameter table uses different field names than the payload beneath it. None of that is unusual for a mature API surface. It is just worth confirming against the real request and response before you wire a production flow around a page range you assumed was there.&lt;/p&gt;

&lt;p&gt;Want to see the exact request/response shape before you write a line of code? The &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;Connect to PDF4me API guide&lt;/a&gt; covers authentication and the base URL, and the &lt;a href="https://github.com/pdf4me/pdf4me-api-samples/tree/main/Merge%20and%20Split/Split%20PDF/CSharp(C%23)/Split%20PDF" rel="noopener noreferrer"&gt;C# sample for Split PDF&lt;/a&gt; on GitHub is a working reference if C# is closer to your stack than Python.&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://pdf4me.com/" rel="noopener noreferrer"&gt;pdf4me.com&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a href="https://docs.pdf4me.com/" rel="noopener noreferrer"&gt;docs.pdf4me.com&lt;/a&gt;&lt;br&gt;
Developer portal: &lt;a href="https://dev.pdf4me.com/" rel="noopener noreferrer"&gt;dev.pdf4me.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>pdf</category>
      <category>automation</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Merging 20 PDFs Into One File Without Losing Which Page Came From Where</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Tue, 01 Sep 2026 10:20:09 +0000</pubDate>
      <link>https://dev.to/pdf4me/merging-20-pdfs-into-one-file-without-losing-which-page-came-from-where-n9i</link>
      <guid>https://dev.to/pdf4me/merging-20-pdfs-into-one-file-without-losing-which-page-came-from-where-n9i</guid>
      <description>&lt;p&gt;Merging PDFs sounds like the simplest operation in the whole document-automation catalog: take several files, stick them together, get one file back. It is, mechanically. What actually causes production incidents is not the merge itself, it's the ordering. A contract package where the signature exhibit lands before the terms it's attached to, a quarterly report where finance's numbers appear ahead of the cover page that's supposed to introduce them, an onboarding packet where page 14 of one PDF ends up sandwiched into the middle of another. None of these are merge failures. They're order failures, and order is entirely the caller's responsibility, not the API's.&lt;/p&gt;

&lt;p&gt;PDF4me's &lt;a href="https://docs.pdf4me.com/pdf4me-api/merge-split/merge/" rel="noopener noreferrer"&gt;Merge&lt;/a&gt; endpoint makes that responsibility explicit in a way a lot of merge tools don't bother to. There is no separate sequencing parameter, no drag-and-drop reordering step, no "sort by filename" fallback. The order you send is the order you get back, full stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the endpoint actually does
&lt;/h2&gt;

&lt;p&gt;The REST call is &lt;code&gt;POST /api/v2/Merge&lt;/code&gt;. Two fields are required: &lt;code&gt;docContent&lt;/code&gt; and &lt;code&gt;docName&lt;/code&gt;. One optional field, &lt;code&gt;async&lt;/code&gt;, switches large batches to asynchronous processing.&lt;/p&gt;

&lt;p&gt;The detail worth pausing on: &lt;code&gt;docContent&lt;/code&gt; here is an array, not a single string. Most PDF4me endpoints take one document per call. Merge is one of the few that takes several at once, and the array's index order becomes the merged document's page order. There's no separate &lt;code&gt;order&lt;/code&gt; or &lt;code&gt;sequence&lt;/code&gt; field sitting next to it. If your source list isn't already in the order you want, you reorder the array before the request goes out, not after.&lt;/p&gt;

&lt;p&gt;The response is also unusual for this API: a 200 comes back as raw binary, the merged PDF itself, not a JSON envelope with a Base64 string inside it. Write the response body straight to a &lt;code&gt;.pdf&lt;/code&gt; file. Parsing it as JSON or trying to Base64-decode it corrupts the output, and it's a mistake the &lt;a href="https://docs.pdf4me.com/pdf4me-api/merge-split/merge/" rel="noopener noreferrer"&gt;docs page's own FAQ&lt;/a&gt; calls out directly as a common cause of "why is my merged file broken" support tickets. A 202 response, from setting &lt;code&gt;async&lt;/code&gt; true, works the same way as PDF4me's other long-running jobs: poll the &lt;code&gt;Location&lt;/code&gt; header on roughly a 10-second interval, per the standard pattern in the &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;connect guide&lt;/a&gt;, until a 200 arrives with the finished bytes.&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.pdf4me.com/api/v2/Merge &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;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Basic YOUR_API_KEY"&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;'{
    "docContent": ["JVBERi0x...cover...", "JVBERi0x...report...", "JVBERi0x...appendix..."],
    "docName": "quarterly-pack.pdf",
    "async": true
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; merged.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That array order, cover, report, appendix, is the exact page order of the output. Swap two entries and the merged PDF changes with them. Nothing else in the request influences page order at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that will actually bite you: four platforms, four different shapes for the same list
&lt;/h2&gt;

&lt;p&gt;Here's the finding worth building a workflow decision around, not just a syntax note. The REST engine underneath is identical everywhere. But PDF4me's own integration docs describe four structurally different ways of handing it an ordered list of files, and porting a merge step between platforms means re-learning the shape, not just the field names.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/make/merge-split/merge-multiple-pdfs/" rel="noopener noreferrer"&gt;Make's Merge Multiple PDFs module&lt;/a&gt; takes a &lt;strong&gt;Files&lt;/strong&gt; array, an &lt;strong&gt;Output File Name&lt;/strong&gt;, and a third parameter none of the other three platforms document at all: &lt;strong&gt;Skip Protected PDF's&lt;/strong&gt;, a boolean that lets a scenario silently skip password-protected input files instead of failing the whole batch. If your source folder sometimes contains a locked PDF, this option only exists on Make. Rebuilding the same scenario in Zapier or n8n means handling protected files yourself, upstream, because the option isn't there to flip.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/zapier/merge-split/merge-multiple-pdfs/" rel="noopener noreferrer"&gt;Zapier's Merge Multiple PDFs action&lt;/a&gt; doesn't expose an array field at all in the way Make does. Its &lt;strong&gt;Files&lt;/strong&gt; parameter is a list you build one entry at a time directly in the Zapier UI, mapped from a trigger or a previous step. The docs are explicit that there's no separate sort field: "arrange your trigger data or add file entries in the order you want them to appear in the merged output." Zapier's page also states plainly that PDF4me does not publish a fixed file-count ceiling for this action, so a real 20-file batch is a "test it with your actual batch size" exercise, not a documented hard limit either way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/power-automate/merge-split/merge-multiple-pdfs/" rel="noopener noreferrer"&gt;Power Automate's action&lt;/a&gt; is the most structurally different of the four. Its own parameter table doesn't describe an array or a list at all. It lists fixed, numbered slots: &lt;strong&gt;File Contents - 1&lt;/strong&gt;, &lt;strong&gt;File Contents - 2&lt;/strong&gt;, each individually marked required, extended by clicking an &lt;strong&gt;Add new item&lt;/strong&gt; button in the flow designer to get &lt;strong&gt;File Contents - 3&lt;/strong&gt;, &lt;strong&gt;File Contents - 4&lt;/strong&gt;, and so on. Functionally it behaves like a list once you've clicked "Add new item" enough times, but the documented shape of the parameter is numbered discrete inputs, not a single collection value the way Make and Zapier frame it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/n8n/merge-split/merge-multiple-pdfs/" rel="noopener noreferrer"&gt;n8n's node&lt;/a&gt; goes a different direction again. Its &lt;strong&gt;PDF Files&lt;/strong&gt; parameter is a true array, closer to Make's framing, but it adds something none of the other three platforms document: a per-request &lt;strong&gt;Input Type&lt;/strong&gt; selector, letting you choose Binary Data, Base64 String, or a public File URL as the source format for the files being merged. n8n's output is also the richest of the four: alongside the merged file, it returns &lt;code&gt;fileSize&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt;, and &lt;code&gt;inputFileCount&lt;/code&gt;, letting a workflow verify that the number of files it sent actually matches the number PDF4me merged, a check Make, Zapier, and Power Automate's documented outputs don't give you directly.&lt;/p&gt;

&lt;p&gt;The one constant across all four genuinely different shapes: input order is output order, everywhere. That's the detail this article's headline is really about. Whatever the platform, however the list is built, whatever you call the parameter, the sequence you hand PDF4me is the sequence the pages come back in. Get the list order right once, and the rest of each platform's quirks (the protected-file skip on Make, the numbered slots on Power Automate, the input-type selector on n8n) are configuration details, not ordering risks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this actually gets used
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Contract and closing packages.&lt;/strong&gt; Main agreement first, exhibits and schedules after, in the exact sequence a signing party or a closing attorney expects. &lt;a href="https://docs.pdf4me.com/integration/zapier/merge-split/merge-multiple-pdfs/" rel="noopener noreferrer"&gt;Zapier's own workflow examples&lt;/a&gt; walk through exactly this: application form first, then supporting documents, assembled the moment every piece is confirmed present.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-department report consolidation.&lt;/strong&gt; &lt;a href="https://docs.pdf4me.com/integration/power-automate/merge-split/merge-multiple-pdfs/" rel="noopener noreferrer"&gt;Power Automate's documented workflow&lt;/a&gt; merges a scheduled batch of sales, finance, operations, and HR reports into one quarterly file, with a cover page inserted first so the merged document reads top to bottom the way a reviewer expects, not in whatever order the individual reports happened to be retrieved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Folder-driven batch merges.&lt;/strong&gt; A &lt;a href="https://docs.pdf4me.com/blog/merge-multiple-pdfs-folder-make/" rel="noopener noreferrer"&gt;Make blog walkthrough&lt;/a&gt; covers the pattern directly: watch a cloud storage folder, iterate over whatever's in it, aggregate the results into an array, then merge, useful precisely because folder listings don't arrive pre-sorted the way a hand-built file list does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Splitting is the deliberate inverse, not a variant.&lt;/strong&gt; If the actual need is breaking one document into parts rather than combining several into one, that's &lt;a href="https://docs.pdf4me.com/pdf4me-api/merge-split/split-pdf/" rel="noopener noreferrer"&gt;Split PDF&lt;/a&gt;, a separate endpoint built for the opposite direction. And if the goal is stamping one PDF's pages on top of another's, rather than appending files end to end, that's &lt;a href="https://docs.pdf4me.com/pdf4me-api/merge-split/merge-overlay/" rel="noopener noreferrer"&gt;Merge Overlay&lt;/a&gt;, a different operation from plain concatenation despite the similar name.&lt;/p&gt;

&lt;p&gt;Before wiring any of this into a production flow, the &lt;a href="https://docs.pdf4me.com/url-api-tester/merge-multiple-pdf-files/" rel="noopener noreferrer"&gt;API Tester's live Merge Multiple PDF Files page&lt;/a&gt; lets you send a real request and inspect a real response with no code, worth a five-minute pass before you write parsing logic against an assumption.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line summary
&lt;/h2&gt;

&lt;p&gt;Merge doesn't reorder anything, it can't, it only concatenates in whatever order it's handed. That responsibility sits entirely with the caller, and PDF4me's own four integration surfaces disagree, sometimes sharply, on what shape that ordered list should take when you build it. Learn the shape for the platform you're actually using (Make's protected-file skip, Zapier's no-sort-field list, Power Automate's numbered slots, n8n's input-type selector) rather than assuming it carries over, and the one thing that never changes, page order following input order, stops being a risk and starts being the one guarantee you can build around.&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://pdf4me.com/" rel="noopener noreferrer"&gt;pdf4me.com&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a href="https://docs.pdf4me.com/" rel="noopener noreferrer"&gt;docs.pdf4me.com&lt;/a&gt;&lt;br&gt;
Developer portal: &lt;a href="https://dev.pdf4me.com/" rel="noopener noreferrer"&gt;dev.pdf4me.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>pdf</category>
      <category>automation</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A Margin Is a Page Property, Not Decoration: Adding Print-Safe Margins by API</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Mon, 31 Aug 2026 12:51:49 +0000</pubDate>
      <link>https://dev.to/pdf4me/a-margin-is-a-page-property-not-decoration-adding-print-safe-margins-by-api-74o</link>
      <guid>https://dev.to/pdf4me/a-margin-is-a-page-property-not-decoration-adding-print-safe-margins-by-api-74o</guid>
      <description>&lt;p&gt;Open a PDF in any desktop editor and "add a margin" feels like a formatting toggle, something you'd expect to sit next to font size or line spacing. It isn't. On PDF4me's &lt;a href="https://docs.pdf4me.com/pdf4me-api/edit/add-margin-to-pdf/" rel="noopener noreferrer"&gt;Add Margin to PDF&lt;/a&gt; endpoint, a margin is a page property. Call it, and the page itself gets physically bigger. A US Letter page (216 x 279mm) with 25mm added on all four sides comes back as 266 x 329mm. Nothing inside moves, shrinks, or reflows. The content sits exactly where it always sat; the page around it just grew.&lt;/p&gt;

&lt;p&gt;That distinction matters more than it sounds like it should, because it's the opposite of what a lot of developers reach for first. If you've ever used Crop PDF to trim scanner edges or strip excess white space, Add Margin does the reverse operation on the same page geometry: Crop removes area, Add Margin adds it. Confuse the two and you'll spend an afternoon debugging why your "cropped" PDF got larger instead of smaller.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the endpoint actually does
&lt;/h2&gt;

&lt;p&gt;The REST call is &lt;code&gt;POST /api/v2/AddMargin&lt;/code&gt;. Two fields are required: &lt;code&gt;docName&lt;/code&gt; (the output filename) and &lt;code&gt;docContent&lt;/code&gt; (the source PDF, Base64-encoded). Four more are optional: &lt;code&gt;marginLeft&lt;/code&gt;, &lt;code&gt;marginRight&lt;/code&gt;, &lt;code&gt;marginTop&lt;/code&gt;, and &lt;code&gt;marginBottom&lt;/code&gt;, each an integer from 0 to 100, in millimeters. Leave one out and, per the REST page itself, that side gets no margin added at all, not a zero, just untouched.&lt;/p&gt;

&lt;p&gt;The response is JSON, not a raw PDF stream: a &lt;code&gt;docName&lt;/code&gt; string and a &lt;code&gt;docContent&lt;/code&gt; field holding the new, larger PDF as a Base64 string. Decode that and you have your margin-expanded file. There's also an optional &lt;code&gt;async&lt;/code&gt; flag for polling long-running jobs instead of waiting on a synchronous response, useful if you're batching large documents through the &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;connect guide&lt;/a&gt;'s standard 202-plus-Location-header pattern.&lt;/p&gt;

&lt;p&gt;Here's the live Python sample, pulled straight from the &lt;a href="https://github.com/pdf4me/pdf4me-api-samples/tree/main/Edit/Add%20Margin%20to%20PDF/Python/Add%20Margin%20To%20PDF" rel="noopener noreferrer"&gt;official sample repository&lt;/a&gt;:&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="n"&gt;payload&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;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pdf_base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# Base64 encoded PDF document content
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docName&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;output.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# Output PDF file name
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marginLeft&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;# Left margin in millimeters (0-100)
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marginRight&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# Right margin in millimeters (0-100)
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marginTop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;# Top margin in millimeters (0-100)
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marginBottom&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;# Bottom margin in millimeters (0-100)
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isAsync&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;             &lt;span class="c1"&gt;# Enable asynchronous processing
&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="n"&gt;url&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="n"&gt;payload&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;verify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the last field name: &lt;code&gt;isAsync&lt;/code&gt;, not &lt;code&gt;async&lt;/code&gt;. The REST docs page itself labels this field &lt;code&gt;async&lt;/code&gt; in its parameter table. The live, working sample in the repository uses &lt;code&gt;isAsync&lt;/code&gt;. This is the same naming mismatch that's shown up on a few other PDF4me endpoints recently, worth double-checking against the &lt;a href="https://docs.pdf4me.com/url-api-tester/" rel="noopener noreferrer"&gt;API Tester&lt;/a&gt; before you write parsing or request-building code that assumes the docs table is field-accurate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that will actually bite you: required versus optional isn't consistent
&lt;/h2&gt;

&lt;p&gt;Here's the more interesting finding, and the reason this is worth an article instead of a changelog line. The REST endpoint treats all four margin fields as optional. Skip a field, that side simply doesn't move. &lt;a href="https://docs.pdf4me.com/integration/zapier/edit/add-margin-to-pdf/" rel="noopener noreferrer"&gt;Zapier's action&lt;/a&gt; matches that behavior exactly: all four fields are optional, and an omitted side defaults to a zero-millimeter margin, no change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/make/edit/add-margin-to-pdf/" rel="noopener noreferrer"&gt;Make&lt;/a&gt; and &lt;a href="https://docs.pdf4me.com/integration/n8n/edit/add-margin-to-pdf/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; do not work that way. Both platforms mark all four margin fields as &lt;strong&gt;required&lt;/strong&gt; in their own parameter tables. You don't get to omit a side; you have to explicitly pass &lt;code&gt;0&lt;/code&gt; for any edge you don't want to expand. Same underlying &lt;code&gt;AddMargin&lt;/code&gt; engine on PDF4me's backend, two different contracts depending on which surface is calling it. If you're porting a Zapier zap to a Make scenario (or vice versa) and you were relying on "just don't set the fields I don't need," that flow will break the moment it hits Make or n8n's required-field validation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/power-automate/edit/add-margin-to-pdf/" rel="noopener noreferrer"&gt;Power Automate's page&lt;/a&gt; adds a third wrinkle, and it's internally inconsistent on its own terms: only &lt;strong&gt;Left Margin&lt;/strong&gt; is marked required with asterisks in the parameter table, while the description text underneath Top, Bottom, and Right Margin each says "Default is 0 if not specified," which reads as optional. The table's asterisk placement and the prose don't fully agree with each other. If you're building a flow against this connector, test what happens when you actually leave Top, Bottom, or Right blank rather than trusting either the table or the paragraph alone.&lt;/p&gt;

&lt;p&gt;The practical takeaway: don't assume behavior carries over when you move an Add Margin call between PDF4me surfaces. Explicitly pass all four values, every time, regardless of which platform you're on. It costs nothing and it's the one pattern that works everywhere without relying on a default that may or may not exist on the surface you're calling from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this actually gets used
&lt;/h2&gt;

&lt;p&gt;The four independent margin values map to real print and compliance requirements, not abstract formatting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Binding preparation.&lt;/strong&gt; A left margin of 30 to 40mm (versus 20 to 25mm on the other three sides) leaves room for spiral binding, comb binding, or three-ring holes without eating into the printed content. &lt;a href="https://docs.pdf4me.com/integration/power-automate/edit/add-margin-to-pdf/" rel="noopener noreferrer"&gt;Power Automate's own workflow examples&lt;/a&gt; describe exactly this: insert a 25mm left margin for binding holes as one step in an automated print-preparation flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Academic formatting.&lt;/strong&gt; APA 7th edition, MLA 9th edition, and Chicago Manual of Style all specify exactly 1 inch, which is 25.4mm, not a rounded 25. &lt;a href="https://docs.pdf4me.com/integration/zapier/edit/add-margin-to-pdf/" rel="noopener noreferrer"&gt;Zapier's documentation&lt;/a&gt; calls this out directly and lists it as one of several standard presets alongside court-filing margins (a larger top margin for clerk stamps and case numbers) and mirrored book-layout margins for facing pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Header and footer clearance.&lt;/strong&gt; Add a top or bottom margin first, then place content in the new white space with &lt;a href="https://docs.pdf4me.com/pdf4me-api/edit/add-header-footer-to-pdf/" rel="noopener noreferrer"&gt;Add HTML Header Footer to PDF&lt;/a&gt; or &lt;a href="https://docs.pdf4me.com/pdf4me-api/edit/add-page-number-to-pdf/" rel="noopener noreferrer"&gt;Add Page Number to PDF&lt;/a&gt;. &lt;a href="https://docs.pdf4me.com/integration/n8n/edit/add-margin-to-pdf/" rel="noopener noreferrer"&gt;n8n's own tips&lt;/a&gt; explicitly recommend this order: create the clearance first, then stamp into it, rather than crowding existing content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Standardizing inconsistent incoming documents.&lt;/strong&gt; If a webhook or shared folder receives PDFs from external suppliers with no consistent layout, running every file through the same four margin values before archiving gives you a uniform page presentation without touching a single page of content.&lt;/p&gt;

&lt;p&gt;One more note on precision: all four documented fields are integers on the REST, Power Automate, Make, and n8n surfaces. Zapier's own page is the only one that explicitly shows a decimal example (&lt;code&gt;25.4&lt;/code&gt; for exact 1-inch academic margins) in its documentation, worth confirming directly in the &lt;a href="https://docs.pdf4me.com/url-api-tester/" rel="noopener noreferrer"&gt;API Tester&lt;/a&gt; before you build a workflow that depends on sub-millimeter precision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line summary
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AddMargin&lt;/code&gt; doesn't decorate a page, it resizes one, additively and without touching existing content. That's a genuinely useful, narrowly-scoped operation once you know it's doing that instead of the CSS-padding-style overlay the name might suggest. The behavior itself is consistent and predictable across every PDF4me surface. What isn't consistent is whether the four margin parameters are optional or required, and that's the detail worth checking before you ship, not after a batch job halfway through a workflow migration starts throwing validation errors nobody expected.&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://pdf4me.com/" rel="noopener noreferrer"&gt;pdf4me.com&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a href="https://docs.pdf4me.com/" rel="noopener noreferrer"&gt;docs.pdf4me.com&lt;/a&gt;&lt;br&gt;
Developer portal: &lt;a href="https://dev.pdf4me.com/" rel="noopener noreferrer"&gt;dev.pdf4me.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>pdf</category>
      <category>automation</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Sign PDF Is Not a Digital Signature: What PDF4me's Signing Endpoints Actually Do</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Fri, 28 Aug 2026 18:04:36 +0000</pubDate>
      <link>https://dev.to/pdf4me/sign-pdf-is-not-a-digital-signature-what-pdf4mes-signing-endpoints-actually-do-2ofd</link>
      <guid>https://dev.to/pdf4me/sign-pdf-is-not-a-digital-signature-what-pdf4mes-signing-endpoints-actually-do-2ofd</guid>
      <description>&lt;p&gt;Search PDF4me's docs for "digital signature" and you land on a page whose browser tab title reads "Sign PDF - Digital Signature API." Read past the headline and the page tells you the opposite: "This is a visual signature, not a cryptographic digital signature." That is not a typo. It is two different features sharing a search result, and if you build a contract pipeline assuming the REST endpoint gives you legal non-repudiation, you find out the hard way that it does not.&lt;/p&gt;

&lt;p&gt;This is worth untangling before you write any integration code, because the fix is simple once you know which tool does which job. It just is not the tool the page title points you toward first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What POST /api/v2/SignPdf actually does
&lt;/h2&gt;

&lt;p&gt;The REST &lt;a href="https://docs.pdf4me.com/pdf4me-api/edit/sign-pdf/" rel="noopener noreferrer"&gt;Sign PDF&lt;/a&gt; endpoint takes a PDF and a signature image, then composites the image onto the page you specify. Required fields: &lt;code&gt;docContent&lt;/code&gt; (Base64 PDF), &lt;code&gt;docName&lt;/code&gt;, &lt;code&gt;imageFile&lt;/code&gt; (Base64 signature image), &lt;code&gt;imageName&lt;/code&gt;, &lt;code&gt;alignX&lt;/code&gt; (Left, Center, Right), &lt;code&gt;alignY&lt;/code&gt; (Top, Middle, Bottom). Optional fields cover sizing (&lt;code&gt;widthInMM&lt;/code&gt;/&lt;code&gt;heightInMM&lt;/code&gt; or the pixel equivalents), margins, &lt;code&gt;opacity&lt;/code&gt;, &lt;code&gt;pages&lt;/code&gt;, &lt;code&gt;showOnlyInPrint&lt;/code&gt;, &lt;code&gt;isBackground&lt;/code&gt;, and async processing.&lt;/p&gt;

&lt;p&gt;Here is the official Python sample, trimmed to the request itself and verified against the live &lt;a href="https://github.com/pdf4me/pdf4me-api-samples/tree/main/Edit/Sign%20PDF" rel="noopener noreferrer"&gt;sample repository&lt;/a&gt;:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pdf4me.com/api/v2/SignPdf&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contract.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;rb&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;pdf_base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&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;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signature.png&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;rb&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;signature_base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&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;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;payload&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;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pdf_base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docName&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;signed-output.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;imageFile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;signature_base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;imageName&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;signature.png&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;alignX&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;Right&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;alignY&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;Bottom&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;widthInMM&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;50&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;heightInMM&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;25&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;marginXInMM&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;20&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;marginYInMM&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;20&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;opacity&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;100&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;isBackground&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isAsync&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Basic YOUR_API_KEY&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;url&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="n"&gt;payload&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One field name is worth flagging: the REST docs page's own optional-parameters table lists the async flag as &lt;code&gt;async&lt;/code&gt;. The official sample script in the repository above sends &lt;code&gt;isAsync&lt;/code&gt; instead, and that is the field that actually works against the live API. If you copy the parameter name straight from the docs table, use &lt;code&gt;isAsync&lt;/code&gt;, not &lt;code&gt;async&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Call this directly from your own backend in any language, no Acrobat, no browser, no human clicking a signature pad. That part is genuinely true to the pitch. What comes back is a PDF with an image glued to it at the position you specified. There is no certificate, no private key, no cryptographic hash tying the mark to the exact bytes of the document at signing time. Edit the PDF afterward and nothing in the file structure flags that the "signature" now sits on a modified document.&lt;/p&gt;

&lt;p&gt;That is a completely legitimate feature to want for internal approval stamps or a cosmetic "reviewed" mark. The problem is only that PDF4me's own page calls it a "Digital Signature API," and that phrase means something specific and different to anyone who has dealt with e-signature compliance before.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actual cryptographic signing looks like
&lt;/h2&gt;

&lt;p&gt;A cryptographic digital signature binds a private key to the exact contents of a document at signing time, in a way a third party can independently verify and that breaks visibly if the document changes afterward. That is what eIDAS and the ESIGN Act are built around when they talk about tamper-evidence and non-repudiation. PDF4me has this capability, documented consistently across three separate integration pages as certificate-based, X.509 signing with a &lt;code&gt;.pfx&lt;/code&gt; or &lt;code&gt;.p12&lt;/code&gt; certificate file and a password protecting its private key.&lt;/p&gt;

&lt;p&gt;The parameters look different because the job is different: a Certificate File, a Certificate Password, which page to sign, plus signer metadata (Signer Name, Sign Reason, Sign Location) embedded in the signature itself, and a &lt;code&gt;Visible&lt;/code&gt; toggle so the signature can stay cryptographically present while showing no visual mark at all. &lt;a href="https://docs.pdf4me.com/integration/power-automate/edit/digital-sign/" rel="noopener noreferrer"&gt;Digital Sign in Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/make/pdf/digital-sign/" rel="noopener noreferrer"&gt;Digital Sign PDF in Make&lt;/a&gt;, and &lt;a href="https://docs.pdf4me.com/integration/zapier/pdf/digital-sign-pdf/" rel="noopener noreferrer"&gt;Digital Sign PDF in Zapier&lt;/a&gt; all describe this the same way, down to the same certificate-plus-password input model.&lt;/p&gt;

&lt;p&gt;Here is the part that matters if you were planning to call this from your own code the way "not Acrobat" implies: none of the three platforms above expose Digital Sign as a plain REST endpoint. There is no &lt;code&gt;/api/v2/DigitalSignPdf&lt;/code&gt; in the API reference; a direct fetch of the URL pattern that would match it returns nothing, and the REST API's own Edit-category sidebar navigation, read directly off the live docs site, lists every signing-adjacent endpoint that exists and Digital Sign is not among them. n8n does not have a Digital Sign action documented at all, only the same visual Sign PDF.&lt;/p&gt;

&lt;p&gt;If a no-code connector platform is already part of your stack, this is a non-issue. If your architecture is backend-only by design, the honest options are routing the signing step through Make, Power Automate, or Zapier as one discrete piece of an otherwise custom pipeline, or handling certificate-based signing with your own library and using PDF4me's REST API for everything upstream and downstream of that one step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking the right one before you write any code
&lt;/h2&gt;

&lt;p&gt;The decision comes down to one question: whether this signature needs to survive a legal challenge. If someone could dispute the document was signed, or dispute it was not altered afterward, you need certificate-based signing, not an image overlay. That rules out the REST &lt;code&gt;Sign PDF&lt;/code&gt; endpoint regardless of how convenient it is to call directly.&lt;/p&gt;

&lt;p&gt;If the signature is cosmetic, an internal sign-off mark, a visual cue in a workflow where the real approval already happened somewhere else, the REST &lt;code&gt;Sign PDF&lt;/code&gt; endpoint is the right, lightweight tool, and it genuinely works the way the pitch implies: called from your own backend, no desktop software, no manual step.&lt;/p&gt;

&lt;p&gt;Worth testing the visual endpoint interactively before writing integration code, through the &lt;a href="https://docs.pdf4me.com/url-api-tester/sign-pdf/" rel="noopener noreferrer"&gt;Sign PDF API Tester&lt;/a&gt;, which, worth noting, is titled "Sign PDF Digitally" on its own page, the same naming pattern repeating a third time. Upload a sample PDF and a signature image, adjust alignment and sizing, and see the actual output before committing to code.&lt;/p&gt;

&lt;p&gt;Sign PDF is also documented per platform if you are automating elsewhere in your stack: &lt;a href="https://docs.pdf4me.com/integration/make/edit/sign-pdf/" rel="noopener noreferrer"&gt;Sign PDF in Make&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/power-automate/edit/sign-pdf/" rel="noopener noreferrer"&gt;Sign PDF in Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/zapier/edit/sign-pdf/" rel="noopener noreferrer"&gt;Sign PDF in Zapier&lt;/a&gt;, and &lt;a href="https://docs.pdf4me.com/integration/n8n/edit/sign-pdf/" rel="noopener noreferrer"&gt;Sign PDF in n8n&lt;/a&gt; all wrap the same visual-signature endpoint described above. If this is your first REST call against PDF4me, start with the &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;connect to the API guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The lesson underneath this is not really about signatures. Read the parameter table, not the page title, before building a compliance-sensitive step around a feature name. PDF4me's own documentation makes the distinction clearly once you are two paragraphs into the actual page. The title just does not warn you that you will need to.&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://pdf4me.com/" rel="noopener noreferrer"&gt;pdf4me.com&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a href="https://docs.pdf4me.com/" rel="noopener noreferrer"&gt;docs.pdf4me.com&lt;/a&gt;&lt;br&gt;
Developer portal: &lt;a href="https://dev.pdf4me.com/" rel="noopener noreferrer"&gt;dev.pdf4me.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>pdf</category>
      <category>security</category>
      <category>automation</category>
    </item>
    <item>
      <title>Flattening a PDF Form Is Not the Same as Protecting It</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Fri, 28 Aug 2026 13:14:20 +0000</pubDate>
      <link>https://dev.to/pdf4me/flattening-a-pdf-form-is-not-the-same-as-protecting-it-c6m</link>
      <guid>https://dev.to/pdf4me/flattening-a-pdf-form-is-not-the-same-as-protecting-it-c6m</guid>
      <description>&lt;p&gt;A finance team collects a signed intake form, flattens it so the fields stop looking like an editable form, and emails it out as the "final" version. Six months later someone asks why the flattened copy opened without a password, printed without restriction, and could still be pulled apart in any PDF editor that touches raw content streams. Nothing was ever protected. The team confused two operations that PDF4me ships as two entirely separate endpoints for a reason.&lt;/p&gt;

&lt;p&gt;Flattening and protecting solve different problems. One changes what a document looks like. The other changes who can open it and what they're allowed to do once it's open. Treating the first as a substitute for the second shows up constantly in automated document pipelines built on Power Automate, Make, Zapier, and n8n, where "flatten" is often the last node before a file gets shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Flatten PDF actually does
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.pdf4me.com/pdf4me-api/convert/flatten-pdf/" rel="noopener noreferrer"&gt;Flatten PDF&lt;/a&gt; REST endpoint (&lt;code&gt;POST /api/v2/FlattenPdf&lt;/code&gt;) merges AcroForm fields, XFA form data, annotations, signatures, and comments into the static page content. A filled text field stops being a field and becomes plain rendered text. A checkbox becomes a small rendered mark instead of an interactive control. The output has no form left to interact with, so nothing can be refilled or resubmitted through a form-aware viewer.&lt;/p&gt;

&lt;p&gt;The request body only needs two fields, per the endpoint's own parameter table: File Content (Base64) and File Name. Worth flagging honestly: the table names them File Content and File Name, but the request-example payload on the same docs page, and the official Python sample below, both use &lt;code&gt;docContent&lt;/code&gt; and &lt;code&gt;docName&lt;/code&gt; as the actual JSON keys, a naming mismatch this docs site carries on more than one endpoint. Match the payload example, not the table header.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;

&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unflattened-sample.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;rb&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;pdf_base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&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;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;payload&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;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pdf_base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docName&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;Flatten_output.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;isAsync&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&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;Basic &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&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;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&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.pdf4me.com/api/v2/FlattenPdf&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="n"&gt;payload&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="c1"&gt;# 200 -&amp;gt; binary PDF in response.content
# 202 -&amp;gt; poll the Location header URL until it returns 200
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the field name is &lt;code&gt;isAsync&lt;/code&gt; (lowercase i) in the real sample code, not &lt;code&gt;IsAsync&lt;/code&gt;. That's the entire feature: no password field, no permission flag, no encryption step. Flatten PDF was never designed to restrict access, and its own documentation doesn't claim otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the confusion starts
&lt;/h2&gt;

&lt;p&gt;The mix-up usually isn't accidental. It's inherited from a nearby field that does double duty. The &lt;a href="https://docs.pdf4me.com/pdf4me-api/forms/fill-a-pdf-form/" rel="noopener noreferrer"&gt;Fill a PDF Form&lt;/a&gt; endpoint has its own &lt;code&gt;KeepPdfEditable&lt;/code&gt; parameter, optional, defaulting to false. Leave it at the default and the filled PDF comes back with its form fields already flattened into static text, "useful for delivering a locked record of what was submitted," in the docs' own words. Set it to true and the recipient can reopen the same fields in any PDF viewer and change the values.&lt;/p&gt;

&lt;p&gt;That single boolean does exactly what Flatten PDF does on its own, bundled as a convenience step at the end of a fill operation. It's easy to read "flattens the form into static text" and file that mentally under "now it's secure." It isn't. A flattened PDF with &lt;code&gt;KeepPdfEditable: false&lt;/code&gt; is just as printable, copyable, and re-editable at the byte level (new text boxes, redaction bypass, image extraction) as any other unprotected file. It only stops the specific action of refilling the original form fields through form tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually restricts access
&lt;/h2&gt;

&lt;p&gt;That job belongs to &lt;a href="https://docs.pdf4me.com/pdf4me-api/security/protect-document/" rel="noopener noreferrer"&gt;Protect Document&lt;/a&gt; (&lt;code&gt;POST /api/v2/Protect&lt;/code&gt;). It takes a password and a &lt;code&gt;pdfPermission&lt;/code&gt; value and returns a new AES-encrypted PDF. The two fields protect against different things: the password gates opening the file at all, while &lt;code&gt;pdfPermission&lt;/code&gt; gates what a user can do once they're past the password. &lt;code&gt;pdfPermission&lt;/code&gt; is allow-list, not deny-list, so setting it to &lt;code&gt;None&lt;/code&gt; blocks printing, copying, editing, annotating, and form filling in one move, and setting it to &lt;code&gt;Fill Forms&lt;/code&gt; opens exactly one door while every other action stays shut.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;

&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sample.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;rb&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;pdf_base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&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;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;payload&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;docName&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;output.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;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pdf_base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&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;Str0ng-P@ss!&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;pdfPermission&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;Fill Forms&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;isAsync&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&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;Basic &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&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;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&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.pdf4me.com/api/v2/Protect&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="n"&gt;payload&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The docs' own parameter table calls this field &lt;code&gt;async&lt;/code&gt;, but the official sample code sends &lt;code&gt;isAsync&lt;/code&gt;, the same lowercase-i naming this docs site uses on the Flatten endpoint too, so treat that as the working name rather than what the table prints.&lt;/p&gt;

&lt;p&gt;It's telling that PDF4me's own Fill a PDF Form documentation names Protect Document as the natural next step, not an alternative: "apply password protection and permissions after filling," recommended specifically for filled forms going out over email. That's the sequence the platform itself points toward, and it's worth testing both endpoints interactively before wiring them into a pipeline, through the &lt;a href="https://docs.pdf4me.com/url-api-tester/flatten-pdf/" rel="noopener noreferrer"&gt;Flatten PDF API Tester&lt;/a&gt; and the &lt;a href="https://docs.pdf4me.com/url-api-tester/protect-document/" rel="noopener noreferrer"&gt;Protect Document API Tester&lt;/a&gt;, to see the request and response shape before any code gets written. Authentication for either endpoint follows the same &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;Connect to the PDF4me V2 API&lt;/a&gt; pattern: an API key in the Authorization header, nothing exotic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-step pattern, not a one-step shortcut
&lt;/h2&gt;

&lt;p&gt;Put together, a filled document that genuinely needs to leave the building locked down runs through three separate operations, each doing one job:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fill&lt;/strong&gt; the form with the submitted data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flatten&lt;/strong&gt; it (or set &lt;code&gt;KeepPdfEditable: false&lt;/code&gt; during fill) so the visual record can't be casually refilled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect&lt;/strong&gt; it with a password and a &lt;code&gt;pdfPermission&lt;/code&gt; value so it can't be opened, printed, or copied by anyone who shouldn't have it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Skipping step three because step two already happened is the exact mistake this article opened with. Flattening is a formatting decision. Protecting is an access-control decision. A document can be flattened and wide open, or unflattened and locked down tight (Protect Document doesn't care whether the source PDF still has live form fields), and most real workflows want both, in that order, for different reasons.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same gap exists in no-code workflows
&lt;/h2&gt;

&lt;p&gt;None of this is unique to raw API calls. The same Flatten PDF operation is exposed as a dedicated module or node in &lt;a href="https://docs.pdf4me.com/integration/make/convert/flatten-pdf/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/power-automate/pdf/flatten-pdf/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/zapier/edit/flatten-pdf/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, and &lt;a href="https://docs.pdf4me.com/integration/n8n/convert/flatten-pdf/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;, each one wrapping the identical REST call behind a visual drag-and-drop step. If a workflow builder drops a Flatten action at the end of a Fill Form scenario and calls it done, the resulting document has exactly the same access-control gap as the raw API version. No-code doesn't change what the endpoint does; it just makes it faster to skip the protection step by accident, since there's no error, no warning, and no failed request. The flatten succeeds every time, whether or not anything downstream actually needed it to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limit
&lt;/h2&gt;

&lt;p&gt;None of this makes a flattened-and-protected PDF unbreakable. &lt;code&gt;pdfPermission&lt;/code&gt; restrictions are enforced by compliant PDF readers, not by the file format itself, the same limitation every PDF permission system has always had. A password gates who can open the file; it does not gate what happens to the bytes after a determined reader ignores its own enforcement. For document distribution and archival, that's still a meaningfully higher bar than an unprotected flattened file sitting in an inbox, but it's worth knowing what the guarantee actually is before promising it to a compliance team.&lt;/p&gt;

&lt;p&gt;The fix here isn't a new feature. It's reading past the name of the endpoint you're already calling, and asking what problem it was actually built to solve before assuming it solved a different one.&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://pdf4me.com/" rel="noopener noreferrer"&gt;pdf4me.com&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a href="https://docs.pdf4me.com/" rel="noopener noreferrer"&gt;docs.pdf4me.com&lt;/a&gt;&lt;br&gt;
Developer portal: &lt;a href="https://dev.pdf4me.com/" rel="noopener noreferrer"&gt;dev.pdf4me.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>api</category>
      <category>security</category>
      <category>automation</category>
    </item>
    <item>
      <title>Rendering Pixel-Accurate PDFs From Raw HTML, No Headless Browser Required</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Thu, 27 Aug 2026 18:18:24 +0000</pubDate>
      <link>https://dev.to/pdf4me/rendering-pixel-accurate-pdfs-from-raw-html-no-headless-browser-required-pc8</link>
      <guid>https://dev.to/pdf4me/rendering-pixel-accurate-pdfs-from-raw-html-no-headless-browser-required-pc8</guid>
      <description>&lt;p&gt;If you've ever generated a PDF from HTML, you've probably met the headless browser. Puppeteer, Playwright, wkhtmltopdf, some flavor of Chromium running invisibly on a server somewhere, waiting to render a page and hand you back a PDF. It works. It also means you're now responsible for keeping a browser binary alive in production: patching it, sizing memory for it, restarting it when it silently hangs on a font that never loaded. That's a lot of infrastructure for what is, conceptually, a single conversion step.&lt;/p&gt;

&lt;p&gt;PDF4me's &lt;a href="https://docs.pdf4me.com/pdf4me-api/convert/html-to-pdf/" rel="noopener noreferrer"&gt;Convert HTML to PDF&lt;/a&gt; endpoint skips that entirely. You send HTML, base64-encoded, to a single REST endpoint. PDF4me renders it server-side and hands back a finished PDF. No browser to install, no headless process to babysit. PDF4me's own &lt;a href="https://docs.pdf4me.com/integration/make/convert/html-to-pdf/" rel="noopener noreferrer"&gt;Make integration page&lt;/a&gt; puts it plainly: the module "renders HTML content or a public web URL into a finished PDF document without a headless browser or extra rendering tools." That's the whole pitch, and it's a useful one if you've spent an afternoon debugging why a headless Chrome instance ran out of memory on a Friday.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the endpoint actually takes
&lt;/h2&gt;

&lt;p&gt;The REST call is &lt;code&gt;POST /api/v2/ConvertHtmlToPdf&lt;/code&gt;. At minimum you're sending &lt;code&gt;docContent&lt;/code&gt; (your HTML, base64-encoded), &lt;code&gt;docName&lt;/code&gt; for the output file, and &lt;code&gt;indexFilePath&lt;/code&gt;, which is where things get more useful than they first look. You're not limited to a single HTML file: PDF4me accepts a ZIP archive containing HTML plus its referenced assets (CSS files, images, fonts), and &lt;code&gt;indexFilePath&lt;/code&gt; tells PDF4me which file inside that ZIP is the entry point. If your invoice template pulls in a separate stylesheet and a logo image, you don't need to inline everything into one file. Zip it, point PDF4me at the index page, done.&lt;/p&gt;

&lt;p&gt;Here's a live-verified Python sample, adapted from PDF4me's own &lt;a href="https://github.com/pdf4me/pdf4me-api-samples/tree/main/CONVERT/Convert%20HTML%20To%20PDF/Python/Convert%20HTML%20To%20PDF" rel="noopener noreferrer"&gt;pdf4me-api-samples&lt;/a&gt; repo (MIT licensed):&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;

&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# from https://dev.pdf4me.com/dashboard/#/api-keys/
&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pdf4me.com/api/v2/ConvertHtmlToPdf&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice.html&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;rb&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;html_base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&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;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;payload&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;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;html_base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docName&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;output.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;indexFilePath&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.html&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;layout&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;Portrait&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;format&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;A4&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;scale&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topMargin&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;40px&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;bottomMargin&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;40px&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;leftMargin&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;40px&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;rightMargin&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;40px&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;printBackground&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;displayHeaderFooter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;async&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&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;Basic &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&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;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;url&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="n"&gt;payload&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="k"&gt;if&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;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output.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;span class="k"&gt;elif&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;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Async: poll the Location header URL until it returns 200
&lt;/span&gt;    &lt;span class="n"&gt;location_url&lt;/span&gt; &lt;span class="o"&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;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth flagging here since I hit both while cross-checking the docs against the actual sample code. First, the real request payload sends a lowercase &lt;code&gt;"async": true&lt;/code&gt;, while the &lt;a href="https://docs.pdf4me.com/url-api-tester/convert-html-to-pdf/" rel="noopener noreferrer"&gt;API Tester&lt;/a&gt; for this same endpoint documents it as &lt;code&gt;IsAsync&lt;/code&gt;. Second, the &lt;a href="https://docs.pdf4me.com/pdf4me-api/convert/html-to-pdf/" rel="noopener noreferrer"&gt;Convert HTML to PDF&lt;/a&gt; REST page's own text says scale runs "between 0 and 0.8," but the working sample code's own comment, the API Tester, and the sibling &lt;a href="https://docs.pdf4me.com/pdf4me-api/convert/url-to-pdf/" rel="noopener noreferrer"&gt;Convert URL to PDF&lt;/a&gt; page all agree on a 0.1 to 2.0 range where 1.0 is original size. If precise scaling matters, test the real boundary in the API Tester rather than trusting either doc page blindly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where your page dimensions actually come from
&lt;/h2&gt;

&lt;p&gt;Here's the part that trips people up coming from a headless-browser mental model: &lt;code&gt;layout&lt;/code&gt; only controls Portrait versus Landscape. It does not control page size, margins, or fonts by itself. Those come from the CSS inside your HTML. PDF4me's Make integration guide is direct about this: "Page dimensions and margins in the output PDF come entirely from the CSS in your HTML... the Layout dropdown only sets Portrait vs Landscape orientation." If you want a true A4 page with a 20mm margin, you write it into a style block with an &lt;code&gt;@page&lt;/code&gt; rule, not into a PDF4me parameter.&lt;/p&gt;

&lt;p&gt;That same guide surfaces a gotcha worth repeating because it will bite you exactly once and then never again: inline styles and embedded &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; blocks render reliably, but external stylesheets loaded via a &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tag only work if PDF4me can reach that URL publicly at render time. If your CSS lives behind a login, a VPN, or a staging environment PDF4me's servers can't hit, your styling silently disappears from the output. No error, just a PDF that doesn't look like your preview. Inline it or embed it if you're not certain the link is public.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same endpoint, four different shapes
&lt;/h2&gt;

&lt;p&gt;The REST call is the mechanism. Each integration platform wraps it differently, and the differences matter for which one fits your stack.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/make/convert/html-to-pdf/" rel="noopener noreferrer"&gt;Make&lt;/a&gt; gives you a two-field module: &lt;code&gt;Document / public file URL&lt;/code&gt; accepts either binary HTML content from an earlier module or, notably, a live public web address directly. If the page you want already exists and is publicly reachable, you can skip building HTML entirely and just point Make at the URL.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/zapier/convert/convert-html-to-pdf/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt; keeps the same idea even simpler: map an HTML file, choose Portrait or Landscape, done. It's the thinnest wrapper of the four.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/power-automate/convert/html-to-pdf/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt; mirrors the REST parameter set closely, including ZIP support with an index file path, which fits teams already pulling multi-file HTML templates out of SharePoint or OneDrive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/n8n/convert/convert-html-to-pdf/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; is the most flexible of the four. Its node accepts four separate input types: Binary Data, Base64 String, raw HTML Code typed directly into the node, or a public URL. That HTML Code option is genuinely useful for quick internal tooling, you can write throwaway HTML straight into the workflow without a prior node generating it.&lt;/p&gt;

&lt;p&gt;If you'd rather convert an already-live page directly instead of assembling HTML yourself, that's a different endpoint: &lt;a href="https://docs.pdf4me.com/pdf4me-api/convert/url-to-pdf/" rel="noopener noreferrer"&gt;Convert URL to PDF&lt;/a&gt; takes a web address plus optional authentication (Basic, OAuth, or API key) and handles the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it before you write a line of code
&lt;/h2&gt;

&lt;p&gt;Every parameter above is testable directly in the &lt;a href="https://docs.pdf4me.com/url-api-tester/convert-html-to-pdf/" rel="noopener noreferrer"&gt;API Tester&lt;/a&gt; without writing a request by hand. Upload a file, fill the fields, hit send, and you get back either a 200 with the PDF, or a 202 with a &lt;code&gt;Location&lt;/code&gt; header to poll if you set async processing on. That async path matters if your HTML is large or you're batching, since polling means you're not holding a connection open waiting on a synchronous render.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worth doing without a browser at all
&lt;/h2&gt;

&lt;p&gt;Converting HTML to PDF is one of the more common document automation asks there is: invoices built from a template, weekly reports assembled from a database query, an email body archived as a compliance record. What changes when you route it through a rendering API instead of a headless browser is what you're no longer responsible for. No browser process to keep patched. No memory leak from a page that never finished loading. No debugging why a font rendered differently in your CI container than on your laptop. You send HTML and parameters, you get back a PDF.&lt;/p&gt;

&lt;p&gt;That's the trade worth understanding before you reach for Puppeteer out of habit: a REST call has a fixed, documented parameter surface. Does a headless browser really need to be your default anymore?&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://pdf4me.com/" rel="noopener noreferrer"&gt;pdf4me.com&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a href="https://docs.pdf4me.com/" rel="noopener noreferrer"&gt;docs.pdf4me.com&lt;/a&gt;&lt;br&gt;
Developer portal: &lt;a href="https://dev.pdf4me.com/" rel="noopener noreferrer"&gt;dev.pdf4me.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>automation</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Turning a Word Form Into a Fillable PDF Without Redesigning It</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Wed, 26 Aug 2026 13:06:05 +0000</pubDate>
      <link>https://dev.to/pdf4me/turning-a-word-form-into-a-fillable-pdf-without-redesigning-it-309a</link>
      <guid>https://dev.to/pdf4me/turning-a-word-form-into-a-fillable-pdf-without-redesigning-it-309a</guid>
      <description>&lt;p&gt;Somewhere in your company there is a Word document with a Developer tab open, a handful of content controls dropped in, and a name like &lt;code&gt;intake-form-v4-FINAL.docx&lt;/code&gt;. It works fine, as long as everyone filling it out has Word. The moment you need to send it to a client, a candidate, or a customer who might be on a phone or a shared kiosk PC, "has Word" stops being a safe assumption. What they do have, almost without exception, is something that can open a PDF.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/pdf4me-api/convert/convert-word-to-pdf-form/" rel="noopener noreferrer"&gt;Convert Word to PDF Form&lt;/a&gt; exists for exactly that handoff. You are not redesigning the form. You are not rebuilding it field by field in a separate PDF form builder. You send the Word file you already built, and you get back a PDF where those same fields still work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the endpoint actually does
&lt;/h2&gt;

&lt;p&gt;The REST API exposes one endpoint: &lt;code&gt;POST /api/v2/ConvertWordToPdfForm&lt;/code&gt;. The request body needs two fields: &lt;code&gt;docContent&lt;/code&gt;, the Word file as Base64, and &lt;code&gt;docName&lt;/code&gt;, the output filename such as &lt;code&gt;output.pdf&lt;/code&gt;. Live-verified against both the &lt;a href="https://docs.pdf4me.com/pdf4me-api/convert/convert-word-to-pdf-form/" rel="noopener noreferrer"&gt;REST docs page&lt;/a&gt; and the official &lt;a href="https://github.com/pdf4me/pdf4me-api-samples/tree/main/CONVERT/Convert%20Word%20To%20PDF%20Form/Python/Convert%20Word%20To%20PDF%20Form" rel="noopener noreferrer"&gt;Python sample&lt;/a&gt; in the pdf4me-api-samples repo, which also sends a third field, &lt;code&gt;async&lt;/code&gt;, and authenticates with Basic auth against &lt;code&gt;api.pdf4me.com&lt;/code&gt; rather than the docs host:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;

&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pdf4me.com/api/v2/ConvertWordToPdfForm&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;intake_form.docx&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;rb&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;word_base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&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;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;payload&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;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;word_base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docName&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;output.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;async&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&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;Basic &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&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;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;url&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="n"&gt;payload&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="k"&gt;if&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;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output.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;span class="k"&gt;elif&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;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Async job accepted, poll response.headers["Location"] until 200
&lt;/span&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth flagging: the sample repo's own README describes the request as &lt;code&gt;multipart/form-data&lt;/code&gt;, but the actual script it ships sends &lt;code&gt;application/json&lt;/code&gt;, exactly like the REST docs page. The script is the one that runs, so that's the version to trust. The API Tester page for this endpoint also references an &lt;code&gt;IsAsync&lt;/code&gt; flag rather than the lowercase &lt;code&gt;async&lt;/code&gt; the working sample sends; if you're building this by hand rather than starting from the sample, test both field names against your own request in the &lt;a href="https://docs.pdf4me.com/url-api-tester/convert-word-to-pdf-form/" rel="noopener noreferrer"&gt;API Tester&lt;/a&gt; before assuming either works.&lt;/p&gt;

&lt;p&gt;Authentication otherwise works the same as every other PDF4me REST call: an API key from the &lt;a href="https://dev.pdf4me.com/dashboard/#/api-keys/" rel="noopener noreferrer"&gt;PDF4me dashboard&lt;/a&gt;. If this is your first PDF4me endpoint, the &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;connect-to-API guide&lt;/a&gt; covers the base URL and header format once, for every endpoint after this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually converts, and where the docs disagree with each other
&lt;/h2&gt;

&lt;p&gt;This is worth reading carefully before you build anything on top of this. PDF4me's own documentation describes what this endpoint preserves in two noticeably different ways, and the more detailed of the two is the more conservative one.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.pdf4me.com/integration/make/convert/word-to-pdf-form/" rel="noopener noreferrer"&gt;Make module page&lt;/a&gt; is the most specific source available, and it says plainly: the Word document must contain formal content controls inserted through Word's Developer tab, specifically Text controls, Check Box controls, and Drop-Down List controls. Text controls become PDF text input fields, checkboxes stay checkboxes, dropdown controls become PDF combo boxes. Everything else, meaning plain underlined blanks, table cells styled to look like a form, or regular body text, gets rendered as static, non-editable content in the output PDF. Make's own FAQ repeats this twice, in slightly different wording, which reads like a page written by someone who has actually watched this conversion fail on a form that only looked like it had fields.&lt;/p&gt;

&lt;p&gt;The REST API page and the &lt;a href="https://docs.pdf4me.com/integration/zapier/convert/convert-word-to-pdf-form/" rel="noopener noreferrer"&gt;Zapier action page&lt;/a&gt;, by contrast, both list a longer set of "advanced controls" the endpoint supposedly preserves: rich text fields, file upload fields, calculation fields, digital signature fields, and combo boxes, on top of the standard text, checkbox, dropdown, and date picker set. Neither page explains how you would author a "calculation field" as a native Word content control, and Make's own documentation, which is otherwise the most thorough of the four integration pages, never mentions any of them. Given that the request payload itself is only &lt;code&gt;docContent&lt;/code&gt;, &lt;code&gt;docName&lt;/code&gt;, and &lt;code&gt;async&lt;/code&gt;, there is no parameter that would let you configure which control types to preserve. The most defensible read: the endpoint reliably handles Word's standard content controls, text, checkbox, dropdown, and date picker, and the "advanced controls" list on the REST and Zapier pages describes a broader ambition than a plain Word content control can actually represent. If your form needs something more exotic, test it through the &lt;a href="https://docs.pdf4me.com/url-api-tester/convert-word-to-pdf-form/" rel="noopener noreferrer"&gt;API Tester&lt;/a&gt; before you build a workflow around it.&lt;/p&gt;

&lt;p&gt;One more distinction worth having straight: this conversion keeps form fields interactive. It does not flatten them. If you need a locked, non-editable copy for archiving or after a signature is collected, that's a separate step, &lt;a href="https://docs.pdf4me.com/pdf4me-api/convert/flatten-pdf/" rel="noopener noreferrer"&gt;Flatten PDF&lt;/a&gt;, chained after this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four ways to put this in a workflow
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Make.&lt;/strong&gt; The &lt;a href="https://docs.pdf4me.com/integration/make/convert/word-to-pdf-form/" rel="noopener noreferrer"&gt;Convert Word to PDF Form module&lt;/a&gt; needs only a Connection, File Name, and the Document binary. No optional parameters exist. Map File Name with its &lt;code&gt;.docx&lt;/code&gt; or &lt;code&gt;.doc&lt;/code&gt; extension intact, since that's how the engine identifies the source format, and map Document from whatever preceding module downloaded or generated the file. A typical scenario: a new client record triggers the scenario, Dropbox downloads the intake form template, this module converts it, and Gmail emails the fillable PDF to the client. Chain a &lt;a href="https://docs.pdf4me.com/pdf4me-api/forms/fill-a-pdf-form/" rel="noopener noreferrer"&gt;Fill PDF Form&lt;/a&gt; module afterward to pre-populate any fields, such as a client's name or an event date, before the recipient ever opens it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zapier.&lt;/strong&gt; The &lt;a href="https://docs.pdf4me.com/integration/zapier/convert/convert-word-to-pdf-form/" rel="noopener noreferrer"&gt;Convert Word to PDF Form action&lt;/a&gt; takes File Content and File Name as its only required fields, and returns File Content, File Name, File URL, Job Id, and Trace Id. A Zap here might trigger the moment a new Word application form is finalized in Google Drive, convert it, and route the resulting PDF to an applicant tracking system or an email step. Zapier's own tips page is blunt about the most common failure mode: a Word document with underlines or boxed table cells styled to look like a form has nothing for this action to actually preserve, since none of that is a real content control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Power Automate.&lt;/strong&gt; The &lt;a href="https://docs.pdf4me.com/integration/power-automate/convert/convert-word-to-pdf-form/" rel="noopener noreferrer"&gt;Convert Word to PDF Form action&lt;/a&gt; fits naturally into a Microsoft 365 flow: File Content (binary) and File Name are both required, and the connector accepts sources directly from SharePoint or OneDrive. This is a natural fit for HR onboarding paperwork or benefits enrollment forms that already live in a SharePoint library: retrieve the Word template, convert it, and route the resulting fillable PDF to the employee or applicant without anyone touching Word.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;n8n.&lt;/strong&gt; The &lt;a href="https://docs.pdf4me.com/integration/n8n/convert/convert-word-to-pdf-form/" rel="noopener noreferrer"&gt;Convert Word to PDF Form node&lt;/a&gt; is the most flexible of the four on input handling. It accepts Binary Data, a Base64 string, or a public URL as the source, useful if your Word templates live somewhere you'd rather reference directly than download through an extra node first. Input File Name, Output File Name, and a Binary Data Output Name are required; an Advanced Options section adds a Custom Profiles field for JSON-based configuration, and the documented example includes a &lt;code&gt;preserveFormFields&lt;/code&gt; flag alongside &lt;code&gt;outputDataFormat&lt;/code&gt;. Since the base REST payload has no equivalent parameter, treat that flag as n8n-specific until you've confirmed what it changes for your own document. The response comes back with a &lt;code&gt;success&lt;/code&gt; boolean, an &lt;code&gt;operation&lt;/code&gt; field, and a human-readable &lt;code&gt;message&lt;/code&gt;, structured enough to build real error handling around instead of guessing from an HTTP status code alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test it before you automate it
&lt;/h2&gt;

&lt;p&gt;Before wiring this into any of the four platforms above, it's worth confirming the request shape and the actual conversion behavior directly. The &lt;a href="https://docs.pdf4me.com/url-api-tester/convert-word-to-pdf-form/" rel="noopener noreferrer"&gt;Convert Word to PDF Form page in the API Tester&lt;/a&gt; lets you send your API key and a Base64 Word document and see the response in the browser, no code required. It's the fastest way to find out, on your own form, whether every content control you expect to survive actually does, before that question surfaces three steps deep in a Make scenario you have to unwind to debug.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this replaces
&lt;/h2&gt;

&lt;p&gt;The alternative to this endpoint is not usually "no fillable PDF." It's a second, parallel form built by hand in a dedicated PDF form tool, disconnected from the Word document someone in HR, legal, or sales is still the one actually maintaining. Every time that Word template changes, someone has to remember to rebuild the PDF version too, or the two quietly drift apart. Converting from the Word source directly means the form template has exactly one place to live.&lt;/p&gt;

&lt;p&gt;One honest gap worth flagging: PDF4me doesn't yet have a dedicated blog walkthrough for this specific endpoint, so beyond the reference docs and the sample repo linked above, that repo is currently the most complete worked example available.&lt;/p&gt;




&lt;p&gt;Website: &lt;a href="https://pdf4me.com/" rel="noopener noreferrer"&gt;pdf4me.com&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a href="https://docs.pdf4me.com/" rel="noopener noreferrer"&gt;docs.pdf4me.com&lt;/a&gt;&lt;br&gt;
Developer portal: &lt;a href="https://dev.pdf4me.com/" rel="noopener noreferrer"&gt;dev.pdf4me.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>automation</category>
      <category>api</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Markdown In, PDF Out: A Docs-as-Code Pipeline in One API Call</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Tue, 25 Aug 2026 07:59:56 +0000</pubDate>
      <link>https://dev.to/pdf4me/markdown-in-pdf-out-a-docs-as-code-pipeline-in-one-api-call-13mp</link>
      <guid>https://dev.to/pdf4me/markdown-in-pdf-out-a-docs-as-code-pipeline-in-one-api-call-13mp</guid>
      <description>&lt;p&gt;Every team that writes docs-as-code eventually hits the same wall. Your README lives in Markdown. Your changelog lives in Markdown. Your architecture notes, your onboarding guide, your API reference, all Markdown, all sitting next to the code where version control can track every change. Then someone in legal, procurement, or a client's IT department asks for "the documentation" and means a PDF. Not a GitHub link. Not a rendered HTML page. A file they can attach to an email, print, or archive.&lt;/p&gt;

&lt;p&gt;That request used to mean opening a Markdown file in some GUI tool, exporting it by hand, and doing it again next release. &lt;a href="https://docs.pdf4me.com/pdf4me-api/convert/markdown-to-pdf/" rel="noopener noreferrer"&gt;Convert Markdown to PDF&lt;/a&gt; turns that into a single REST call, which means it turns into a step in a pipeline instead of a chore on someone's calendar.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the endpoint actually does
&lt;/h2&gt;

&lt;p&gt;The REST API exposes one endpoint for this: &lt;code&gt;POST /api/v2/ConvertMdToPdf&lt;/code&gt; against the base URL &lt;code&gt;https://api.pdf4me.com&lt;/code&gt;, per the &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;Connect to PDF4me API guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two parameters are required: &lt;strong&gt;File Content&lt;/strong&gt;, the Markdown file encoded as Base64, and &lt;strong&gt;File Name&lt;/strong&gt;, the source filename with its extension, for example &lt;code&gt;document.md&lt;/code&gt;. There's one optional parameter, &lt;strong&gt;Markdown File Path&lt;/strong&gt;, which only matters if you're sending a ZIP archive instead of a single file. If your input is a ZIP of Markdown files, this field tells PDF4me which file inside the archive to render, something like &lt;code&gt;docs/readme.md&lt;/code&gt;. Leave it empty for a direct &lt;code&gt;.md&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;The JSON payload uses &lt;code&gt;docContent&lt;/code&gt; for the Base64 source, &lt;code&gt;docName&lt;/code&gt; for the output name, and &lt;code&gt;mdFilePath&lt;/code&gt; for the in-archive path when relevant, alongside &lt;code&gt;IsAsync&lt;/code&gt; for polling behavior on larger files. Send those fields with your API key in the Authorization header, and the response comes back with the converted file content and name, ready to save, attach, or pipe into the next step.&lt;/p&gt;

&lt;p&gt;What actually gets preserved matters more than the endpoint shape. The conversion handles the Markdown syntax that documentation is actually built from: headers from H1 through H6 with correct hierarchy, bold, italic, strikethrough, and inline code, ordered and unordered lists with proper nesting, fenced code blocks with syntax highlighting, links, embedded images, and tables with column alignment intact. That's the difference between a PDF that looks like documentation and one that looks like a wall of plain text with stray asterisks in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal working request
&lt;/h2&gt;

&lt;p&gt;Here's the shape of a real call, field names verified against the live docs page and the API Tester's own quick reference:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;README.md&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;rb&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;md_base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&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;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;payload&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;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;md_base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docName&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;README.md&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;mdFilePath&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="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;# only needed when docContent is a ZIP
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IsAsync&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&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;Content-Type&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;application/json&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;Authorization&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;Basic YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&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.pdf4me.com/api/v2/ConvertMdToPdf&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="n"&gt;payload&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="k"&gt;if&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;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;README.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;out&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="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;span class="k"&gt;elif&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;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# large file, poll the Location header until it returns 200
&lt;/span&gt;    &lt;span class="n"&gt;poll_url&lt;/span&gt; &lt;span class="o"&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;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&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;status_code&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;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick note on the docs page itself: the generic payload example shown there (&lt;code&gt;docName: "output.pdf"&lt;/code&gt;) is boilerplate shared across every Convert endpoint's page and doesn't match this endpoint's own parameter table, which is explicit that File Name is the &lt;em&gt;source&lt;/em&gt; Markdown filename, not the output PDF name. The API Tester's quick reference confirms the real field set: &lt;code&gt;api-key&lt;/code&gt;, &lt;code&gt;docContent&lt;/code&gt;, &lt;code&gt;docName&lt;/code&gt;, &lt;code&gt;mdFilePath&lt;/code&gt;, &lt;code&gt;IsAsync&lt;/code&gt;. Trust the parameter table and the tester over the boilerplate example if you ever see the two disagree.&lt;/p&gt;

&lt;p&gt;Status 200 means the PDF came back in the response body immediately. Status 202 means PDF4me accepted the job and is processing it asynchronously. On the &lt;a href="https://docs.pdf4me.com/url-api-tester/convert-markdown-to-pdf/" rel="noopener noreferrer"&gt;API Tester's live page&lt;/a&gt; for this endpoint, setting &lt;code&gt;IsAsync&lt;/code&gt; to true is exactly how you'd trigger that path for a larger file, then poll the &lt;code&gt;Location&lt;/code&gt; header URL until it resolves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Batch conversion, not just one file at a time
&lt;/h2&gt;

&lt;p&gt;Documentation rarely lives as a single file. A docs folder might hold dozens of &lt;code&gt;.md&lt;/code&gt; files: guides, references, changelogs, contributing notes. The endpoint's ZIP support exists for exactly this. Send a ZIP archive as the file content, and use &lt;strong&gt;Markdown File Path&lt;/strong&gt; to point at the specific file inside it you want rendered for that call. To convert an entire folder, you run the endpoint once per file, which is precisely the kind of repetitive step a workflow tool should own instead of a person.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four ways to wire it into a pipeline
&lt;/h2&gt;

&lt;p&gt;A docs-as-code pipeline generally looks the same regardless of which automation tool sits in the middle: something triggers on a documentation change, the Markdown gets fetched, PDF4me converts it, and the PDF lands somewhere a human or a customer can find it. Here's what that looks like on each platform PDF4me supports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make.&lt;/strong&gt; The &lt;a href="https://docs.pdf4me.com/integration/make/convert/markdown-to-pdf/" rel="noopener noreferrer"&gt;Convert Markdown to PDF module&lt;/a&gt; takes a Connection, File Name, Document (the binary buffer), and conditionally Markdown File Path for ZIP input. It maps cleanly onto a scenario that triggers on a new GitHub release, downloads the README, converts it, and attaches the result to the release assets. Two things worth knowing before you build this: relative image paths in your Markdown may not resolve when the module renders the PDF, so use absolute URLs for embedded images, and the module converts one file per run, so processing a full docs folder means placing a Repeater or Iterator ahead of it. Make's own docs note the module follows the CommonMark spec, with GitHub-flavored extensions like tables and task lists handled per &lt;a href="https://docs.github.com/en/get-started/writing-on-github" rel="noopener noreferrer"&gt;GitHub's Markdown guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zapier.&lt;/strong&gt; The &lt;a href="https://docs.pdf4me.com/integration/zapier/pdf/markdown-to-pdf/" rel="noopener noreferrer"&gt;Markdown To PDF action&lt;/a&gt; takes File Name and File Content as required fields, with Markdown File Path again reserved for ZIP input, and returns a direct URL to the converted PDF alongside the filename and extension. A typical Zap here triggers when documentation updates in a GitHub repository, retrieves the latest Markdown files, converts each one, and can chain into watermarking, packaging, and publishing steps before notifying a channel or customer list that new docs are live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Power Automate.&lt;/strong&gt; The &lt;a href="https://docs.pdf4me.com/integration/power-automate/convert/convert-markdown-to-pdf/" rel="noopener noreferrer"&gt;Convert Markdown To PDF action&lt;/a&gt; fits naturally into a Microsoft 365 flow: File Content and Input File Name are required, Markdown File Path is conditional on ZIP input, and the output comes back as binary File Content plus File Name ready to drop into SharePoint, attach to an Outlook email, or archive in OneDrive. This is the natural home for a flow that watches a SharePoint documentation library, converts new or updated &lt;code&gt;.md&lt;/code&gt; files, and routes the PDF to stakeholders without anyone touching Word.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;n8n.&lt;/strong&gt; The &lt;a href="https://docs.pdf4me.com/integration/n8n/convert/markdown-to-pdf/" rel="noopener noreferrer"&gt;Convert Markdown to PDF node&lt;/a&gt; is the most flexible of the four on input handling: it accepts Binary Data, a Base64 string, or a public URL as the Markdown source, which matters if your documentation lives somewhere you'd rather point at than download through an extra node. Document Name, Output File Name, and Output Binary Field Name are required; an Advanced Options section adds Custom Profiles for JSON-based configuration like &lt;code&gt;outputDataFormat&lt;/code&gt;. The response includes not just the file but a &lt;code&gt;success&lt;/code&gt; boolean and a &lt;code&gt;message&lt;/code&gt; field, which is the kind of structured signal that makes error handling in a longer workflow far less guesswork.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a full pipeline looks like end to end
&lt;/h2&gt;

&lt;p&gt;Put the pieces together and a docs-as-code-to-PDF pipeline looks roughly like this: a trigger fires on a documentation change, whether that's a GitHub release, a SharePoint file update, or a scheduled batch job. The relevant Markdown file, or a ZIP of several, gets fetched. PDF4me converts it, preserving headers, code blocks, tables, and links exactly as they were written. From there the PDF can be watermarked with a version number, merged with other converted files into a single package, and pushed to wherever it needs to land: a customer portal, an email attachment, a documentation archive, or all three.&lt;/p&gt;

&lt;p&gt;None of this requires anyone to open a Markdown-to-PDF desktop tool ever again. The source of truth stays in version control, in Markdown, exactly where a docs-as-code workflow wants it, and the PDF becomes a generated artifact instead of a manually maintained parallel copy that drifts out of sync the first time someone forgets to update it.&lt;/p&gt;

&lt;p&gt;One honest gap worth flagging: PDF4me doesn't yet have a dedicated blog walkthrough for this specific endpoint, so if you're looking for a worked example beyond the reference docs and code samples linked above, the &lt;a href="https://github.com/pdf4me/pdf4me-api-samples" rel="noopener noreferrer"&gt;GitHub samples repository&lt;/a&gt; is currently the most complete place to start, alongside the &lt;a href="https://docs.pdf4me.com/url-api-tester/convert-markdown-to-pdf/" rel="noopener noreferrer"&gt;interactive API Tester&lt;/a&gt; for trying a request before you write any code at all.&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://pdf4me.com/" rel="noopener noreferrer"&gt;pdf4me.com&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a href="https://docs.pdf4me.com/" rel="noopener noreferrer"&gt;docs.pdf4me.com&lt;/a&gt;&lt;br&gt;
Developer portal: &lt;a href="https://dev.pdf4me.com/" rel="noopener noreferrer"&gt;dev.pdf4me.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>pdf</category>
      <category>automation</category>
      <category>api</category>
    </item>
    <item>
      <title>Testing Every PDF4me Endpoint Before You Write a Line of Integration Code</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Tue, 25 Aug 2026 07:25:13 +0000</pubDate>
      <link>https://dev.to/pdf4me/testing-every-pdf4me-endpoint-before-you-write-a-line-of-integration-code-2l42</link>
      <guid>https://dev.to/pdf4me/testing-every-pdf4me-endpoint-before-you-write-a-line-of-integration-code-2l42</guid>
      <description>&lt;p&gt;You open a docs page for an endpoint you have never called, read the parameter table once, and start writing the request body from memory. Which field takes the file, docContent or file? Does the filename go in a separate field, and does the extension actually matter, or is that just convention? You send the request. It comes back 401. Not because your logic is wrong. Because you guessed at one header and guessed wrong.&lt;/p&gt;

&lt;p&gt;That twenty minutes is the tax every developer pays the first time they touch a new API, and it has nothing to do with how good the documentation is. Reading about a request and sending one are different skills. PDF4me's answer to that gap is the &lt;a href="https://docs.pdf4me.com/url-api-tester/" rel="noopener noreferrer"&gt;Interactive API Tester&lt;/a&gt;, and the useful thing about it is not that it exists. It is that there is a dedicated page for every single endpoint, not one generic form you have to reconfigure each time.&lt;/p&gt;

&lt;h2&gt;
  
  
  One tool, 78 pages, zero code
&lt;/h2&gt;

&lt;p&gt;Most API explorers give you a single generic interface and expect you to know which fields matter for the operation you are running. PDF4me's tester inverts that. There are &lt;a href="https://docs.pdf4me.com/url-api-tester/" rel="noopener noreferrer"&gt;78 endpoint-specific pages&lt;/a&gt;, each one already built around the exact inputs that one action expects. Want to see what &lt;a href="https://docs.pdf4me.com/url-api-tester/parse-document/" rel="noopener noreferrer"&gt;Parse Document&lt;/a&gt; actually returns for a real invoice? There is a page for exactly that, not a generic call-any-endpoint console you have to configure from scratch. Want to check how &lt;a href="https://docs.pdf4me.com/url-api-tester/merge-multiple-pdf-files/" rel="noopener noreferrer"&gt;Merge Multiple PDF Files&lt;/a&gt; behaves when you throw three files at it instead of two? Same story. &lt;a href="https://docs.pdf4me.com/url-api-tester/add-barcode-to-pdf/" rel="noopener noreferrer"&gt;Add Barcode to PDF&lt;/a&gt; and &lt;a href="https://docs.pdf4me.com/url-api-tester/fill-pdf-form/" rel="noopener noreferrer"&gt;Fill PDF Form&lt;/a&gt; each have their own dedicated form too.&lt;/p&gt;

&lt;p&gt;Every page follows the same shape. Upload the source file, fill in the parameters through dropdowns and text fields instead of hand-typing JSON, submit, and watch the response come back in real time: the processed output file, ready to download, alongside the raw JSON response and the HTTP status code the server actually returned. You are not reading a static example response copied into a docs page months ago. You are looking at what the API does with your file, right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it's actually good for
&lt;/h2&gt;

&lt;p&gt;The docs describe five concrete uses, and they map to five real moments in a build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verifying behavior before you write a line of integration code.&lt;/strong&gt; You know exactly what a response looks like before you write a single line of parsing logic against it, instead of discovering the shape of the JSON after your code already assumes something different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing edge cases on purpose.&lt;/strong&gt; Password-protected files, unusually large uploads, specific page ranges. These are the inputs that tend to surface bugs weeks after launch, precisely because nobody tried them during development. Trying them here costs nothing but a few minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exploring which parameters produce which output.&lt;/strong&gt; Some parameters are easier to see the effect of than to read about. Watermark opacity, compression profile, page range syntax. Changing a value and immediately seeing the result beats re-reading a parameter description three times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debugging by comparing expected versus actual.&lt;/strong&gt; If your production integration returns something you did not expect, running the identical operation through the tester isolates the variable. Is the API behaving differently than you assumed, or is the bug in your own request construction? The tester answers that in one request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demonstrating capability without writing anything.&lt;/strong&gt; A working demo for a stakeholder, a proof of concept for a teammate deciding whether an endpoint fits their use case. None of that requires a sandbox environment or a single line of integration code.&lt;/p&gt;

&lt;h2&gt;
  
  
  From the tester to real code
&lt;/h2&gt;

&lt;p&gt;The tester is a bridge, not a destination. Once you have confirmed an endpoint does what you need, the next step is wiring it into your own application, and the &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;Connect to the PDF4me V2 API&lt;/a&gt; reference is what you translate that confirmation into. The contract is consistent across the whole API: every endpoint is a POST request to &lt;code&gt;https://api.pdf4me.com&lt;/code&gt;, with a JSON body, an &lt;code&gt;Authorization&lt;/code&gt; header carrying your API key, and file content sent as a Base64 string in a &lt;code&gt;docContent&lt;/code&gt; field. The &lt;code&gt;docName&lt;/code&gt; field is not cosmetic. It carries the file extension, and that extension is part of what the request needs to process the file correctly.&lt;/p&gt;

&lt;p&gt;Here is what that looks like for &lt;a href="https://docs.pdf4me.com/pdf4me-api/convert/convert-to-pdf/" rel="noopener noreferrer"&gt;Convert to PDF&lt;/a&gt;, taken directly from the live request example on its docs page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST https://api.pdf4me.com/api/v2/ConvertToPdf
Content-Type: application/json
Authorization: YOUR_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"docContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;base64 content of a .docx, .pptx, .xlsx, or image file&amp;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;"docName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"output"&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;And here is the same shape for &lt;a href="https://docs.pdf4me.com/pdf4me-api/pdf/get-pdf-metadata/" rel="noopener noreferrer"&gt;Get PDF Metadata&lt;/a&gt;, a different endpoint entirely, using the same two required fields:&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;"docContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;base64 content of the PDF&amp;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;"docName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"output.pdf"&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;Same two fields, same header pattern, different endpoint path and a completely different response shape on the way back. That consistency is exactly why testing five different endpoints in the browser first is worth the time: you are not learning five different request formats, you are confirming the same format works for five different jobs, and catching the field-level details specific to each one along the way.&lt;/p&gt;

&lt;p&gt;Getting to that first authenticated request starts at &lt;a href="https://docs.pdf4me.com/general-guidelines/getting-started-api-portal/" rel="noopener noreferrer"&gt;Getting Started with the API Portal&lt;/a&gt;: register, activate access to your key, and open the &lt;a href="https://dev.pdf4me.com/dashboard/#/api-keys/" rel="noopener noreferrer"&gt;dashboard&lt;/a&gt; to copy it. The tester and your eventual integration code authenticate the exact same way. Nothing changes when you move from one to the other except that you are now writing the request instead of filling in a form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where testing in the browser catches the gap early
&lt;/h2&gt;

&lt;p&gt;That 401 from the opening scenario is not hypothetical. A missing, malformed, or revoked API key produces exactly that response, and PDF4me's own &lt;a href="https://docs.pdf4me.com/general-guidelines/troubleshooting/401-unauthorized/" rel="noopener noreferrer"&gt;401 Unauthorized troubleshooting page&lt;/a&gt; exists because it is one of the most common ways a first integration attempt goes wrong. Hit that error inside the tester, where the only variable is the key you pasted into a form field, and it takes seconds to fix. Hit it three layers deep inside application code you just wrote, where the bug could be your header construction, your encoding, your key rotation logic, or the request body itself, and it takes a lot longer to isolate.&lt;/p&gt;

&lt;p&gt;That is the actual value on offer here. Not that the tester replaces writing code. It never will, and it is not built to run load tests, chain multiple endpoints into a workflow, or serve as a regression suite for your CI pipeline. Those are jobs for your own test harness, or for a no-code platform if the workflow itself belongs in Zapier, Make, Power Automate, or n8n rather than custom code. What the tester does is remove the guessing from the first contact with an endpoint, so the code you eventually write is built against something you have actually seen work, not something you assumed would.&lt;/p&gt;




&lt;p&gt;Website: &lt;a href="https://pdf4me.com/" rel="noopener noreferrer"&gt;pdf4me.com&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a href="https://docs.pdf4me.com/" rel="noopener noreferrer"&gt;docs.pdf4me.com&lt;/a&gt;&lt;br&gt;
Developer portal: &lt;a href="https://dev.pdf4me.com/" rel="noopener noreferrer"&gt;dev.pdf4me.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>devtools</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Teaching 70+ Coding Agents the Right PDF4me Endpoint for the Job</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Mon, 24 Aug 2026 10:44:12 +0000</pubDate>
      <link>https://dev.to/pdf4me/teaching-70-coding-agents-the-right-pdf4me-endpoint-for-the-job-22im</link>
      <guid>https://dev.to/pdf4me/teaching-70-coding-agents-the-right-pdf4me-endpoint-for-the-job-22im</guid>
      <description>&lt;p&gt;Ask a coding agent to "compress this PDF using the PDF4me API" and it will happily write you something. Whether that something is correct is a different question. Most general-purpose coding assistants have never seen PDF4me's actual endpoint list, its auth header format, or the shape of a real request body, so they guess. Sometimes the guess is close. Sometimes it invents a parameter that does not exist, or routes a compression request to a conversion endpoint because the names sound similar.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/pdf4me-agent-skills/getting-started/" rel="noopener noreferrer"&gt;PDF4me Agent Skills&lt;/a&gt; exist to close that gap. A skill is an installable package that teaches an AI coding assistant, directly and specifically, how the PDF4me REST API actually works: which endpoint handles which job, what the auth convention looks like, what a payload is supposed to contain. Instead of a model reconstructing that from training data, it reads it from a reference file sitting in your own project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a skill actually is
&lt;/h2&gt;

&lt;p&gt;The current skill, &lt;code&gt;pdf4me-api&lt;/code&gt;, is hosted on GitHub at &lt;a href="https://github.com/pdf4me/pdf4me-skills" rel="noopener noreferrer"&gt;github.com/pdf4me/pdf4me-skills&lt;/a&gt;. Live-checking the repo itself (not just the docs page) surfaces the exact skill structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;skills/pdf4me-api/SKILL.md         # manifest, routing, and usage guidance
skills/pdf4me-api/references/      # API reference notes grouped by feature area
skills/pdf4me-api/scripts/         # executable helpers for common flows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you ask your agent to write PDF4me integration code, it consults these references before it writes a line, the same way a careful developer would open the docs before guessing at a parameter name. The skill covers PDF, Word, Excel, and image processing tasks through the REST API: conversion, editing, merge and split, OCR, extraction, generation, barcode, forms, and security operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to install it, verified against the live repo
&lt;/h2&gt;

&lt;p&gt;The docs page and the repo's own README describe installation slightly differently, and the repo is the more current source. Two paths exist, and which one you use depends on which agent you run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;, via the plugin marketplace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add pdf4me/pdf4me-skills
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;pdf4me@pdf4me-plugin
/reload-plugins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Invoke it explicitly afterward with &lt;code&gt;/pdf4me:pdf4me-api&lt;/code&gt;, or describe a PDF4me task and let Claude Code trigger it automatically. You can also try the skill without installing anything, using &lt;code&gt;claude --plugin-dir /path/to/pdf4me-skills&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codex, Cursor, GitHub Copilot, Cline, Gemini CLI, Windsurf, Zed, and 70+ other agents&lt;/strong&gt;, via the &lt;code&gt;npx skills&lt;/code&gt; CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add https://github.com/pdf4me/pdf4me-skills.git &lt;span class="nt"&gt;--skill&lt;/span&gt; pdf4me-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command opens an interactive picker: select which installed agents should receive the skill, choose project or global scope, and pick an installation method. Symlink installation is worth choosing deliberately, since it makes future updates to the skill's reference files propagate automatically instead of leaving you on a stale copy. Under project scope, the files land at &lt;code&gt;.agents/skills/pdf4me-api/&lt;/code&gt;, checked into your repo like any other project asset your team and your agent both read. In Codex specifically, invoke the skill explicitly with &lt;code&gt;$pdf4me-api&lt;/code&gt;, or let Codex select it automatically from a plain-language request.&lt;/p&gt;

&lt;p&gt;That 70-plus figure is not marketing shorthand. It reflects how many coding tools the &lt;code&gt;npx skills&lt;/code&gt; CLI has adapters for. If your team runs five different agents across five different engineers, one install command handles all of them instead of five separate integration writeups.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent actually gets
&lt;/h2&gt;

&lt;p&gt;This is what turns "compress this PDF" from a guess into a grounded answer. The skill documents endpoint routing, auth conventions, and payload shapes, category by category. This matters more than it sounds like it should, because PDF4me's REST API spans a genuinely wide surface, with separate endpoint families and their own request shapes for each category. A generalist coding agent with no grounding will, at best, get the common cases right and quietly misfire on the rest.&lt;/p&gt;

&lt;p&gt;The auth convention itself: every request needs an &lt;code&gt;Authorization: Basic &amp;lt;api_key&amp;gt;&lt;/code&gt; header. The skill documents this format but does not store or transmit your key. That key comes from the &lt;a href="https://dev.pdf4me.com/dashboard/#/api-keys/" rel="noopener noreferrer"&gt;PDF4me API dashboard&lt;/a&gt;, the same place covered in &lt;a href="https://docs.pdf4me.com/general-guidelines/getting-started-api-portal/" rel="noopener noreferrer"&gt;Getting Started with the PDF4me API Portal&lt;/a&gt; for anyone setting up an account for the first time. The underlying reference both the skill and this section are built on is &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;Connect to the PDF4me V2 API&lt;/a&gt;, which documents the base URL (&lt;code&gt;https://api.pdf4me.com&lt;/code&gt;), the &lt;code&gt;POST /api/v2/&lt;/code&gt; endpoint pattern, and the full response-code table, live-confirmed while writing this piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skills versus MCP, briefly
&lt;/h2&gt;

&lt;p&gt;If you followed this cluster's earlier piece on wiring &lt;a href="https://docs.pdf4me.com/integration/pdf4me-mcp/getting-started/" rel="noopener noreferrer"&gt;PDF4me's MCP server&lt;/a&gt; into Cursor, VS Code, Claude Desktop, and Windsurf, the natural question is which mechanism to reach for. MCP exposes live callable tools an agent invokes directly during a chat session. Agent Skills load static, structured guidance the agent reads and then writes its own code from, whether that is a curl command, a Python script, or a full integration module. Use Agent Skills when the assistant should generate REST API code, curl, or scripts. Use MCP when the client should call PDF4me tools directly during the conversation. Both can be installed together, and a developer who wants an agent that can both call PDF4me live and also write standalone integration code has a real reason to run both at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the loop before you ship
&lt;/h2&gt;

&lt;p&gt;Once your agent, guided by the skill, generates a request against a PDF4me endpoint, you do not have to trust it blind. The &lt;a href="https://docs.pdf4me.com/url-api-tester/" rel="noopener noreferrer"&gt;Interactive API Tester&lt;/a&gt; is a form-based, no-code way to fire the exact same endpoint yourself: upload a source file, set the parameters the agent's generated code claims to be setting, and compare what actually comes back against what the agent told you to expect. It is a fast way to catch a subtly wrong parameter before it ships inside a production integration, and it works whether or not you trust the code the agent just handed you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limit
&lt;/h2&gt;

&lt;p&gt;Agent Skills teach an assistant how the API is shaped. They do not replace testing, and they do not guarantee the model applies the guidance correctly every time, particularly on edge cases the reference files describe briefly rather than exhaustively. Treat generated code the way you would treat a capable but new engineer's first draft: probably closer to right than a guess from nothing, still worth a second look before it touches production data. For a team running several different coding agents, the alternative, each engineer's agent independently guessing at the same wide API surface, is a worse starting point than one shared, versioned reference every agent reads from the same place.&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://pdf4me.com/" rel="noopener noreferrer"&gt;pdf4me.com&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a href="https://docs.pdf4me.com/" rel="noopener noreferrer"&gt;docs.pdf4me.com&lt;/a&gt;&lt;br&gt;
Developer portal: &lt;a href="https://dev.pdf4me.com/" rel="noopener noreferrer"&gt;dev.pdf4me.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devtools</category>
      <category>api</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Dead Links Inside a Generated PDF, Repointed Without Rebuilding the File</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:36:48 +0000</pubDate>
      <link>https://dev.to/pdf4me/dead-links-inside-a-generated-pdf-repointed-without-rebuilding-the-file-3eh9</link>
      <guid>https://dev.to/pdf4me/dead-links-inside-a-generated-pdf-repointed-without-rebuilding-the-file-3eh9</guid>
      <description>&lt;p&gt;Somewhere in your document pipeline right now, there's a PDF with a hyperlink pointing at the wrong place. A "Pay Now" button on an invoice that still points at a staging checkout URL from three environments ago. A "View your policy" link in a generated insurance document that points at a page your CMS retired last quarter. A partner portal link that worked fine at generation time and now 404s, because whoever owns that redirect changed it without telling anyone who touches your documents.&lt;/p&gt;

&lt;p&gt;None of this is rare. It's what happens when links get baked into a document at generation time and nobody revisits them after the fact. The document itself is fine. Every other field is correct. But the one clickable thing a reader might actually act on sends them somewhere wrong, and by the time someone notices, hundreds or thousands of copies are already sitting in inboxes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The instinct that wastes an afternoon
&lt;/h2&gt;

&lt;p&gt;The obvious fix is to regenerate the document. Pull the source data, fix the URL in the template, re-run the generation job, redistribute. That works if you still have the source, the template hasn't drifted, and you're only fixing one document. It stops working the moment any of those conditions fail: the source system has moved on, the template that produced this specific batch is three versions old, or you're staring at ten thousand PDFs that were generated correctly at the time and just need one link swapped everywhere.&lt;/p&gt;

&lt;p&gt;Regenerating from source treats the symptom as if it were the whole document. It isn't. The text, the layout, the compliance boilerplate, the signature block, all of that is correct and doesn't need to change. The only thing wrong is a target URL sitting underneath a clickable region. Rebuilding an entire file to fix one pointer is a lot of blast radius for a small, well-defined problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually broken: the annotation layer, not the document
&lt;/h2&gt;

&lt;p&gt;A hyperlink in a PDF isn't part of the visible text. It's a separate object, a link annotation, that PDF viewers overlay on a region of the page and route to a target when clicked. The text underneath can say "Pay Now" or "View your policy" and never change; the annotation is what decides where the click actually goes. That separation is what makes a targeted fix possible: you can update where a link points, or the display text tied to it, without touching a single pixel of the rendered page.&lt;/p&gt;

&lt;p&gt;That's what PDF4me's &lt;a href="https://docs.pdf4me.com/pdf4me-api/pdf4me/update-hyperlinks-annotation/" rel="noopener noreferrer"&gt;Update Hyperlink Annotation&lt;/a&gt; endpoint does. It operates on the annotation layer directly, changing hyperlink text, the destination URL, or both, in an existing PDF. No source template, no regeneration, no re-rendering the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The endpoint, verified
&lt;/h2&gt;

&lt;p&gt;The REST endpoint is a single POST call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://api.pdf4me.com/api/v2/UpdateHyperlinkAnnotation
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each hyperlink change is described by a &lt;code&gt;SearchOn&lt;/code&gt;/&lt;code&gt;SearchValue&lt;/code&gt; pair that identifies the target, plus the current and new values for both the display text and the URL:&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;"docName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"output.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"docContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JVBERi..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"updatehyperlinkannotationlist"&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;"SearchOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"SearchValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://www.google.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"IsExpression"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"TextCurrentValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://www.google.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"TextNewValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://pdf4me.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"URLCurrentValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://www.google.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"URLNewValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://pdf4me.com"&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;"async"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;&lt;code&gt;updatehyperlinkannotationlist&lt;/code&gt; takes an array, so one call can carry as many corrections as a document needs, each with its own search target and its own current/new text and URL pair. That's the batch-correction case from a single request: fix every stale link in a document, or across a set of documents, in one pass instead of one-by-one.&lt;/p&gt;

&lt;p&gt;A quick verification note, because it's worth naming when it happens: PDF4me's official Python sample for this endpoint (in &lt;a href="https://github.com/pdf4me/pdf4me-api-samples/tree/main/PDF4me/Update%20Hyperlinks%20Annotation/Python/Update%20Hyperlinks%20Annotation" rel="noopener noreferrer"&gt;pdf4me-api-samples&lt;/a&gt;) confirms the endpoint path exactly as shown above, singular &lt;code&gt;UpdateHyperlinkAnnotation&lt;/code&gt;, matching the live docs page. That repo's own README describes a different payload shape (a plural endpoint name and separate &lt;code&gt;hyperlinks&lt;/code&gt;/&lt;code&gt;annotations&lt;/code&gt; arrays) that doesn't match either the live docs page or the actual &lt;code&gt;.py&lt;/code&gt; script sitting next to it, only the README text is off. The code sample and the docs page agree with each other; this article follows those two, not the README.&lt;/p&gt;

&lt;p&gt;Here's the working Python call, trimmed from that sample:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pdf4me.com/api/v2/UpdateHyperlinkAnnotation&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sample.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;rb&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;base64_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&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;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;payload&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;docName&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;output.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;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;base64_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updatehyperlinkannotationlist&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="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SearchOn&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;Text&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;SearchValue&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;http://www.google.com&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;IsExpression&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TextCurrentValue&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;http://www.google.com&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;TextNewValue&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;https://pdf4me.com&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;URLCurrentValue&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;http://www.google.com&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;URLNewValue&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;https://pdf4me.com&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;async&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&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;Content-Type&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;application/json&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;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;Basic &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&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;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="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;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="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&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;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Synchronous: binary PDF content comes back directly
&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hyperlinks_updated_PDF_output.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;out&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="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;span class="k"&gt;elif&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;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Asynchronous: poll the Location header until the job finishes
&lt;/span&gt;    &lt;span class="n"&gt;location_url&lt;/span&gt; &lt;span class="o"&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;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 200 response hands back the corrected PDF's binary content directly. A 202 means the job is processing asynchronously, useful for larger files or batches, and the response carries a &lt;code&gt;Location&lt;/code&gt; header to poll until it's done. Same shape developers will recognize from other PDF4me endpoints: fast documents resolve inline, heavier jobs hand you a job to check on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fires in a real pipeline
&lt;/h2&gt;

&lt;p&gt;Three shapes of this problem show up constantly once you start looking for them.&lt;/p&gt;

&lt;p&gt;Domain migrations are the most common. A company moves its customer portal from one domain to another, or a marketing team reworks a URL structure for SEO reasons, and every previously generated document that linked to the old structure is now technically broken, even though nothing about the document itself changed. Nobody wants to regenerate a year of historical invoices because the checkout domain moved.&lt;/p&gt;

&lt;p&gt;Redirect rot is the quiet version of the same problem. A link worked at generation time because it pointed at a redirect, and the redirect target changed later, or got removed entirely. The document was correct when it was made. It just didn't stay correct, because the thing it depended on wasn't under the document's control.&lt;/p&gt;

&lt;p&gt;Batch correction is the operational version: you generated a thousand documents overnight, a QA pass the next morning catches that one link across the whole batch points at the wrong place, and you need to fix that one link on every file without touching anything else on any of them. Rebuilding a thousand documents from source to fix one URL is the kind of afternoon nobody wants, and it's exactly what the array-based payload above is built for.&lt;/p&gt;

&lt;h2&gt;
  
  
  How each low-code surface exposes it
&lt;/h2&gt;

&lt;p&gt;If you'd rather try the endpoint against a real document before wiring it into code, PDF4me's &lt;a href="https://docs.pdf4me.com/url-api-tester/update-hyperlinks-annotation/" rel="noopener noreferrer"&gt;interactive API Tester&lt;/a&gt; runs it directly in the browser, useful for confirming your search values and matching logic before either becomes part of a pipeline.&lt;/p&gt;

&lt;p&gt;For no-code and low-code builds, Make, Zapier, and n8n each expose this as its own node. Make frames it as a &lt;a href="https://docs.pdf4me.com/integration/make/edit/update-hyperlinks/" rel="noopener noreferrer"&gt;batch link editor&lt;/a&gt;: search for link text or a URL inside a scenario and replace it, which is exactly the shape a domain-migration or redirect-cleanup job takes. Zapier's equivalent is described the same way, a &lt;a href="https://docs.pdf4me.com/integration/zapier/pdf/update-hyperlinks-annotation/" rel="noopener noreferrer"&gt;batch link editor&lt;/a&gt; for searching and replacing link text or URLs across a Zap. n8n's &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me/update-hyperlinks-annotation/" rel="noopener noreferrer"&gt;Update Hyperlinks Annotation&lt;/a&gt; node does the same job inside a workflow, modifying existing hyperlink annotations by text, URL, or both, which slots naturally into a document-correction workflow that's already watching a folder or a webhook for new files.&lt;/p&gt;

&lt;p&gt;Worth naming honestly: as of this writing, there's no Power Automate page for this specific endpoint in PDF4me's documentation. If your pipeline runs on Power Automate, this capability isn't available there the way it is on Make, Zapier, and n8n. That's a real gap, not an oversight in this article, and worth checking PDF4me's &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-power-automate/" rel="noopener noreferrer"&gt;Power Automate integration guides&lt;/a&gt; directly if this is a blocker for your stack, since integration coverage does expand over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't solve
&lt;/h2&gt;

&lt;p&gt;It's worth being precise about the boundary here, because it's easy to conflate adjacent problems. Update Hyperlink Annotation changes real link annotations, the clickable objects PDF viewers recognize as links. It does not turn plain text that merely looks like a URL into a clickable one, and it doesn't touch a hyperlink that was flattened into the page during rendering rather than kept as a live annotation.&lt;/p&gt;

&lt;p&gt;If what you actually need is to add a brand-new clickable link to text that currently has none, that's a different job: &lt;a href="https://docs.pdf4me.com/integration/make/pdf/create-hyperlinks/" rel="noopener noreferrer"&gt;creating hyperlinks in a PDF via Make&lt;/a&gt; or the equivalent in &lt;a href="https://docs.pdf4me.com/integration/zapier/edit/create-hyperlinks-in-pdf/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, not updating one. And if the goal is auditing what's already there before deciding what to fix, PDF4me's link extraction tools in &lt;a href="https://docs.pdf4me.com/integration/make/pdf/extract-hyperlinks/" rel="noopener noreferrer"&gt;Make&lt;/a&gt; and &lt;a href="https://docs.pdf4me.com/integration/zapier/extract/extract-hyperlinks-from-pdf/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt; pull every hyperlink out of a document as structured data, URL, link text, and page number, exactly what you'd run first across a batch before deciding which links actually need repointing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the link layer before you touch the source
&lt;/h2&gt;

&lt;p&gt;The habit worth building is small but easy to skip under pressure: before regenerating anything, ask whether the actual problem lives on the annotation layer. If a document is correct except for where one link points, the fix is narrower than rebuilding the file, and it scales to a thousand documents exactly as easily as it scales to one. Get started with &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;connecting to the PDF4me API&lt;/a&gt; if you're wiring this into your own code, or reach for whichever automation platform already runs your document pipeline. Either way, fix the problem at the layer where it actually lives.&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://pdf4me.com/" rel="noopener noreferrer"&gt;pdf4me.com&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a href="https://docs.pdf4me.com/" rel="noopener noreferrer"&gt;docs.pdf4me.com&lt;/a&gt;&lt;br&gt;
Developer portal: &lt;a href="https://dev.pdf4me.com/" rel="noopener noreferrer"&gt;dev.pdf4me.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>automation</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Reading a Swiss QR Bill Back Into Structured Payment Data</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Fri, 21 Aug 2026 16:11:24 +0000</pubDate>
      <link>https://dev.to/pdf4me/reading-a-swiss-qr-bill-back-into-structured-payment-data-28g4</link>
      <guid>https://dev.to/pdf4me/reading-a-swiss-qr-bill-back-into-structured-payment-data-28g4</guid>
      <description>&lt;p&gt;Somewhere in a finance team's inbox is a supplier invoice with a Swiss QR bill at the bottom of it. Since Switzerland's QR-bill standard took effect, every Swiss payment slip looks like this: a QR code sitting under a printed block of IBAN, amount, and reference details, replacing the old red and orange slips entirely. It is a clean, standardized format. It is also, for whoever has to get that data into accounting software, still a QR code someone scans, reads, and retypes by hand. Standardizing the printed layout solved a printing problem. It did not solve a data-entry problem, because scanning a QR code and understanding what it means are two very different things.&lt;/p&gt;

&lt;p&gt;That gap is where most Swiss QR bill automation attempts stall. A generic QR scanner decodes the code fine and hands back a wall of text, because a Swiss QR bill does not encode a URL or a short string the way a marketing poster does. It encodes the Swiss Payments Code: a strict, multi-line data block carrying the creditor's IBAN, the payment amount and currency, a reference number, and more, all packed into a specific line order defined by the Swiss payment standard. Read that with a generic decoder and what comes back is technically correct and practically useless, a blob of text with no keys and no structure that still needs a second parsing layer written by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the endpoint actually returns
&lt;/h2&gt;

&lt;p&gt;PDF4me's &lt;a href="https://docs.pdf4me.com/pdf4me-api/barcode/read-swissqr-code/" rel="noopener noreferrer"&gt;Read Swiss QR Code&lt;/a&gt; endpoint skips that second layer entirely.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;POST /api/v2/ReadSwissQRBill&lt;/code&gt; takes a base64-encoded PDF and decodes the embedded QR bill directly into structured payment data as a JSON object, not a raw text dump. Three fields go in: &lt;code&gt;docContent&lt;/code&gt; (the base64 PDF), &lt;code&gt;docName&lt;/code&gt; (the filename), and &lt;code&gt;async&lt;/code&gt; (true for larger batches, using the standard 202-plus-polling pattern). What comes back, once decoded, is a &lt;code&gt;swissQrCodeData&lt;/code&gt; object with the fields a finance workflow actually needs already separated: &lt;code&gt;amount&lt;/code&gt;, &lt;code&gt;currency&lt;/code&gt;, &lt;code&gt;iban&lt;/code&gt;, &lt;code&gt;creditorName&lt;/code&gt;, &lt;code&gt;paymentReference&lt;/code&gt;, plus &lt;code&gt;dueDate&lt;/code&gt; and &lt;code&gt;purpose&lt;/code&gt; when the bill carries them.&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;"fileName"&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.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mimeType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fileSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2456789&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"swissQrCodeData"&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;"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;"100.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CHF"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"iban"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CH9300762011623852957"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"creditorName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Swiss Company AG"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"paymentReference"&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-2024-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dueDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-12-31"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"purpose"&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 payment"&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 distinction matters more than it sounds like it should. PDF4me also has a general-purpose &lt;a href="https://docs.pdf4me.com/pdf4me-api/barcode/read-barcode-from-pdf/" rel="noopener noreferrer"&gt;Read Barcode from PDF&lt;/a&gt; endpoint, and it is tempting to assume one barcode reader should handle every barcode. It will read the QR code's raw payload just fine. What it will not do is know that one block of that payload is the creditor's IBAN and another is the reference number, or that the reference needs different handling depending on whether it is a QR reference or a Creditor Reference under ISO 11649. Read Swiss QR Code exists specifically because that interpretation layer is the actual work, and it is not worth repeating by hand in every integration a team builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  A working example
&lt;/h2&gt;

&lt;p&gt;Here is the request in Python, adapted from &lt;a href="https://github.com/pdf4me/pdf4me-api-samples/tree/main/Barcode/Read%20SwissQR%20Code" rel="noopener noreferrer"&gt;PDF4me's official sample repository&lt;/a&gt;:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# from https://dev.pdf4me.com/dashboard/#/api-keys/
&lt;/span&gt;&lt;span class="n"&gt;pdf_file_path&lt;/span&gt; &lt;span class="o"&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="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pdf4me.com/api/v2/ReadSwissQRBill&lt;/span&gt;&lt;span class="sh"&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="n"&gt;pdf_file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&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;pdf_base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&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;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;payload&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;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pdf_base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_file_path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;async&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&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;Basic &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&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;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;url&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="n"&gt;payload&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="k"&gt;if&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;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;swissQrCodeData&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;elif&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;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Async: poll the Location header until it returns 200
&lt;/span&gt;    &lt;span class="n"&gt;location_url&lt;/span&gt; &lt;span class="o"&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;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;poll&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location_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;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;poll&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;swissQrCodeData&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&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;Error: &lt;/span&gt;&lt;span class="si"&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;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - &lt;/span&gt;&lt;span class="si"&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;text&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The async branch matters more here than it does for most PDF4me endpoints. Swiss QR recognition can take longer than a standard barcode read, and the official sample uses an extended 20-retry, 10-second polling loop specifically because of that, not as boilerplate copied from another endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits in an accounts payable pipeline
&lt;/h2&gt;

&lt;p&gt;The realistic entry point for this endpoint is a folder, not a single file. Invoices land in a shared Dropbox, SharePoint, or email inbox, each one a PDF with a Swiss QR bill on it, arriving on no predictable schedule. Wiring Read Swiss QR Code into that intake point means every incoming invoice gets its payment data extracted automatically, at the moment it arrives, instead of sitting in a queue for someone to open and transcribe. PDF4me's own workflow write-ups show this pattern in practice: reading the creditor and reference straight out of a QR-bill PDF to auto-rename the file for filing, on &lt;a href="https://docs.pdf4me.com/blog/rename-pdfs-using-swissqr-make/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/blog/rename-pdfs-using-swissqr-zapier/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, and &lt;a href="https://docs.pdf4me.com/blog/rename-pdfs-using-swissqr-pdf4me-n8n/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;, so the manual "open PDF, find the reference number, type it into the ERP" step disappears from the process rather than getting faster.&lt;/p&gt;

&lt;p&gt;The same extracted data closes the loop the other direction too. A business that generates its own Swiss QR bills with PDF4me's &lt;a href="https://docs.pdf4me.com/pdf4me-api/barcode/create-swissqr-bill/" rel="noopener noreferrer"&gt;Create Swiss QR Bill&lt;/a&gt; endpoint can verify what actually printed by reading it back with this endpoint, useful as a sanity check before a batch of invoices goes out the door. And for teams processing invoice batches that arrive as one long combined PDF rather than individual files, &lt;a href="https://docs.pdf4me.com/pdf4me-api/merge-split/split-pdf-by-swiss-qr/" rel="noopener noreferrer"&gt;Split PDF by Swiss QR&lt;/a&gt; uses the same QR-bill boundary to break a multi-invoice batch into individual documents before extraction even starts, so the reading step operates on one invoice at a time instead of hunting for boundaries inside a combined file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into a no-code workflow
&lt;/h2&gt;

&lt;p&gt;None of this requires custom backend code to reach production. In &lt;a href="https://docs.pdf4me.com/integration/make/barcode/read-swissqr-code/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, Read Swiss QR Code is a scenario module that takes a watched file and returns the decoded payment fields directly into the scenario's data flow, ready to route into a Google Sheet, a database module, or a rename-and-file step. In &lt;a href="https://docs.pdf4me.com/integration/zapier/barcode/read-swissqr-code/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, the same extraction runs as a Zap step, triggered the moment a new file lands in a watched Dropbox or Drive folder, with the creditor, IBAN, amount, and reference coming back as fields a downstream Zap step can act on without a separate parsing action. In &lt;a href="https://docs.pdf4me.com/integration/power-automate/barcode/read-swissqr-code/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, it slots into a flow the same way, a natural fit for finance teams already running approval or filing flows on Microsoft's automation stack. In &lt;a href="https://docs.pdf4me.com/integration/n8n/barcode/read-swiss-qr-code/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;, it becomes a node inside a self-hosted workflow a team owns end to end, useful where the extracted data needs to feed a custom or internal accounting system.&lt;/p&gt;

&lt;p&gt;Across all four, the shape of the workflow is the same: a file appears, its payment data comes out as structured fields a few seconds later, and nobody has opened the PDF to read it themselves. Not that reading one QR bill by hand is slow, it takes seconds, but that it is one more manual step in a chain that was supposed to run without a person watching it, and every manual step in that chain is a place invoices get delayed, misfiled, or mistyped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeing it before writing a line of integration code
&lt;/h2&gt;

&lt;p&gt;Before wiring this into a production flow, it helps to see the real output. The &lt;a href="https://docs.pdf4me.com/url-api-tester/read-swissqr-code/" rel="noopener noreferrer"&gt;API Tester&lt;/a&gt; runs Read Swiss QR Code directly in the browser: upload a QR-bill PDF, call the endpoint, and see the actual structured JSON response, field names and all, before writing any integration code against it. Getting authenticated against the API in the first place is covered in &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;Connect to the PDF4me V2 API&lt;/a&gt;, which lays out the base URL, headers, and request format the code sample above builds on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limit
&lt;/h2&gt;

&lt;p&gt;This endpoint reads Swiss QR bills. It is built against the Swiss Payments Code standard specifically, not a general-purpose payment-QR decoder for every country's invoicing format. A business handling SEPA or other regional payment QR formats alongside Swiss ones needs to treat those as separate extraction problems, not assume one endpoint covers all of them. Within its actual scope, though, the value is specific and unglamorous in the best way: a field that used to require a human to look at a QR code and know what a Swiss Payments Code block means now arrives as JSON, and the person who used to retype IBANs from scanned invoices gets to stop doing that particular part of their job.&lt;/p&gt;




&lt;p&gt;Website: &lt;a href="https://pdf4me.com/" rel="noopener noreferrer"&gt;pdf4me.com&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a href="https://docs.pdf4me.com/" rel="noopener noreferrer"&gt;docs.pdf4me.com&lt;/a&gt;&lt;br&gt;
Developer portal: &lt;a href="https://dev.pdf4me.com/" rel="noopener noreferrer"&gt;dev.pdf4me.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>automation</category>
      <category>finance</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
